diff --git a/.github/workflows/cd-mobile-mcp.yml b/.github/workflows/cd-mobile-mcp.yml new file mode 100644 index 0000000000..00869ebcf9 --- /dev/null +++ b/.github/workflows/cd-mobile-mcp.yml @@ -0,0 +1,67 @@ +name: 'CD: mobile-mcp' + +on: + push: + tags: ['mobile-mcp-v*'] + workflow_dispatch: + inputs: + version: + description: 'Version to publish (without v prefix, e.g. 0.1.0)' + required: true + dry_run: + description: 'Dry run (build only, no publish)' + required: false + type: 'boolean' + default: true + +permissions: + contents: 'read' + id-token: 'write' + +jobs: + build-and-publish: + runs-on: 'ubuntu-latest' + steps: + - uses: 'actions/checkout@v4' + + - uses: 'actions/setup-node@v5' + with: + node-version: '22' + registry-url: 'https://registry.npmjs.org' + + - name: 'Determine version' + id: 'version' + run: | + if [[ "$GITHUB_REF" == refs/tags/mobile-mcp-v* ]]; then + VERSION="${GITHUB_REF#refs/tags/mobile-mcp-v}" + else + VERSION="${{ inputs.version }}" + fi + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + - name: 'Install dependencies' + working-directory: 'packages/mobile-mcp' + run: 'npm ci --ignore-scripts' + + - name: 'Lint' + working-directory: 'packages/mobile-mcp' + run: 'npm run lint' + + - name: 'Set version' + working-directory: 'packages/mobile-mcp' + run: 'npm version "${{ steps.version.outputs.version }}" --no-git-tag-version' + + - name: 'Build' + working-directory: 'packages/mobile-mcp' + run: 'npm run build' + + - name: 'Test' + working-directory: 'packages/mobile-mcp' + run: 'npx playwright test test/coord-norm.test.ts' + + - name: 'Publish' + if: '${{ !inputs.dry_run }}' + working-directory: 'packages/mobile-mcp' + run: 'npm publish --access public --provenance' + env: + NODE_AUTH_TOKEN: '${{ secrets.NPM_TOKEN }}' diff --git a/eslint.config.js b/eslint.config.js index 62db967445..f74416e232 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -34,6 +34,7 @@ export default tseslint.config( '.qwen/**', 'packages/desktop/**', 'packages/cua-driver/**', // vendored trycua/cua driver (Rust + scripts); not qwen-code TS + 'packages/mobile-mcp/**', // vendored mobile-next/mobile-mcp; has own eslint config ], }, eslint.configs.recommended, diff --git a/package-lock.json b/package-lock.json index fd328894a1..d961e8500b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1047,6 +1047,16 @@ "node": ">=18" } }, + "node_modules/@emnapi/runtime": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", + "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.6", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz", @@ -1842,6 +1852,471 @@ "import-meta-resolve": "^4.2.0" } }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@inquirer/confirm": { "version": "5.1.14", "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.14.tgz", @@ -2565,6 +3040,59 @@ "@chevrotain/types": "~11.1.1" } }, + "node_modules/@mobilewright/core": { + "version": "0.0.45", + "resolved": "https://registry.npmjs.org/@mobilewright/core/-/core-0.0.45.tgz", + "integrity": "sha512-5wtqoG4AOAkr3u+Gegodvt6aeuF2Qe6Y5P0/p1HL8S1lFZ035MCD96TxE0zEDYNVaYyVLIaIAN59ri2O3Vi+JQ==", + "license": "Apache-2.0", + "dependencies": { + "@mobilewright/protocol": "^0.0.45", + "debug": "^4.4.3", + "playwright-core": "1.58.2", + "sharp": "^0.34.5" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@mobilewright/driver-mobilecli": { + "version": "0.0.45", + "resolved": "https://registry.npmjs.org/@mobilewright/driver-mobilecli/-/driver-mobilecli-0.0.45.tgz", + "integrity": "sha512-6v8hyjw3jgP6TeH4yqtKfXphr2sSIZj0BhD013Juk1//AYOdnGdltC2c0kbdeWUdqFB+K3G4Kc32fBVi7Gbfgg==", + "license": "Apache-2.0", + "dependencies": { + "@mobilewright/protocol": "^0.0.45", + "debug": "^4.4.3", + "mobilecli": "0.3.84", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@mobilewright/driver-mobilenext": { + "version": "0.0.45", + "resolved": "https://registry.npmjs.org/@mobilewright/driver-mobilenext/-/driver-mobilenext-0.0.45.tgz", + "integrity": "sha512-xXGuAb+ihezvtETr883mIlxreqECjRdXSL8jbFgK4VhJSk1/P+98zWlTMQRMVgwKOg9IKv+GG6JXSkTFD08YKw==", + "license": "Apache-2.0", + "dependencies": { + "@mobilewright/protocol": "^0.0.45", + "debug": "^4.4.3", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@mobilewright/protocol": { + "version": "0.0.45", + "resolved": "https://registry.npmjs.org/@mobilewright/protocol/-/protocol-0.0.45.tgz", + "integrity": "sha512-bArKuq4f7woi9ysBiRb1UWxKGHgZ+vwEH8WbpMYXXoEelDTpes0MZfMs4YNWisYSdwl4IRLiHzseQqMkJDPePQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, "node_modules/@modelcontextprotocol/sdk": { "version": "1.29.0", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", @@ -2665,6 +3193,18 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@nodable/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-9uGyhaQavEUMC8AIddIjau4NsnsXhou+j5sBAGojCM1oxmQpVKTWR/9JxABD6UAv12vpIms55fPZKFQEhG6uBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -3305,6 +3845,69 @@ "node": ">=14" } }, + "node_modules/@playwright/test": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz", + "integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@playwright/test/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/@playwright/test/node_modules/playwright": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", + "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/@playwright/test/node_modules/playwright-core": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", + "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@pnpm/config.env-replace": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", @@ -3450,6 +4053,10 @@ "resolved": "packages/chrome-extension", "link": true }, + "node_modules/@qwen-code/mobile-mcp": { + "resolved": "packages/mobile-mcp", + "link": true + }, "node_modules/@qwen-code/qwen-code": { "resolved": "packages/cli", "link": true @@ -4341,6 +4948,39 @@ "node": ">=6" } }, + "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/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@tanstack/react-virtual": { "version": "3.14.2", "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.14.2.tgz", @@ -4796,6 +5436,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/commander": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@types/commander/-/commander-2.12.0.tgz", + "integrity": "sha512-DDmRkovH7jPjnx7HcbSnqKg2JeNANyxNZeUvB0iE+qKBLN+vzN5iSIwt+J2PFSmBuYEut4mgQvI/fTX9YQH/vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "*" + } + }, "node_modules/@types/configstore": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/@types/configstore/-/configstore-6.0.2.tgz", @@ -5237,6 +5887,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "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", @@ -6839,6 +7496,18 @@ "node": ">= 8" } }, + "node_modules/anynum": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.1.tgz", + "integrity": "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/archiver": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", @@ -7701,6 +8370,40 @@ "node": ">= 0.8" } }, + "node_modules/c8": { + "version": "10.1.3", + "resolved": "https://registry.npmjs.org/c8/-/c8-10.1.3.tgz", + "integrity": "sha512-LvcyrOAaOnrrlMpW22n690PUvxiq4Uf9WMhQwNJ9vgagkL/ph1+D4uvjvDA5XCbykrc0sx+ay6pVi9YZ1GnhyA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.1", + "@istanbuljs/schema": "^0.1.3", + "find-up": "^5.0.0", + "foreground-child": "^3.1.1", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.1.6", + "test-exclude": "^7.0.1", + "v8-to-istanbul": "^9.0.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1" + }, + "bin": { + "c8": "bin/c8.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "monocart-coverage-reports": "^2" + }, + "peerDependenciesMeta": { + "monocart-coverage-reports": { + "optional": true + } + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -9601,9 +10304,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, "license": "Apache-2.0", - "optional": true, "engines": { "node": ">=8" } @@ -10416,6 +11117,19 @@ "ms": "^2.1.1" } }, + "node_modules/eslint-plugin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin/-/eslint-plugin-1.0.1.tgz", + "integrity": "sha512-ervp8C09On0fLA258TvE08AqAr/bhRYgHVZd3BrJjD4JfOA2JGANDLGs06j51oWqfPd7Feoo3OoqHD+fuI2sFQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "requireindex": "~1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/eslint-plugin-check-file": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/eslint-plugin-check-file/-/eslint-plugin-check-file-3.3.1.tgz", @@ -10510,6 +11224,21 @@ "requireindex": "^1.2.0" } }, + "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-plugin-react": { "version": "7.37.5", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", @@ -10765,6 +11494,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/eslint-plugin/node_modules/requireindex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.1.0.tgz", + "integrity": "sha512-LBnkqsDE7BZKvqylbmn7lTIVdpx4K/QCduRATpO5R+wtPmky/a8pN1bO2D6wXppn1497AJF9mNjqAXr6bdl9jg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.5" + } + }, "node_modules/eslint-scope": { "version": "8.4.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", @@ -11240,6 +11979,44 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fast-xml-builder": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.1.tgz", + "integrity": "sha512-tPb5TTWfgfVx5BNSi2xV0eLr89POeXXn0dXIsCJ9m1narrWxeIyx6je9d7Rce/3NyXLbvuQmLkxq+RuxMWejvw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.5.0", + "xml-naming": "^0.1.0" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.8.0.tgz", + "integrity": "sha512-6bIM7fsJxeo3uXv7OncQYsBAMPJ7V16Slahl/6M98C/i2q+vB1+4a0MtrvYwDFEUrwDSbAmeLDRXsOBwrL7yAg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.2.0", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.3.0", + "xml-naming": "^0.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", @@ -11339,6 +12116,13 @@ "url": "https://opencollective.com/express" } }, + "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", @@ -15455,6 +16239,13 @@ "node": ">= 0.6" } }, + "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/micromark": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", @@ -16337,6 +17128,46 @@ "obliterator": "^2.0.4" } }, + "node_modules/mobilecli": { + "version": "0.3.84", + "resolved": "https://registry.npmjs.org/mobilecli/-/mobilecli-0.3.84.tgz", + "integrity": "sha512-TOyyavbO5mtzMlJ6vdAZbtjgH4GM392biOxiKDV9s4lMPzA0TSUphBZVCOKsCN03kotNJA2JytdY500H6iHvJw==", + "license": "MIT", + "bin": { + "mobilecli": "index.js" + } + }, + "node_modules/mobilewright": { + "version": "0.0.45", + "resolved": "https://registry.npmjs.org/mobilewright/-/mobilewright-0.0.45.tgz", + "integrity": "sha512-z9QJRtiGBax310zAhoMBvULm8uKnVPcVn+ELhII1E1LLPJDzoGYhFcVLfAzS8ela99KqYRKFS0D75ayCpENhFQ==", + "license": "Apache-2.0", + "dependencies": { + "@mobilewright/core": "^0.0.45", + "@mobilewright/driver-mobilecli": "^0.0.45", + "@mobilewright/driver-mobilenext": "^0.0.45", + "@mobilewright/protocol": "^0.0.45", + "commander": "^14.0.3", + "debug": "^4.4.3", + "playwright": "1.58.2", + "ws": "^8.18.0" + }, + "bin": { + "mobilewright": "dist/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/mobilewright/node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, "node_modules/mock-fs": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-5.5.0.tgz", @@ -17682,6 +18513,21 @@ "node": ">=8" } }, + "node_modules/path-expression-matcher": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.1.tgz", + "integrity": "sha512-h7bxdzhHk8Knyc4Tj+jMaa7fEEoUJy7p1qtbVgkYg1Uhpe5Np5VuGXCRZnkZvU+Q42M1vStt0ifa3ueykRJPmQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -17852,13 +18698,12 @@ } }, "node_modules/playwright": { - "version": "1.57.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", - "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", - "dev": true, + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.57.0" + "playwright-core": "1.58.2" }, "bin": { "playwright": "cli.js" @@ -17871,10 +18716,9 @@ } }, "node_modules/playwright-core": { - "version": "1.57.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", - "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", - "dev": true, + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" @@ -17887,14 +18731,12 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, "os": [ "darwin" ], - "peer": true, "engines": { "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } @@ -19733,6 +20575,50 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -20526,6 +21412,21 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/strnum": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.4.1.tgz", + "integrity": "sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "anynum": "^1.0.1" + } + }, "node_modules/structured-source": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/structured-source/-/structured-source-4.0.0.tgz", @@ -22115,6 +23016,21 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -22825,6 +23741,21 @@ "node": ">=18" } }, + "node_modules/xml-naming": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", + "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/xml2js": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", @@ -23630,6 +24561,254 @@ "node": ">=18.17" } }, + "packages/mobile-mcp": { + "name": "@qwen-code/mobile-mcp", + "version": "0.0.1", + "license": "Apache-2.0", + "dependencies": { + "@modelcontextprotocol/sdk": "1.26.0", + "ajv": "^8.18.0", + "commander": "14.0.0", + "express": "5.1.0", + "fast-xml-parser": "5.8.0", + "mobilewright": "0.0.45", + "qs": "^6.15.0", + "zod": "^4.1.13", + "zod-to-json-schema": "3.25.0" + }, + "bin": { + "mcp-server-mobile": "lib/index.js" + }, + "devDependencies": { + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "^9.19.0", + "@playwright/test": "^1.49.0", + "@stylistic/eslint-plugin": "^3.0.1", + "@types/commander": "^2.12.0", + "@types/express": "^5.0.3", + "@types/node": "^22.13.10", + "@typescript-eslint/eslint-plugin": "^8.28.0", + "@typescript-eslint/parser": "^8.26.1", + "@typescript-eslint/utils": "^8.26.1", + "c8": "^10.1.3", + "eslint": "^9.19.0", + "eslint-plugin": "^1.0.1", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-notice": "^1.0.0", + "husky": "^9.1.7", + "typescript": "5.8.2" + }, + "engines": { + "node": ">=18" + } + }, + "packages/mobile-mcp/node_modules/@modelcontextprotocol/sdk": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", + "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "packages/mobile-mcp/node_modules/@modelcontextprotocol/sdk/node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "packages/mobile-mcp/node_modules/@modelcontextprotocol/sdk/node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + }, + "packages/mobile-mcp/node_modules/@types/node": { + "version": "22.20.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.0.tgz", + "integrity": "sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "packages/mobile-mcp/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "packages/mobile-mcp/node_modules/commander": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.0.tgz", + "integrity": "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "packages/mobile-mcp/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" + } + }, + "packages/mobile-mcp/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "packages/mobile-mcp/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" + } + }, + "packages/mobile-mcp/node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "packages/mobile-mcp/node_modules/zod-to-json-schema": { + "version": "3.25.0", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.0.tgz", + "integrity": "sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" + } + }, "packages/sdk-typescript": { "name": "@qwen-code/sdk", "version": "0.1.8", diff --git a/packages/mobile-mcp/.gitignore b/packages/mobile-mcp/.gitignore new file mode 100644 index 0000000000..85b563feaf --- /dev/null +++ b/packages/mobile-mcp/.gitignore @@ -0,0 +1,7 @@ +**/.DS_Store +node_modules +lib +.vscode +coverage +test-results +playwright-report \ No newline at end of file diff --git a/packages/mobile-mcp/.vendored-from b/packages/mobile-mcp/.vendored-from new file mode 100644 index 0000000000..6c656e971d --- /dev/null +++ b/packages/mobile-mcp/.vendored-from @@ -0,0 +1 @@ +c5d7d27fd diff --git a/packages/mobile-mcp/.vendored-patches.md b/packages/mobile-mcp/.vendored-patches.md new file mode 100644 index 0000000000..1068317c85 --- /dev/null +++ b/packages/mobile-mcp/.vendored-patches.md @@ -0,0 +1,22 @@ +# Local patches on top of upstream mobile-next/mobile-mcp + +Vendored from: `c5d7d27fd` (upstream v0.0.61, main branch) + +Upstream sync mechanism: `git subtree pull` (see `scripts/sync-from-upstream.sh`). + +## Local modifications + +| Patch | Description | Touches | +| -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- | +| Rename to @qwen-code/mobile-mcp | npm package name, repository field | `package.json` | +| Disable upstream telemetry | Fork doesn't phone home to upstream PostHog | `src/server.ts` | +| Relative coordinate shim | Opt-in 0–1000 normalized coordinates (env `MOBILE_MCP_COORDINATE_SPACE`) | `src/coord-norm.ts`, `src/server.ts` | +| InstallOptions + Android tools | Extended `installApp` with options; added `dumpUiHierarchy`, `pullFile`, `pushFile`; MCP tools `mobile_ui_dump`, `mobile_adb_pull`, `mobile_adb_push` | `src/robot.ts`, `src/android.ts`, `src/server.ts` | +| getAndroidRobotFromDevice helper | DRY extraction for Android-only tool device validation | `src/server.ts` | + +## When syncing upstream + +After `git subtree pull`, expect conflicts in `src/server.ts` (our coord hooks + new tools) +and `package.json` (name/repo). The coord shim (`src/coord-norm.ts`) is a new file and +won't conflict. Backend files (`android.ts`, `ios.ts`, etc.) are only touched for the +InstallOptions signature change — low conflict surface. diff --git a/packages/mobile-mcp/CHANGELOG.md b/packages/mobile-mcp/CHANGELOG.md new file mode 100644 index 0000000000..f8d3314e2f --- /dev/null +++ b/packages/mobile-mcp/CHANGELOG.md @@ -0,0 +1,240 @@ +## [0.0.61](https://github.com/mobile-next/mobile-mcp/releases/tag/0.0.61) (2026-07-02) +* Fix: Catch system signals for clean v8 exit ([#366](https://github.com/mobile-next/mobile-mcp/pull/366)) +* Fix: Detect go-ios installed via npm (bare semver version) ([#355](https://github.com/mobile-next/mobile-mcp/pull/355)), thanks to [@ravijagga](https://github.com/ravijagga) +* Docs: Added CONTRIBUTING.md ([#365](https://github.com/mobile-next/mobile-mcp/pull/365)) +* Chore: Updated hono packages for security ([#367](https://github.com/mobile-next/mobile-mcp/pull/367)) + +## [0.0.60](https://github.com/mobile-next/mobile-mcp/releases/tag/0.0.60) (2026-06-15) +* Chore: Updated mobilewright SDK version ([#361](https://github.com/mobile-next/mobile-mcp/pull/361)) + +## [0.0.59](https://github.com/mobile-next/mobile-mcp/releases/tag/0.0.59) (2026-06-09) +* Chore: Updated hono packages for security ([#349](https://github.com/mobile-next/mobile-mcp/pull/349)) +* Chore: Updated mobilewright SDK version ([#350](https://github.com/mobile-next/mobile-mcp/pull/350)) + +## [0.0.58](https://github.com/mobile-next/mobile-mcp/releases/tag/0.0.58) (2026-06-02) +* Fix: Understand getRobot device types and platforms ([#346](https://github.com/mobile-next/mobile-mcp/pull/346)) +* Chore: Moved tests from mocha/nyc to playwright to reduce dependency vulnerabilities ([#347](https://github.com/mobile-next/mobile-mcp/pull/347)) + +## [0.0.57](https://github.com/mobile-next/mobile-mcp/releases/tag/0.0.57) (2026-05-28) +* Chore: Update mobilewright to 0.0.41 ([#341](https://github.com/mobile-next/mobile-mcp/pull/341)) + +## [0.0.56](https://github.com/mobile-next/mobile-mcp/releases/tag/0.0.56) (2026-05-17) +* Chore: Update mobilewright to 0.0.38 ([#338](https://github.com/mobile-next/mobile-mcp/pull/338)) +* Fix: Restore mcp-publisher functionality ([#337](https://github.com/mobile-next/mobile-mcp/pull/337)) + +## [0.0.55](https://github.com/mobile-next/mobile-mcp/releases/tag/0.0.55) (2026-05-16) +* General: Replace mobilecli with mobilewright SDK ([#334](https://github.com/mobile-next/mobile-mcp/pull/334)) +* General: Bump fast-xml-parser to 5.8.0 to fix security vulnerability ([#335](https://github.com/mobile-next/mobile-mcp/pull/335)) + +## [0.0.54](https://github.com/mobile-next/mobile-mcp/releases/tag/0.0.54) (2026-05-04) +* Server: Update mobilecli to 0.3.70 +* iOS: Fixed cases where testmanagerd would get Device Kit stuck in a black screen of death +* iOS: Added 'placeholder' to view tree response + +## [0.0.53](https://github.com/mobile-next/mobile-mcp/releases/tag/0.0.53) (2026-05-01) +* Server: Add `mobile_list_crashes` tool to list crash reports on a device +* Server: Add `mobile_get_crash` tool to retrieve full crash report content +* Server: Upgrade mobilecli from 0.2.0 to 0.3.68 +* iOS: Replaced use of WebdriverAgent with iOS Device Kit (open source, apache license) +* CI: Restrict `contents` permission to `read` +* CI: Remove Java setup step from build workflow + +## [0.0.52](https://github.com/mobile-next/mobile-mcp/releases/tag/0.0.52) (2026-04-13) +* Server: Block cross-origin requests on SSE transport ([#311](https://github.com/mobile-next/mobile-mcp/pull/311)) +* Server: Warn when SSE server starts without `MOBILEMCP_AUTH` set ([#311](https://github.com/mobile-next/mobile-mcp/pull/311)) +* Server: Reject concurrent SSE connections instead of silently replacing them ([#311](https://github.com/mobile-next/mobile-mcp/pull/311)) +* Server: Clear SSE transport on connection close to allow reconnection ([#311](https://github.com/mobile-next/mobile-mcp/pull/311)) +* Server: Update mobilecli dependency from @mobilenext/mobilecli to mobilecli 0.2.0 ([#316](https://github.com/mobile-next/mobile-mcp/pull/316)) +* CI: Fix script injection by passing `github.ref_name` through env vars ([#314](https://github.com/mobile-next/mobile-mcp/pull/314)) +* CI: Use `npm ci` instead of `npm install` for reproducible builds ([#314](https://github.com/mobile-next/mobile-mcp/pull/314)) +* CI: Pin mcp-publisher to version 1.5.0 ([#314](https://github.com/mobile-next/mobile-mcp/pull/314)) +* CI: Remove `npm update` from tag release to preserve lockfile integrity ([#314](https://github.com/mobile-next/mobile-mcp/pull/314)) + +## [0.0.51](https://github.com/mobile-next/mobile-mcp/releases/tag/0.0.51) (2026-04-03) +* Server: Replace `--port` with `--listen` flag accepting `[host:]port` format, default to localhost ([#306](https://github.com/mobile-next/mobile-mcp/pull/306)) +* Server: Add optional Bearer token auth via `MOBILEMCP_AUTH` env variable ([#306](https://github.com/mobile-next/mobile-mcp/pull/306)) +* Server: Add `MOBILEMCP_DISABLE_TELEMETRY` env variable to opt out of anonymous telemetry ([#305](https://github.com/mobile-next/mobile-mcp/pull/305)) +* Server: Security update for path-to-regexp package ([#307](https://github.com/mobile-next/mobile-mcp/pull/307)) + +## [0.0.50](https://github.com/mobile-next/mobile-mcp/releases/tag/0.0.50) (2026-03-27) +* Server: Restrict open_url tool to http/https schemes unless `MOBILEMCP_ALLOW_UNSAFE_URLS=1` is set ([#299](https://github.com/mobile-next/mobile-mcp/pull/299)) thanks to [@manthanghasadiya](https://github.com/manthanghasadiya) for reporting this. + +## [0.0.49](https://github.com/mobile-next/mobile-mcp/releases/tag/0.0.49) (2026-03-24) +* Server: Fix path traversal in save screenshot and record video ([#296](https://github.com/mobile-next/mobile-mcp/pull/296)) thanks to [@AbhiTheModder](https://github.com/AbhiTheModder) for reporting this. + +## [0.0.48](https://github.com/mobile-next/mobile-mcp/releases/tag/0.0.48) (2026-03-20) +* Server: Security updates for fast-xml-parser ([#292](https://github.com/mobile-next/mobile-mcp/pull/292)) +* Server: Fix handling errors in getDeviceType to prevent empty device list ([#286](https://github.com/mobile-next/mobile-mcp/pull/286)) thanks to [@ls-andy](https://github.com/ls-andy) + +## [0.0.47](https://github.com/mobile-next/mobile-mcp/releases/tag/0.0.47) (2026-03-09) +* Server: Use zod coerce to fix number parameter parsing ([#284](https://github.com/mobile-next/mobile-mcp/pull/284)) +* Server: Updated packages for security ([#285](https://github.com/mobile-next/mobile-mcp/pull/285)) +* iOS: Support locales when launching apps ([#283](https://github.com/mobile-next/mobile-mcp/pull/283)) +* Android: Support locales when launching apps ([#283](https://github.com/mobile-next/mobile-mcp/pull/283)) + +## [0.0.46](https://github.com/mobile-next/mobile-mcp/releases/tag/0.0.46) (2026-03-03) +* Server: Improved tool description for listing devices, easier on prompting ([#282](https://github.com/mobile-next/mobile-mcp/pull/282)) +* iOS: Added support for screen recording for both real devices and simulators ([#282](https://github.com/mobile-next/mobile-mcp/pull/282)) +* Android: Added support for screen recording for both real devices and emulators ([#282](https://github.com/mobile-next/mobile-mcp/pull/282)) + +## [0.0.45](https://github.com/mobile-next/mobile-mcp/releases/tag/0.0.45) (2026-03-02) +* Server: Updated fast-xml-parser package for security ([#281](https://github.com/mobile-next/mobile-mcp/pull/281)) +* Server: Fix noParams issue that started annoying Claude Code recently ([#280](https://github.com/mobile-next/mobile-mcp/pull/280)) +* Android: Fix shell escaping through launchApp ([#279](https://github.com/mobile-next/mobile-mcp/pull/279)) thanks to [@yuhanghuang](https://github.com/yuhanghuang) +* Android: Escape url when calling openUrl ([#278](https://github.com/mobile-next/mobile-mcp/pull/278)) thanks to [yuhanghuang](https://github.com/yuhanghuang) + +## [0.0.44](https://github.com/mobile-next/mobile-mcp/releases/tag/0.0.44) (2026-02-25) +* General: Rolling out support for remote devices, allocate Android and iOS devices on Mobile Fleet ([#273](https://github.com/mobile-next/mobile-mcp/pull/273)) + +## [0.0.43](https://github.com/mobile-next/mobile-mcp/releases/tag/0.0.43) (2026-02-24) +* General: Increase buffers used for screenshots, fixes bugs where screenshot was >4MB ([#270](https://github.com/mobile-next/mobile-mcp/pull/270)) +* General: Upgraded several npm packages for security ([#272](https://github.com/mobile-next/mobile-mcp/pull/272)) + +## [0.0.42](https://github.com/mobile-next/mobile-mcp/releases/tag/0.0.42) (2026-02-03) +* General: Upgraded mobilecli to 0.0.54, [see changes](https://github.com/mobile-next/mobilecli/releases) ([ba3ec1b](https://github.com/mobile-next/mobile-mcp/commit/ba3ec1b9251487ad8444eb22fa3312c7b79d7787)) +* General: Updated fast-xml-parser package for security ([#261](https://github.com/mobile-next/mobile-mcp/pull/261)) + +## [0.0.41](https://github.com/mobile-next/mobile-mcp/releases/tag/0.0.41) (2026-01-27) +* General: upgraded mobilecli to 0.0.52, [see changes](https://github.com/mobile-next/mobilecli/releases) ([d7e25f3](https://github.com/mobile-next/mobile-mcp/commit/d7e25f3543e87a436572c29f2b1766bd276a4d68)) +* Android: fix: include elements with resource-id or checkable attributes ([#254](https://github.com/mobile-next/mobile-mcp/pull/254)) by [@singhsume123](https://github.com/singhsume123) + +## [0.0.40](https://github.com/mobile-next/mobile-mcp/releases/tag/0.0.40) (2026-01-15) +* Server: bump @modelcontextprotocol/sdk from 1.24.2 to 1.25.2 for security ([#252](https://github.com/mobile-next/mobile-mcp/pull/252)) + +## [0.0.39](https://github.com/mobile-next/mobile-mcp/releases/tag/0.0.39) (2026-01-01) +* Server: added tool annotations for improved LLM tool understanding ([#246](https://github.com/mobile-next/mobile-mcp/pull/246)) +* iOS: added 'duration' parameter to longpress for custom press durations ([#247](https://github.com/mobile-next/mobile-mcp/pull/247)) +* Android: added 'duration' parameter to longpress for custom press durations ([#247](https://github.com/mobile-next/mobile-mcp/pull/247)) + +## [0.0.38](https://github.com/mobile-next/mobile-mcp/releases/tag/0.0.38) (2025-12-09) +* iOS: migrated iPhone Simulator calls to use mobilecli binary for improved performance ([#241](https://github.com/mobile-next/mobile-mcp/pull/241)) +* iOS: automatically downloading and installing Webdriver Agent on simulator, get started in iOS development in seconds ([#241](https://github.com/mobile-next/mobile-mcp/pull/241)) + +## [0.0.37](https://github.com/mobile-next/mobile-mcp/releases/tag/0.0.37) (2025-12-08) +* Server: migrated to new mcp sdk tool registration API ([#239](https://github.com/mobile-next/mobile-mcp/pull/239)) +* Server: updated to @modelcontextprotocol/sdk 1.24.2 and other dependencies for security ([#239](https://github.com/mobile-next/mobile-mcp/pull/239)) + +## [0.0.36](https://github.com/mobile-next/mobile-mcp/releases/tag/0.0.36) (2025-11-19) +* Server: upgraded libraries (glob, js-yaml) and mobilecli ([#234](https://github.com/mobile-next/mobile-mcp/pull/234)) + +## [0.0.35](https://github.com/mobile-next/mobile-mcp/releases/tag/0.0.35) (2025-11-14) +* Server: added list of available MCP tools to README for better discoverability ([043cf3d](https://github.com/mobile-next/mobile-mcp/commit/043cf3d)) +* Android: fixed adb path resolution on Windows by always using .exe extension ([178b2fb](https://github.com/mobile-next/mobile-mcp/commit/178b2fb)) by [@mattheww-skyward](https://github.com/mattheww-skyward) + +## [0.0.34](https://github.com/mobile-next/mobile-mcp/releases/tag/0.0.34) (2025-11-01) +* Server: dry-run release for benchmarking how mobilecli detects devices ([#226](https://github.com/mobile-next/mobile-mcp/pull/226)) + +## [0.0.33](https://github.com/mobile-next/mobile-mcp/releases/tag/0.0.33) (2025-10-20) +* Server: added debug information for understanding screenshot issues on old devices ([#213](https://github.com/mobile-next/mobile-mcp/pull/213)) + +## [0.0.32](https://github.com/mobile-next/mobile-mcp/releases/tag/0.0.32) (2025-10-08) +* Server: fixed wrong separator when listing iOS simulators ([#208](https://github.com/mobile-next/mobile-mcp/pull/208)) +* iOS: double tap at screen location ([#207](https://github.com/mobile-next/mobile-mcp/pull/207)) +* Android: reduce stdout pollution by adb shell monkey ([#211](https://github.com/mobile-next/mobile-mcp/pull/211)) +* Android: fix mobile_take_screenshot on very old android devices ([#204](https://github.com/mobile-next/mobile-mcp/pull/204)) by [@boulaycote](https://github.com/boulaycote) +* Android: double tap at screen location ([#194](https://github.com/mobile-next/mobile-mcp/pull/194)) by [@SakshamSahgal](https://github.com/SakshamSahgal) + +## [0.0.31](https://github.com/mobile-next/mobile-mcp/releases/tag/0.0.31) (2025-10-07) +* Server: resolve mobilecli libc issues on very old linux distros ([#206](https://github.com/mobile-next/mobile-mcp/pull/206)) +* Server: identify mcp-client for compatiblity patches ([#205](https://github.com/mobile-next/mobile-mcp/pull/205)) + +## [0.0.30](https://github.com/mobile-next/mobile-mcp/releases/tag/0.0.30) (2025-10-06) +* Server: introduction of mobilecli tool, will replace imagemagick, sips, go-ios and adb in the future ([#196](https://github.com/mobile-next/mobile-mcp/pull/196)) +* iOS: app installation and uninstallation ([#202](https://github.com/mobile-next/mobile-mcp/pull/202)) +* Android: app installation and uninstallation ([#202](https://github.com/mobile-next/mobile-mcp/pull/202)) + +## [0.0.29](https://github.com/mobile-next/mobile-mcp/releases/tag/0.0.29) (2025-09-26) +* Server: bumped mcp sdk to latest version ([#199](https://github.com/mobile-next/mobile-mcp/pull/199)) +* Server: locked production npm packages to specific version ([#199](https://github.com/mobile-next/mobile-mcp/pull/199)) +* Server: renamed tool 'swipe_on_screen' to 'mobile_swipe_on_screen' ([#197](https://github.com/mobile-next/mobile-mcp/pull/197)) + +## [0.0.28](https://github.com/mobile-next/mobile-mcp/releases/tag/0.0.28) (2025-09-15) +* Server: added 'device' parameter to all tools ([#181](https://github.com/mobile-next/mobile-mcp/pull/181)) +* Server: enable agents to access multiple devices at once (eg, 'explain what's on screen on all devices connected') + ([#181](https://github.com/mobile-next/mobile-mcp/pull/181)) + +## [0.0.27](https://github.com/mobile-next/mobile-mcp/releases/tag/0.0.27) (2025-09-10) +* Server: use 'sips' image scaling on mac if found, removes requirement to install ImageMagick for image scaling ([#188](https://github.com/mobile-next/mobile-mcp/pull/188)) + +## [0.0.26](https://github.com/mobile-next/mobile-mcp/releases/tag/0.0.26) (2025-09-09) +* Server: support listing of mobile-mcp in github's mcp registry ([e96404e](https://github.com/mobile-next/mobile-mcp/commit/e96404e0e513e48ebcfe7956800203cc0f363526)) + +## [0.0.25](https://github.com/mobile-next/mobile-mcp/releases/tag/0.0.25) (2025-09-08) +* Server: install mobile-mcp in vscode with a single-click in README ([#173](https://github.com/mobile-next/mobile-mcp/pull/173)) +* Android: try finding 'adb' under $HOME/Library/Android if $ANDROID_HOME is not defined ([#183](https://github.com/mobile-next/mobile-mcp/pull/183)) +* Android: better escaping of text input, for improved security ([#182](https://github.com/mobile-next/mobile-mcp/pull/183)) + +## [0.0.24](https://github.com/mobile-next/mobile-mcp/releases/tag/0.0.24) (2025-08-24) +* iOS: new tool for long press ([#143](https://github.com/mobile-next/mobile-mcp/pull/143)) +* Android: new tool for long press ([#143](https://github.com/mobile-next/mobile-mcp/pull/143)) +* Android: fixed screenshot from devices with multiple devices (foldables) again ([#171](https://github.com/mobile-next/mobile-mcp/pull/171)) + +## [0.0.23](https://github.com/mobile-next/mobile-mcp/releases/tag/0.0.23) (2025-07-31) +* Android: fixed a bug where devices with multiple screens (such as foldables) failed to take and save screenshot ([#159](https://github.com/mobile-next/mobile-mcp/pull/159)) + +## [0.0.22](https://github.com/mobile-next/mobile-mcp/releases/tag/0.0.22) (2025-07-17) +* iOS: fixed detection of go-ios installation ([#132](https://github.com/mobile-next/mobile-mcp/pull/132) by [@codeaholicguy](https://github.com/codeaholicguy) + +## [0.0.21](https://github.com/mobile-next/mobile-mcp/releases/tag/0.0.21) (2025-06-27) +* Server: use node: prefixed modules (like node:fs) ([449c498](https://github.com/mobile-next/mobile-mcp/commit/449c498e6e9a3e68aab55ea82f15c296171fc05e)) +* iOS: automatically start WebDriverAgent on simulator if already installed ([#126](https://github.com/mobile-next/mobile-mcp/pull/126)) +* Android: fixed detection of com.mobilenext.devicekit when running mcp on windows ([c11c642](https://github.com/mobile-next/mobile-mcp/commit/c11c6427c71cb7cef6ce87005047df977f6bea8a)) + +## [0.0.20](https://github.com/mobile-next/mobile-mcp/releases/tag/0.0.20) (2025-06-23) +* Server: new tool `save_screenshot` which saves the screenshot to disk, to be used by other mcp servers ([#112](https://github.com/mobile-next/mobile-mcp/pull/112)) +* Server: new tool `use_default_device` which picks the only device that is connected, to speed up use ([#112](https://github.com/mobile-next/mobile-mcp/pull/112)) +* iOS: Use wda to grab screenshots for both real devices and simulators ([#115](https://github.com/mobile-next/mobile-mcp/pull/115)) +* Android: Support for utf-8 text in sendKeys, see [wiki page]() for getting started ([#117](https://github.com/mobile-next/mobile-mcp/pull/117)) + +## [0.0.19](https://github.com/mobile-next/mobile-mcp/releases/tag/0.0.19) (2025-06-16) +* Server: Fixed support for Windsurf, where some tools caused a -32602 error ([#101](https://github.com/mobile-next/mobile-mcp/pull/101)) by [@amebahead](https://github.com/amebahead) +* iOS: Support for swipe left and right. Support x,y,direction,duration for custom swipes ([#92](https://github.com/mobile-next/mobile-mcp/pull/92/)) by [@benlmyers](https://github.com/benlmyers) +* Android: Support for swipe left and right. Support x,y,direction,duration for custom swipes ([#92](https://github.com/mobile-next/mobile-mcp/pull/92/)) by [@benlmyers](https://github.com/benlmyers) +* Android: Fix for get elements on screen, where uiautomator prints out warnings before the actual xml ([#86](https://github.com/mobile-next/mobile-mcp/pull/86)) by [@wenerme](https://github.com/wenerme) + +## [0.0.18](https://github.com/mobile-next/mobile-mcp/releases/tag/0.0.18) (2025-06-12) +* Server: New support for SSE (Server-Sent-Events) transport, [see wiki for more information](https://github.com/mobile-next/mobile-mcp/wiki/Using-SSE-Transport) ([1b70d40](https://github.com/mobile-next/mobile-mcp/commit/1b70d403cd562a97a0723464f2b286f2fd6eee0a)) +* iOS: Using plutil for `simctl listapps` parsing, might probably fix some parsing issues ([cfba3aa](https://github.com/mobile-next/mobile-mcp/commit/cfba3aaac5beb66d08d1138fe42c924309ede303)) +* Other: We have a new Slack server, join us at http://mobilenexthq.com/join-slack + +## [0.0.17](https://github.com/mobile-next/mobile-mcp/releases/tag/0.0.17) (2025-05-16) +* iOS: Fixed parsing of simctl listapps where CFBundleDisplayName contains non-alphanumerical characters ([#59](https://github.com/mobile-next/mobile-mcp/issues/59)) ([bf19771d](https://github.com/mobile-next/mobile-mcp/pull/63/commits/bf19771dcd49444ba4841ec649e3a72a03b54c74)) + +## [0.0.16](https://github.com/mobile-next/mobile-mcp/releases/tag/0.0.16) (2025-05-10) +* Server: Detect if there is a new version of the mcp and notify user ([14b015f](https://github.com/mobile-next/mobile-mcp/commit/14b015f29ab47aa1f3ae122a670a58eb7ef51fd8)) +* Server: Instead of returning x,y for tap, return [top,left,width,height] of elements on screen ([3169d2f](https://github.com/mobile-next/mobile-mcp/commit/3169d2f46f0c789e4c3188e137ac645d6f6eb27c)) +* iOS: Fixed coordinates location for iOS with retina display after image scaledown ([3169d2f](https://github.com/mobile-next/mobile-mcp/commit/3169d2f46f0c789e4c3188e137ac645d6f6eb27c)) +* iOS: Added detection of StaticText and Image in mobile_list_elements_on_screen ([debe75b](https://github.com/mobile-next/mobile-mcp/commit/debe75b5c8afcafcef8328201e9886bffdd1f128)) + +## [0.0.15](https://github.com/mobile-next/mobile-mcp/releases/tag/0.0.15) (2025-05-04) +* Android: Fixed broken Android screenshots on Windows because of crlf ([#53](https://github.com/mobile-next/mobile-mcp/pull/53/files) by [@hanyuan97](https://github.com/hanyuan97)) + +## [0.0.14](https://github.com/mobile-next/mobile-mcp/releases/tag/0.0.14) (2025-05-02) +* Server: Fix a bug where xcrun was required, now works on Linux as well ([7fddba7](https://github.com/mobile-next/mobile-mcp/commit/7fddba71af51690cfa76f81154f72c3120ab7f07)) +* Server: Removed dependency on sharp which was causing issues during installation, now ImageMagick is an optional dependency +* Android: Try uiautomator-dump multiple times, in case ui hierarchy is not stable +* Android: Return more information about elements on screen for better element detection +* Android: Support for Android TV using dpad for navigation ([399443d](https://github.com/mobile-next/mobile-mcp/commit/399443d519284a54b670a1598689a73d178db2ec) by [@surajsau](https://github.com/surajsau)) + +## [0.0.13](https://github.com/mobile-next/mobile-mcp/releases/tag/0.0.13) (2025-04-17) +* Server: Fix a bug where 'adb' is required to even work with iOS-only ([#30](https://github.com/mobile-next/mobile-mcp/issues/30)) ([867f662](https://github.com/mobile-next/mobile-mcp/pull/35/commits/867f662ac2edc68d542519bd72d1762d3dbca18d)) +* iOS: Support for orientation changes ([844dc0e](https://github.com/mobile-next/mobile-mcp/pull/28/commits/844dc0eb953169871b4cdd2a57735bf50abe721a)) +* Android: Support for orientation changes (eg 'change device to landscape') ([844dc0e](https://github.com/mobile-next/mobile-mcp/pull/28/commits/844dc0eb953169871b4cdd2a57735bf50abe721a)) +* Android: Improve element detection by using element name if label not found ([8e8aadf](https://github.com/mobile-next/mobile-mcp/pull/33/commits/8e8aadfd7f300ff5b7f0a7857a99d1103cd9e941) by [@tomoya0x00](https://github.com/tomoya0x00)) + +## [0.0.12](https://github.com/mobile-next/mobile-mcp/releases/tag/0.0.12) (2025-04-12) +* Server: If hitting an error with tunnel, forward proxy, wda, descriptive error and link to documentation will be returned +* iOS: go-ios path can be set in env GO_IOS_PATH +* iOS: Support go-ios that was built locally (no version) +* iOS: Return bundle display name for apps for better app launch +* iOS: Fixed finding element coordinates on retina displays +* iOS: Saving temporary screenshots onto temporary directory ([#19](https://github.com/mobile-next/mobile-mcp/issues/19)) +* iOS: Find elements better by removing off-screen and hidden elements +* Android: Support for 'adb' under ANDROID_HOME +* Android: Find elements better using accessibility hints and class names + +## [0.0.11](https://github.com/mobile-next/mobile-mcp/releases/tag/0.0.11) (2025-04-06) +* Server: Support submit after sending text (\n) +* Server: Added support for multiple devices at the same time +* iOS: Support for iOS physical devices using go-ios ([see wiki](https://github.com/mobile-next/mobile-mcp/wiki/Getting-Started-with-iOS-Physical-Device)) +* iOS: Added support for icons, search fields, and switches when getting elements on screen diff --git a/packages/mobile-mcp/LICENSE b/packages/mobile-mcp/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/packages/mobile-mcp/LICENSE @@ -0,0 +1,201 @@ + 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. + + Copyright [yyyy] [name of copyright owner] + + 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/packages/mobile-mcp/README.md b/packages/mobile-mcp/README.md new file mode 100644 index 0000000000..520381ee0b --- /dev/null +++ b/packages/mobile-mcp/README.md @@ -0,0 +1,137 @@ +# @qwen-code/mobile-mcp + +Fork of [mobile-next/mobile-mcp](https://github.com/mobile-next/mobile-mcp) for [qwen-code](https://github.com/QwenLM/qwen-code), with opt-in relative coordinate support and additional Android tooling. + +This package is an MCP server that enables LLM agents to interact with mobile devices (iOS and Android) through screenshots, accessibility elements, and coordinate-based touch actions. It supports simulators, emulators, and real devices. + +## Upstream + +Based on [mobile-next/mobile-mcp](https://github.com/mobile-next/mobile-mcp) v0.0.61 (`c5d7d27`). We track upstream via `git subtree` and will continue to sync updates. See [.vendored-patches.md](.vendored-patches.md) for local modifications and [scripts/sync-from-upstream.sh](scripts/sync-from-upstream.sh) for the sync mechanism. + +## What we added + +### 1. Opt-in 0-1000 relative coordinate mode + +Mirrors the [cua-driver relative coordinate shim](../cua-driver/docs/relative-coordinates-design.md) for mobile. When enabled, all coordinate inputs/outputs are normalized to a 0-1000 scale, matching the Qwen VL model's `computer_use` / `mobile_use` coordinate convention. + +**Environment variables:** + +| Variable | Values | Default | Description | +| ----------------------------- | -------------------- | ------- | -------------------------------------------------------- | +| `MOBILE_MCP_COORDINATE_SPACE` | `0` (off) / `1` (on) | `0` | Enable 0-1000 normalized coordinates | +| `MOBILE_MCP_COORDINATE_SCALE` | Any positive integer | `1000` | Full scale value (use `999` for `mobile_use` convention) | + +**How it works:** + +- **Input denormalization**: Coordinate tools (`mobile_click_on_screen_at_coordinates`, `mobile_double_tap_on_screen`, `mobile_long_press_on_screen_at_coordinates`, `mobile_swipe_on_screen`) convert 0-1000 input to device pixels/points before execution. +- **Output normalization**: `mobile_list_elements_on_screen` element coordinates are converted from pixels/points to 0-1000. `mobile_get_screen_size` reports 1000x1000. +- **Description rewriting**: Tool descriptions change from "in pixels" to "in 0-1000 normalized coordinates" when enabled. +- **Default off**: Zero behavior change when not configured. Fully backward compatible. + +The normalization basis is `getScreenSize()` — logical points on iOS, physical pixels on Android. The shim runs entirely in `server.ts`; backend files (`android.ts`, `ios.ts`, etc.) are untouched. + +### 2. Extended Android install options + +`mobile_install_app` now supports Android-specific flags: + +| Parameter | Flag | Description | +| ------------------- | ---- | ------------------------------------- | +| `replace` | `-r` | Replace existing app (default `true`) | +| `grant_permissions` | `-g` | Grant all runtime permissions | +| `allow_downgrade` | `-d` | Allow version code downgrade | +| `allow_test` | `-t` | Allow test APKs | + +iOS/simulator silently ignores these options. + +### 3. Android-specific tools + +| Tool | Description | +| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `mobile_ui_dump` | Dump the full UI hierarchy as raw XML via `uiautomator`. Unlike `mobile_list_elements_on_screen` (filtered flat JSON), this returns the complete unfiltered XML tree with all node attributes. Use for debugging or when `mobile_list_elements_on_screen` misses elements. Supports `--compressed`. | +| `mobile_adb_pull` | Pull a file from Android device to local filesystem | +| `mobile_adb_push` | Push a local file to Android device (default restricted to `/sdcard/`, `force=true` to override) | + +### 4. Telemetry disabled by default + +Upstream PostHog telemetry is off by default in this fork. Set `MOBILEMCP_ENABLE_TELEMETRY=1` to re-enable. + +## Available MCP Tools + +### Device Management + +- **`mobile_list_available_devices`** - List all available devices (simulators, emulators, and real devices) +- **`mobile_get_screen_size`** - Get the screen size of the mobile device +- **`mobile_get_orientation`** / **`mobile_set_orientation`** - Get/set screen orientation + +### App Management + +- **`mobile_list_apps`** - List all installed apps +- **`mobile_launch_app`** / **`mobile_terminate_app`** - Launch/terminate apps +- **`mobile_install_app`** - Install app with optional Android flags (-r/-g/-d/-t) +- **`mobile_uninstall_app`** - Uninstall app + +### Screen Interaction + +- **`mobile_take_screenshot`** / **`mobile_save_screenshot`** - Capture screen +- **`mobile_list_elements_on_screen`** - List UI elements with coordinates (cross-platform) +- **`mobile_click_on_screen_at_coordinates`** - Tap at x,y +- **`mobile_double_tap_on_screen`** - Double-tap at x,y +- **`mobile_long_press_on_screen_at_coordinates`** - Long press at x,y +- **`mobile_swipe_on_screen`** - Swipe in any direction + +### Input & Navigation + +- **`mobile_type_keys`** - Type text into focused element +- **`mobile_press_button`** - Press device buttons (HOME, BACK, etc.) +- **`mobile_open_url`** - Open URL in browser + +### Recording & Debugging + +- **`mobile_start_screen_recording`** / **`mobile_stop_screen_recording`** - Screen recording +- **`mobile_list_crashes`** / **`mobile_get_crash`** - Crash reports + +### Android Only + +- **`mobile_ui_dump`** - Full UI hierarchy XML dump +- **`mobile_adb_pull`** / **`mobile_adb_push`** - File transfer via ADB + +## Usage + +### MCP server configuration + +```json +{ + "mcpServers": { + "mobile-mcp": { + "command": "npx", + "args": ["@qwen-code/mobile-mcp"] + } + } +} +``` + +With relative coordinates enabled: + +```json +{ + "mcpServers": { + "mobile-mcp": { + "command": "npx", + "args": ["@qwen-code/mobile-mcp"], + "env": { + "MOBILE_MCP_COORDINATE_SPACE": "1" + } + } + } +} +``` + +### Prerequisites + +- **Android**: [Android SDK Platform Tools](https://developer.android.com/tools/releases/platform-tools) (`adb` on PATH) +- **iOS real devices**: [go-ios](https://github.com/danielpaulus/go-ios) +- **iOS simulators**: Xcode with simulator runtimes + [mobilecli](https://github.com/mobile-next/mobilecli) (installed automatically via `mobilewright` dependency) + +## License + +Apache-2.0 (same as upstream) diff --git a/packages/mobile-mcp/eslint.config.mjs b/packages/mobile-mcp/eslint.config.mjs new file mode 100644 index 0000000000..b004623993 --- /dev/null +++ b/packages/mobile-mcp/eslint.config.mjs @@ -0,0 +1,161 @@ +import typescriptEslint from "@typescript-eslint/eslint-plugin"; +import tsParser from "@typescript-eslint/parser"; +import stylistic from "@stylistic/eslint-plugin"; +import importRules from "eslint-plugin-import"; + +const plugins = { + "@stylistic": stylistic, + "@typescript-eslint": typescriptEslint, + import: importRules, +}; + +export const baseRules = { + "@typescript-eslint/no-unused-vars": [ + 2, + {args: "none", caughtErrors: "none"}, + ], + + /** + * Enforced rules + */ + // syntax preferences + "object-curly-spacing": ["error", "always"], + quotes: [ + 2, + "double", + { + 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, "all"], + "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, + "tab", + {SwitchCase: 1, CallExpression: {arguments: "first"}, MemberExpression: 1}, + ], + "key-spacing": [ + 2, + { + beforeColon: false, + }, + ], + "eol-last": 2, +}; + +const languageOptions = { + parser: tsParser, + ecmaVersion: 9, + sourceType: "module", +}; + +export default [ + { + files: ["**/*.ts"], + plugins, + languageOptions, + rules: baseRules, + }, +]; diff --git a/packages/mobile-mcp/package-lock.json b/packages/mobile-mcp/package-lock.json new file mode 100644 index 0000000000..c8332efa42 --- /dev/null +++ b/packages/mobile-mcp/package-lock.json @@ -0,0 +1,6261 @@ +{ + "name": "@mobilenext/mobile-mcp", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@mobilenext/mobile-mcp", + "version": "0.0.1", + "license": "Apache-2.0", + "dependencies": { + "@modelcontextprotocol/sdk": "1.26.0", + "ajv": "^8.18.0", + "commander": "14.0.0", + "express": "5.1.0", + "fast-xml-parser": "5.8.0", + "mobilewright": "0.0.45", + "qs": "^6.15.0", + "zod": "^4.1.13", + "zod-to-json-schema": "3.25.0" + }, + "bin": { + "mcp-server-mobile": "lib/index.js" + }, + "devDependencies": { + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "^9.19.0", + "@playwright/test": "^1.49.0", + "@stylistic/eslint-plugin": "^3.0.1", + "@types/commander": "^2.12.0", + "@types/express": "^5.0.3", + "@types/node": "^22.13.10", + "@typescript-eslint/eslint-plugin": "^8.28.0", + "@typescript-eslint/parser": "^8.26.1", + "@typescript-eslint/utils": "^8.26.1", + "c8": "^10.1.3", + "eslint": "^9.19.0", + "eslint-plugin": "^1.0.1", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-notice": "^1.0.0", + "husky": "^9.1.7", + "typescript": "5.8.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "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/eslint-utils/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-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", + "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "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/@eslint/eslintrc/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/@eslint/js": { + "version": "9.35.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.35.0.tgz", + "integrity": "sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.15.2", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@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.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "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.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.30", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", + "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mobilewright/core": { + "version": "0.0.45", + "resolved": "https://registry.npmjs.org/@mobilewright/core/-/core-0.0.45.tgz", + "integrity": "sha512-5wtqoG4AOAkr3u+Gegodvt6aeuF2Qe6Y5P0/p1HL8S1lFZ035MCD96TxE0zEDYNVaYyVLIaIAN59ri2O3Vi+JQ==", + "license": "Apache-2.0", + "dependencies": { + "@mobilewright/protocol": "^0.0.45", + "debug": "^4.4.3", + "playwright-core": "1.58.2", + "sharp": "^0.34.5" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@mobilewright/driver-mobilecli": { + "version": "0.0.45", + "resolved": "https://registry.npmjs.org/@mobilewright/driver-mobilecli/-/driver-mobilecli-0.0.45.tgz", + "integrity": "sha512-6v8hyjw3jgP6TeH4yqtKfXphr2sSIZj0BhD013Juk1//AYOdnGdltC2c0kbdeWUdqFB+K3G4Kc32fBVi7Gbfgg==", + "license": "Apache-2.0", + "dependencies": { + "@mobilewright/protocol": "^0.0.45", + "debug": "^4.4.3", + "mobilecli": "0.3.84", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@mobilewright/driver-mobilenext": { + "version": "0.0.45", + "resolved": "https://registry.npmjs.org/@mobilewright/driver-mobilenext/-/driver-mobilenext-0.0.45.tgz", + "integrity": "sha512-xXGuAb+ihezvtETr883mIlxreqECjRdXSL8jbFgK4VhJSk1/P+98zWlTMQRMVgwKOg9IKv+GG6JXSkTFD08YKw==", + "license": "Apache-2.0", + "dependencies": { + "@mobilewright/protocol": "^0.0.45", + "debug": "^4.4.3", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@mobilewright/protocol": { + "version": "0.0.45", + "resolved": "https://registry.npmjs.org/@mobilewright/protocol/-/protocol-0.0.45.tgz", + "integrity": "sha512-bArKuq4f7woi9ysBiRb1UWxKGHgZ+vwEH8WbpMYXXoEelDTpes0MZfMs4YNWisYSdwl4IRLiHzseQqMkJDPePQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", + "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/zod-to-json-schema": { + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", + "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" + } + }, + "node_modules/@nodable/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, + "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/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@playwright/test": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz", + "integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@playwright/test/node_modules/playwright": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", + "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/@playwright/test/node_modules/playwright-core": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", + "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "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/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/commander": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@types/commander/-/commander-2.12.0.tgz", + "integrity": "sha512-DDmRkovH7jPjnx7HcbSnqKg2JeNANyxNZeUvB0iE+qKBLN+vzN5iSIwt+J2PFSmBuYEut4mgQvI/fTX9YQH/vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz", + "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.7.tgz", + "integrity": "sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "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/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.18.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.1.tgz", + "integrity": "sha512-rzSDyhn4cYznVG+PCzGe1lwuMYJrcBS1fc3JqSa2PvtABwWo+dZ1ij5OVok3tqfpEBCBoaR4d7upFJk73HRJDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.5", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", + "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", + "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.43.0.tgz", + "integrity": "sha512-8tg+gt7ENL7KewsKMKDHXR1vm8tt9eMxjJBYINf6swonlWgkYn5NwyIgXpbbDxTNU5DgpDFfj95prcTq2clIQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.43.0", + "@typescript-eslint/type-utils": "8.43.0", + "@typescript-eslint/utils": "8.43.0", + "@typescript-eslint/visitor-keys": "8.43.0", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "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.43.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.43.0.tgz", + "integrity": "sha512-B7RIQiTsCBBmY+yW4+ILd6mF5h1FUwJsVvpqkrgpszYifetQ2Ke+Z4u6aZh0CblkUGIdR59iYVyXqqZGkZ3aBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.43.0", + "@typescript-eslint/types": "8.43.0", + "@typescript-eslint/typescript-estree": "8.43.0", + "@typescript-eslint/visitor-keys": "8.43.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 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.43.0.tgz", + "integrity": "sha512-htB/+D/BIGoNTQYffZw4uM4NzzuolCoaA/BusuSIcC8YjmBYQioew5VUZAYdAETPjeed0hqCaW7EHg+Robq8uw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.43.0", + "@typescript-eslint/types": "^8.43.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": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.43.0.tgz", + "integrity": "sha512-daSWlQ87ZhsjrbMLvpuuMAt3y4ba57AuvadcR7f3nl8eS3BjRc8L9VLxFLk92RL5xdXOg6IQ+qKjjqNEimGuAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.43.0", + "@typescript-eslint/visitor-keys": "8.43.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/tsconfig-utils": { + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.43.0.tgz", + "integrity": "sha512-ALC2prjZcj2YqqL5X/bwWQmHA2em6/94GcbB/KKu5SX3EBDOsqztmmX1kMkvAJHzxk7TazKzJfFiEIagNV3qEA==", + "dev": true, + "license": "MIT", + "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 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.43.0.tgz", + "integrity": "sha512-qaH1uLBpBuBBuRf8c1mLJ6swOfzCXryhKND04Igr4pckzSEW9JX5Aw9AgW00kwfjWJF0kk0ps9ExKTfvXfw4Qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.43.0", + "@typescript-eslint/typescript-estree": "8.43.0", + "@typescript-eslint/utils": "8.43.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.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 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.43.0.tgz", + "integrity": "sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw==", + "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.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.43.0.tgz", + "integrity": "sha512-7Vv6zlAhPb+cvEpP06WXXy/ZByph9iL6BQRBDj4kmBsW98AqEeQHlj/13X+sZOrKSo9/rNKH4Ul4f6EICREFdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.43.0", + "@typescript-eslint/tsconfig-utils": "8.43.0", + "@typescript-eslint/types": "8.43.0", + "@typescript-eslint/visitor-keys": "8.43.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.1.0" + }, + "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 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.43.0.tgz", + "integrity": "sha512-S1/tEmkUeeswxd0GGcnwuVQPFWo8NzZTOMxCvw8BX7OMxnNae+i8Tm7REQen/SwUIPoPqfKn7EaZ+YLpiB3k9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.43.0", + "@typescript-eslint/types": "8.43.0", + "@typescript-eslint/typescript-estree": "8.43.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 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.43.0.tgz", + "integrity": "sha512-T+S1KqRD4sg/bHfLwrpF/K3gQLBM1n7Rp7OjjikjTEssI2YJzQpi5WXoynOaQ93ERIuq3O8RBTOUYDKszUCEHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.43.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-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.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "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.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "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.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", + "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "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/c8": { + "version": "10.1.3", + "resolved": "https://registry.npmjs.org/c8/-/c8-10.1.3.tgz", + "integrity": "sha512-LvcyrOAaOnrrlMpW22n690PUvxiq4Uf9WMhQwNJ9vgagkL/ph1+D4uvjvDA5XCbykrc0sx+ay6pVi9YZ1GnhyA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.1", + "@istanbuljs/schema": "^0.1.3", + "find-up": "^5.0.0", + "foreground-child": "^3.1.1", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.1.6", + "test-exclude": "^7.0.1", + "v8-to-istanbul": "^9.0.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1" + }, + "bin": { + "c8": "bin/c8.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "monocart-coverage-reports": "^2" + }, + "peerDependenciesMeta": { + "monocart-coverage-reports": { + "optional": true + } + } + }, + "node_modules/c8/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/c8/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/c8/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/c8/node_modules/test-exclude": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", + "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^10.2.2" + }, + "engines": { + "node": ">=18" + } + }, + "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/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?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": "14.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.0.tgz", + "integrity": "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "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.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.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.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/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/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=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/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "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/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "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.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "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.4", + "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.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "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-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.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.19" + }, + "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/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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.35.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.35.0.tgz", + "integrity": "sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.1", + "@eslint/core": "^0.15.2", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.35.0", + "@eslint/plugin-kit": "^0.3.5", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "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": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin/-/eslint-plugin-1.0.1.tgz", + "integrity": "sha512-ervp8C09On0fLA258TvE08AqAr/bhRYgHVZd3BrJjD4JfOA2JGANDLGs06j51oWqfPd7Feoo3OoqHD+fuI2sFQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "requireindex": "~1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-notice": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-notice/-/eslint-plugin-notice-1.0.0.tgz", + "integrity": "sha512-M3VLQMZzZpvfTZ/vy9FmClIKq5rLBbQpM0KgfLZPJPrVXpmJYeobmmb+lfJzHWdNm8PWwvw8KlafQWo2N9xx1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-root": "^1.1.0", + "lodash": "^4.17.21", + "metric-lcs": "^0.1.2" + }, + "peerDependencies": { + "eslint": ">=3.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "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/eslint/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/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/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.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.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": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-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/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fast-xml-builder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", + "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.5.0", + "xml-naming": "^0.1.0" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.8.0.tgz", + "integrity": "sha512-6bIM7fsJxeo3uXv7OncQYsBAMPJ7V16Slahl/6M98C/i2q+vB1+4a0MtrvYwDFEUrwDSbAmeLDRXsOBwrL7yAg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.2.0", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.3.0", + "xml-naming": "^0.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "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.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "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/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "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": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/glob/node_modules/brace-expansion": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/hono": { + "version": "4.12.27", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.27.tgz", + "integrity": "sha512-1yrb/+w6HWQJrUCLkJ2IF5jNIPvvFkblV5RNOYl6bV+OA6p9GLcMpHFFGTosSvHvcAUibuUukRqhlYI4z32C7Q==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "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/http-errors/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/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/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/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-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-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "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/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jose": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", + "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-yaml": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "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": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/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/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.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "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/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "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.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "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/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mobilecli": { + "version": "0.3.84", + "resolved": "https://registry.npmjs.org/mobilecli/-/mobilecli-0.3.84.tgz", + "integrity": "sha512-TOyyavbO5mtzMlJ6vdAZbtjgH4GM392biOxiKDV9s4lMPzA0TSUphBZVCOKsCN03kotNJA2JytdY500H6iHvJw==", + "license": "MIT", + "bin": { + "mobilecli": "index.js" + } + }, + "node_modules/mobilewright": { + "version": "0.0.45", + "resolved": "https://registry.npmjs.org/mobilewright/-/mobilewright-0.0.45.tgz", + "integrity": "sha512-z9QJRtiGBax310zAhoMBvULm8uKnVPcVn+ELhII1E1LLPJDzoGYhFcVLfAzS8ela99KqYRKFS0D75ayCpENhFQ==", + "license": "Apache-2.0", + "dependencies": { + "@mobilewright/core": "^0.0.45", + "@mobilewright/driver-mobilecli": "^0.0.45", + "@mobilewright/driver-mobilenext": "^0.0.45", + "@mobilewright/protocol": "^0.0.45", + "commander": "^14.0.3", + "debug": "^4.4.3", + "playwright": "1.58.2", + "ws": "^8.18.0" + }, + "bin": { + "mobilewright": "dist/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/mobilewright/node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "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/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "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-expression-matcher": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "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-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "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.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/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.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", + "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.7.0", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "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/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requireindex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.1.0.tgz", + "integrity": "sha512-LBnkqsDE7BZKvqylbmn7lTIVdpx4K/QCduRATpO5R+wtPmky/a8pN1bO2D6wXppn1497AJF9mNjqAXr6bdl9jg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.5" + } + }, + "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.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "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/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "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/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=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-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/strnum": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", + "integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "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/tsconfig-paths/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/tsconfig-paths/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/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "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.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/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/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/v8-to-istanbul/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "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/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "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.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "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/xml-naming": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", + "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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": "4.1.13", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz", + "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.0", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.0.tgz", + "integrity": "sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" + } + } + } +} diff --git a/packages/mobile-mcp/package.json b/packages/mobile-mcp/package.json new file mode 100644 index 0000000000..9c99b8fdfb --- /dev/null +++ b/packages/mobile-mcp/package.json @@ -0,0 +1,68 @@ +{ + "name": "@qwen-code/mobile-mcp", + "version": "0.0.1", + "description": "Mobile MCP with opt-in relative coordinate support", + "repository": { + "type": "git", + "url": "git+https://github.com/QwenLM/qwen-code.git", + "directory": "packages/mobile-mcp" + }, + "engines": { + "node": ">=18" + }, + "license": "Apache-2.0", + "scripts": { + "build": "tsc && chmod +x lib/index.js", + "lint": "eslint .", + "fixlint": "eslint . --fix", + "test": "c8 playwright test", + "watch": "tsc --watch", + "clean": "rm -rf lib", + "prepare": "" + }, + "files": [ + "lib" + ], + "dependencies": { + "@modelcontextprotocol/sdk": "1.26.0", + "ajv": "^8.18.0", + "commander": "14.0.0", + "express": "5.1.0", + "fast-xml-parser": "5.8.0", + "qs": "^6.15.0", + "zod": "^4.1.13", + "zod-to-json-schema": "3.25.0", + "mobilewright": "0.0.45" + }, + "devDependencies": { + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "^9.19.0", + "@playwright/test": "^1.49.0", + "@stylistic/eslint-plugin": "^3.0.1", + "@types/commander": "^2.12.0", + "@types/express": "^5.0.3", + "@types/node": "^22.13.10", + "@typescript-eslint/eslint-plugin": "^8.28.0", + "@typescript-eslint/parser": "^8.26.1", + "@typescript-eslint/utils": "^8.26.1", + "c8": "^10.1.3", + "eslint": "^9.19.0", + "eslint-plugin": "^1.0.1", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-notice": "^1.0.0", + "husky": "^9.1.7", + "typescript": "5.8.2" + }, + "main": "lib/index.js", + "bin": { + "mcp-server-mobile": "lib/index.js" + }, + "directories": { + "lib": "lib" + }, + "author": "", + "bugs": { + "url": "https://github.com/QwenLM/qwen-code/issues" + }, + "homepage": "https://github.com/QwenLM/qwen-code/tree/main/packages/mobile-mcp#readme" +} diff --git a/packages/mobile-mcp/playwright.config.ts b/packages/mobile-mcp/playwright.config.ts new file mode 100644 index 0000000000..d07f6b4832 --- /dev/null +++ b/packages/mobile-mcp/playwright.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from "@playwright/test"; + +// These are plain Node tests (no browser). Playwright is used purely as the +// test runner, so no browser projects are configured. +export default defineConfig({ + testDir: "./test", + testMatch: "*.ts", + + // Device tests (android/ios/iphone-simulator) mutate real device state and + // must run serially, exactly as they did under mocha's single process. + workers: 1, + fullyParallel: false, + + // Device operations include several multi-second sleeps; the 30s default is + // too tight. + timeout: 60_000, +}); diff --git a/packages/mobile-mcp/scripts/sync-from-upstream.sh b/packages/mobile-mcp/scripts/sync-from-upstream.sh new file mode 100755 index 0000000000..ee43d115a4 --- /dev/null +++ b/packages/mobile-mcp/scripts/sync-from-upstream.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# sync-from-upstream.sh — pull upstream mobile-mcp changes via git subtree. +# +# Usage: +# packages/mobile-mcp/scripts/sync-from-upstream.sh [] +# +# Default: pulls from 'main' branch of the mobile-mcp-upstream remote. +# +# Prerequisites: +# git remote add mobile-mcp-upstream https://github.com/mobile-next/mobile-mcp.git +# +# After running: review the merge, resolve any conflicts in src/server.ts +# (our coord-norm hooks and new tools), and src/coord-norm.ts (new file, +# won't conflict). Then commit. +# +# If subtree pull becomes impractical (e.g. upstream history grows too large), +# fall back to the cua-driver approach: generate a diff between two refs in a +# local upstream clone and apply with `git apply --reject`. +set -euo pipefail + +BRANCH="${1:-main}" +REMOTE="mobile-mcp-upstream" +PREFIX="packages/mobile-mcp" + +REPO_ROOT="$(git rev-parse --show-toplevel)" +cd "$REPO_ROOT" + +# Ensure remote exists +if ! git remote | grep -q "^${REMOTE}$"; then + echo "Adding remote ${REMOTE}..." + git remote add "$REMOTE" https://github.com/mobile-next/mobile-mcp.git +fi + +echo "Fetching ${REMOTE}..." +git fetch "$REMOTE" + +echo "Pulling from ${REMOTE}/${BRANCH} into ${PREFIX}..." +git subtree pull --prefix="$PREFIX" "$REMOTE" "$BRANCH" --squash \ + -m "chore(mobile-mcp): sync with upstream ${REMOTE}/${BRANCH}" + +# Update the vendored-from marker +UPSTREAM_SHA="$(git rev-parse "${REMOTE}/${BRANCH}")" +echo "$UPSTREAM_SHA" > "${PREFIX}/.vendored-from" +git add "${PREFIX}/.vendored-from" + +echo "" +echo "Done. Upstream synced to ${UPSTREAM_SHA}." +echo "Review the merge, resolve any conflicts, then amend or commit." diff --git a/packages/mobile-mcp/server.json b/packages/mobile-mcp/server.json new file mode 100644 index 0000000000..754808b26c --- /dev/null +++ b/packages/mobile-mcp/server.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", + "name": "io.github.qwenlm/mobile-mcp", + "description": "MCP server for iOS and Android Mobile Development, Automation and Testing (qwen-code fork with relative coordinate support)", + "repository": { + "url": "https://github.com/QwenLM/qwen-code", + "source": "github" + }, + "version": "{{VERSION}}", + "packages": [ + { + "registryType": "npm", + "registryBaseUrl": "https://registry.npmjs.org", + "identifier": "@qwen-code/mobile-mcp", + "version": "{{VERSION}}", + "transport": { + "type": "stdio" + } + } + ] +} diff --git a/packages/mobile-mcp/src/android.ts b/packages/mobile-mcp/src/android.ts new file mode 100644 index 0000000000..e4b185324f --- /dev/null +++ b/packages/mobile-mcp/src/android.ts @@ -0,0 +1,857 @@ +import path from 'node:path'; +import { execFileSync } from 'node:child_process'; +import { existsSync } from 'node:fs'; + +import * as xml from 'fast-xml-parser'; + +import { + ActionableError, + Button, + InstalledApp, + InstallOptions, + Robot, + ScreenElement, + ScreenElementRect, + ScreenSize, + SwipeDirection, + Orientation, +} from './robot'; +import { validatePackageName, validateLocale } from './utils'; + +export interface AndroidDevice { + deviceId: string; + deviceType: 'tv' | 'mobile'; +} + +interface UiAutomatorXmlNode { + node: UiAutomatorXmlNode[]; + class?: string; + text?: string; + bounds?: string; + hint?: string; + focused?: string; + checkable?: string; + 'content-desc'?: string; + 'resource-id'?: string; +} + +interface UiAutomatorXml { + hierarchy: { + node: UiAutomatorXmlNode; + }; +} + +const getAdbPath = (): string => { + const exeName = process.env.platform === 'win32' ? 'adb.exe' : 'adb'; + if (process.env.ANDROID_HOME) { + return path.join(process.env.ANDROID_HOME, 'platform-tools', exeName); + } + + if (process.platform === 'win32' && process.env.LOCALAPPDATA) { + const windowsAdbPath = path.join( + process.env.LOCALAPPDATA, + 'Android', + 'Sdk', + 'platform-tools', + 'adb.exe', + ); + if (existsSync(windowsAdbPath)) { + return windowsAdbPath; + } + } + + if (process.platform === 'darwin' && process.env.HOME) { + const defaultAndroidSdk = path.join( + process.env.HOME, + 'Library', + 'Android', + 'sdk', + 'platform-tools', + 'adb', + ); + if (existsSync(defaultAndroidSdk)) { + return defaultAndroidSdk; + } + } + + // fallthrough, hope for the best + return exeName; +}; + +const BUTTON_MAP: Record = { + BACK: 'KEYCODE_BACK', + HOME: 'KEYCODE_HOME', + VOLUME_UP: 'KEYCODE_VOLUME_UP', + VOLUME_DOWN: 'KEYCODE_VOLUME_DOWN', + ENTER: 'KEYCODE_ENTER', + DPAD_CENTER: 'KEYCODE_DPAD_CENTER', + DPAD_UP: 'KEYCODE_DPAD_UP', + DPAD_DOWN: 'KEYCODE_DPAD_DOWN', + DPAD_LEFT: 'KEYCODE_DPAD_LEFT', + DPAD_RIGHT: 'KEYCODE_DPAD_RIGHT', +}; + +const TIMEOUT = 30000; +const MAX_BUFFER_SIZE = 1024 * 1024 * 8; + +type AndroidDeviceType = 'tv' | 'mobile'; + +export class AndroidRobot implements Robot { + public constructor(private deviceId: string) {} + + public adb(...args: string[]): Buffer { + return execFileSync(getAdbPath(), ['-s', this.deviceId, ...args], { + maxBuffer: MAX_BUFFER_SIZE, + timeout: TIMEOUT, + }); + } + + public silentAdb(...args: string[]): Buffer { + return execFileSync(getAdbPath(), ['-s', this.deviceId, ...args], { + maxBuffer: MAX_BUFFER_SIZE, + timeout: TIMEOUT, + stdio: ['pipe', 'pipe', 'pipe'], + }); + } + + public getSystemFeatures(): string[] { + return this.adb('shell', 'pm', 'list', 'features') + .toString() + .split('\n') + .map((line) => line.trim()) + .filter((line) => line.startsWith('feature:')) + .map((line) => line.substring('feature:'.length)); + } + + public async getScreenSize(): Promise { + const screenSize = this.adb('shell', 'wm', 'size') + .toString() + .split(' ') + .pop(); + + if (!screenSize) { + throw new Error('Failed to get screen size'); + } + + const scale = 1; + const [width, height] = screenSize.split('x').map(Number); + return { width, height, scale }; + } + + public async listApps(): Promise { + // only apps that have a launcher activity are returned + return this.adb( + 'shell', + 'cmd', + 'package', + 'query-activities', + '-a', + 'android.intent.action.MAIN', + '-c', + 'android.intent.category.LAUNCHER', + ) + .toString() + .split('\n') + .map((line) => line.trim()) + .filter((line) => line.startsWith('packageName=')) + .map((line) => line.substring('packageName='.length)) + .filter((value, index, self) => self.indexOf(value) === index) + .map((packageName) => ({ + packageName, + appName: packageName, + })); + } + + private async listPackages(): Promise { + return this.adb('shell', 'pm', 'list', 'packages') + .toString() + .split('\n') + .map((line) => line.trim()) + .filter((line) => line.startsWith('package:')) + .map((line) => line.substring('package:'.length)); + } + + public async launchApp(packageName: string, locale?: string): Promise { + validatePackageName(packageName); + + if (locale) { + validateLocale(locale); + try { + this.silentAdb( + 'shell', + 'cmd', + 'locale', + 'set-app-locales', + packageName, + '--locales', + locale, + ); + } catch (error) { + // set-app-locales requires Android 13+ (API 33), silently ignore on older versions + } + } + + try { + this.silentAdb( + 'shell', + 'monkey', + '-p', + packageName, + '-c', + 'android.intent.category.LAUNCHER', + '1', + ); + } catch (error) { + throw new ActionableError( + `Failed launching app with package name "${packageName}", please make sure it exists`, + ); + } + } + + public async listRunningProcesses(): Promise { + return this.adb('shell', 'ps', '-e') + .toString() + .split('\n') + .map((line) => line.trim()) + .filter((line) => line.startsWith('u')) // non-system processes + .map((line) => line.split(/\s+/)[8]); // get process name + } + + public async swipe(direction: SwipeDirection): Promise { + const screenSize = await this.getScreenSize(); + const centerX = screenSize.width >> 1; + + let x0: number, y0: number, x1: number, y1: number; + + switch (direction) { + case 'up': + x0 = x1 = centerX; + y0 = Math.floor(screenSize.height * 0.8); + y1 = Math.floor(screenSize.height * 0.2); + break; + case 'down': + x0 = x1 = centerX; + y0 = Math.floor(screenSize.height * 0.2); + y1 = Math.floor(screenSize.height * 0.8); + break; + case 'left': + x0 = Math.floor(screenSize.width * 0.8); + x1 = Math.floor(screenSize.width * 0.2); + y0 = y1 = Math.floor(screenSize.height * 0.5); + break; + case 'right': + x0 = Math.floor(screenSize.width * 0.2); + x1 = Math.floor(screenSize.width * 0.8); + y0 = y1 = Math.floor(screenSize.height * 0.5); + break; + default: + throw new ActionableError( + `Swipe direction "${direction}" is not supported`, + ); + } + + this.adb( + 'shell', + 'input', + 'swipe', + `${x0}`, + `${y0}`, + `${x1}`, + `${y1}`, + '1000', + ); + } + + public async swipeFromCoordinate( + x: number, + y: number, + direction: SwipeDirection, + distance?: number, + ): Promise { + const screenSize = await this.getScreenSize(); + + let x0: number, y0: number, x1: number, y1: number; + + // Use provided distance or default to 30% of screen dimension + const defaultDistanceY = Math.floor(screenSize.height * 0.3); + const defaultDistanceX = Math.floor(screenSize.width * 0.3); + const swipeDistanceY = distance || defaultDistanceY; + const swipeDistanceX = distance || defaultDistanceX; + + switch (direction) { + case 'up': + x0 = x1 = x; + y0 = y; + y1 = Math.max(0, y - swipeDistanceY); + break; + case 'down': + x0 = x1 = x; + y0 = y; + y1 = Math.min(screenSize.height, y + swipeDistanceY); + break; + case 'left': + x0 = x; + x1 = Math.max(0, x - swipeDistanceX); + y0 = y1 = y; + break; + case 'right': + x0 = x; + x1 = Math.min(screenSize.width, x + swipeDistanceX); + y0 = y1 = y; + break; + default: + throw new ActionableError( + `Swipe direction "${direction}" is not supported`, + ); + } + + this.adb( + 'shell', + 'input', + 'swipe', + `${x0}`, + `${y0}`, + `${x1}`, + `${y1}`, + '1000', + ); + } + + private getDisplayCount(): number { + return this.adb('shell', 'dumpsys', 'SurfaceFlinger', '--display-id') + .toString() + .split('\n') + .filter((s) => s.startsWith('Display ')).length; + } + + private getFirstDisplayId(): string | null { + try { + // Try using cmd display get-displays (Android 11+) + const displays = this.adb('shell', 'cmd', 'display', 'get-displays') + .toString() + .split('\n') + .filter((s) => s.startsWith('Display id ')) + // filter for state ON even though get-displays only returns turned on displays + .filter((s) => s.indexOf(', state ON,') >= 0) + // another paranoia check + .filter((s) => s.indexOf(', uniqueId ') >= 0); + + if (displays.length > 0) { + const m = displays[0].match(/uniqueId \"([^\"]+)\"/); + if (m !== null) { + let displayId = m[1]; + if (displayId.startsWith('local:')) { + displayId = displayId.substring('local:'.length); + } + + return displayId; + } + } + } catch (error) { + // cmd display get-displays not available on this device + } + + // fallback: parse dumpsys display for display info (compatible with older Android versions) + try { + const dumpsys = this.adb('shell', 'dumpsys', 'display').toString(); + + // look for DisplayViewport entries with isActive=true and type=INTERNAL + const viewportMatch = dumpsys.match( + /DisplayViewport\{type=INTERNAL[^}]*isActive=true[^}]*uniqueId='([^']+)'/, + ); + if (viewportMatch) { + let uniqueId = viewportMatch[1]; + if (uniqueId.startsWith('local:')) { + uniqueId = uniqueId.substring('local:'.length); + } + + return uniqueId; + } + + // fallback: look for active display with state ON + const displayStateMatch = dumpsys.match( + /Display Id=(\d+)[\s\S]*?Display State=ON/, + ); + if (displayStateMatch) { + return displayStateMatch[1]; + } + } catch (error) { + // dumpsys display also failed + } + + return null; + } + + public async getScreenshot(): Promise { + if (this.getDisplayCount() <= 1) { + // backward compatibility for android 10 and below, and for single display devices + return this.adb('exec-out', 'screencap', '-p'); + } + + // find the first display that is turned on, and capture that one + const displayId = this.getFirstDisplayId(); + if (displayId === null) { + // no idea why, but we have displayCount >= 2, yet we failed to parse + // let's go with screencap's defaults and hope for the best + return this.adb('exec-out', 'screencap', '-p'); + } + + return this.adb('exec-out', 'screencap', '-p', '-d', `${displayId}`); + } + + private collectElements(node: UiAutomatorXmlNode): ScreenElement[] { + const elements: Array = []; + + if (node.node) { + if (Array.isArray(node.node)) { + for (const childNode of node.node) { + elements.push(...this.collectElements(childNode)); + } + } else { + elements.push(...this.collectElements(node.node)); + } + } + + if ( + node.text || + node['content-desc'] || + node.hint || + node['resource-id'] || + node.checkable === 'true' + ) { + const element: ScreenElement = { + type: node.class || 'text', + text: node.text, + label: node['content-desc'] || node.hint || '', + rect: this.getScreenElementRect(node), + }; + + if (node.focused === 'true') { + // only provide it if it's true, otherwise don't confuse llm + element.focused = true; + } + + const resourceId = node['resource-id']; + if (resourceId !== null && resourceId !== '') { + element.identifier = resourceId; + } + + if (element.rect.width > 0 && element.rect.height > 0) { + elements.push(element); + } + } + + return elements; + } + + public async getElementsOnScreen(): Promise { + const parsedXml = await this.getUiAutomatorXml(); + const hierarchy = parsedXml.hierarchy; + const elements = this.collectElements(hierarchy.node); + return elements; + } + + public async terminateApp(packageName: string): Promise { + validatePackageName(packageName); + this.adb('shell', 'am', 'force-stop', packageName); + } + + public async installApp( + path: string, + options?: InstallOptions, + ): Promise { + try { + const args: string[] = ['install']; + const opts = options || {}; + if (opts.replace !== false) { + args.push('-r'); + } + if (opts.grantPermissions) { + args.push('-g'); + } + if (opts.allowDowngrade) { + args.push('-d'); + } + if (opts.allowTest) { + args.push('-t'); + } + args.push(path); + this.adb(...args); + } catch (error: any) { + const stdout = error.stdout ? error.stdout.toString() : ''; + const stderr = error.stderr ? error.stderr.toString() : ''; + const output = (stdout + stderr).trim(); + throw new ActionableError(output || error.message); + } + } + + public async uninstallApp(bundleId: string): Promise { + try { + this.adb('uninstall', bundleId); + } catch (error: any) { + const stdout = error.stdout ? error.stdout.toString() : ''; + const stderr = error.stderr ? error.stderr.toString() : ''; + const output = (stdout + stderr).trim(); + throw new ActionableError(output || error.message); + } + } + + public async openUrl(url: string): Promise { + this.adb( + 'shell', + 'am', + 'start', + '-a', + 'android.intent.action.VIEW', + '-d', + this.escapeShellText(url), + ); + } + + private isAscii(text: string): boolean { + return /^[\x00-\x7F]*$/.test(text); + } + + private escapeShellText(text: string): string { + // escape all shell special characters that could be used for injection + return text.replace(/[\\'"` \t\n\r|&;()<>{}[\]$*?]/g, '\\$&'); + } + + private async isDeviceKitInstalled(): Promise { + const packages = await this.listPackages(); + return packages.includes('com.mobilenext.devicekit'); + } + + public async sendKeys(text: string): Promise { + if (text === '') { + // bailing early, so we don't run adb shell with empty string. + // this happens when you prompt with a simple "submit". + return; + } + + if (this.isAscii(text)) { + // adb shell input only supports ascii characters. and + // some of the keys have to be escaped. + const _text = this.escapeShellText(text); + this.adb('shell', 'input', 'text', _text); + } else if (await this.isDeviceKitInstalled()) { + // try sending over clipboard + const base64 = Buffer.from(text).toString('base64'); + + // send clipboard over and immediately paste it + this.adb( + 'shell', + 'am', + 'broadcast', + '-a', + 'devicekit.clipboard.set', + '-e', + 'encoding', + 'base64', + '-e', + 'text', + base64, + '-n', + 'com.mobilenext.devicekit/.ClipboardBroadcastReceiver', + ); + this.adb('shell', 'input', 'keyevent', 'KEYCODE_PASTE'); + + // clear clipboard when we're done + this.adb( + 'shell', + 'am', + 'broadcast', + '-a', + 'devicekit.clipboard.clear', + '-n', + 'com.mobilenext.devicekit/.ClipboardBroadcastReceiver', + ); + } else { + throw new ActionableError( + 'Non-ASCII text is not supported on Android, please install mobilenext devicekit, see https://github.com/mobile-next/devicekit-android', + ); + } + } + + public async pressButton(button: Button) { + if (!BUTTON_MAP[button]) { + throw new ActionableError(`Button "${button}" is not supported`); + } + + const mapped = BUTTON_MAP[button]; + this.adb('shell', 'input', 'keyevent', mapped); + } + + public async tap(x: number, y: number): Promise { + this.adb('shell', 'input', 'tap', `${x}`, `${y}`); + } + + public async longPress( + x: number, + y: number, + duration: number, + ): Promise { + // a long press is a swipe with no movement and a long duration + this.adb( + 'shell', + 'input', + 'swipe', + `${x}`, + `${y}`, + `${x}`, + `${y}`, + `${duration}`, + ); + } + + public async doubleTap(x: number, y: number): Promise { + await this.tap(x, y); + await new Promise((r) => setTimeout(r, 100)); // short delay + await this.tap(x, y); + } + + public async setOrientation(orientation: Orientation): Promise { + const value = orientation === 'portrait' ? 0 : 1; + + // disable auto-rotation prior to setting the orientation + this.adb( + 'shell', + 'settings', + 'put', + 'system', + 'accelerometer_rotation', + '0', + ); + this.adb( + 'shell', + 'content', + 'insert', + '--uri', + 'content://settings/system', + '--bind', + 'name:s:user_rotation', + '--bind', + `value:i:${value}`, + ); + } + + public async getOrientation(): Promise { + const rotation = this.adb( + 'shell', + 'settings', + 'get', + 'system', + 'user_rotation', + ) + .toString() + .trim(); + return rotation === '0' ? 'portrait' : 'landscape'; + } + + public async dumpUiHierarchy(compressed: boolean = false): Promise { + for (let tries = 0; tries < 10; tries++) { + const args = ['exec-out', 'uiautomator', 'dump']; + if (compressed) { + args.push('--compressed'); + } + args.push('/dev/tty'); + const dump = this.adb(...args).toString(); + if (dump.includes('null root node returned by UiTestAutomationBridge')) { + continue; + } + const xmlStart = dump.indexOf(' { + for (let tries = 0; tries < 10; tries++) { + const dump = this.adb( + 'exec-out', + 'uiautomator', + 'dump', + '/dev/tty', + ).toString(); + // note: we're not catching other errors here. maybe we should check for { + const dump = await this.getUiAutomatorDump(); + const parser = new xml.XMLParser({ + ignoreAttributes: false, + attributeNamePrefix: '', + }); + + return parser.parse(dump) as UiAutomatorXml; + } + + private getScreenElementRect(node: UiAutomatorXmlNode): ScreenElementRect { + const bounds = String(node.bounds); + + const [, left, top, right, bottom] = + bounds.match(/^\[(\d+),(\d+)\]\[(\d+),(\d+)\]$/)?.map(Number) || []; + return { + x: left, + y: top, + width: right - left, + height: bottom - top, + }; + } +} + +export class AndroidDeviceManager { + private getDeviceType(name: string): AndroidDeviceType { + try { + const device = new AndroidRobot(name); + const features = device.getSystemFeatures(); + if ( + features.includes('android.software.leanback') || + features.includes('android.hardware.type.television') + ) { + return 'tv'; + } + return 'mobile'; + } catch (error) { + // Fallback to mobile if we cannot determine device type + return 'mobile'; + } + } + + private getDeviceVersion(deviceId: string): string { + try { + const output = execFileSync( + getAdbPath(), + ['-s', deviceId, 'shell', 'getprop', 'ro.build.version.release'], + { + timeout: 5000, + }, + ) + .toString() + .trim(); + return output; + } catch (error) { + return 'unknown'; + } + } + + private getDeviceName(deviceId: string): string { + try { + // Try getting AVD name first (for emulators) + const avdName = execFileSync( + getAdbPath(), + ['-s', deviceId, 'shell', 'getprop', 'ro.boot.qemu.avd_name'], + { + timeout: 5000, + }, + ) + .toString() + .trim(); + + if (avdName !== '') { + // Replace underscores with spaces (e.g., "Pixel_9_Pro" -> "Pixel 9 Pro") + return avdName.replace(/_/g, ' '); + } + + // Fall back to product model + const output = execFileSync( + getAdbPath(), + ['-s', deviceId, 'shell', 'getprop', 'ro.product.model'], + { + timeout: 5000, + }, + ) + .toString() + .trim(); + return output; + } catch (error) { + return deviceId; + } + } + + public getConnectedDevices(): AndroidDevice[] { + try { + const names = execFileSync(getAdbPath(), ['devices']) + .toString() + .split('\n') + .map((line) => line.trim()) + .filter((line) => line !== '') + .filter((line) => !line.startsWith('List of devices attached')) + .filter((line) => line.split('\t')[1]?.trim() === 'device') // Only include devices that are online and ready + .map((line) => line.split('\t')[0]); + + return names.map((name) => ({ + deviceId: name, + deviceType: this.getDeviceType(name), + })); + } catch (error) { + console.error( + 'Could not execute adb command, maybe ANDROID_HOME is not set?', + ); + return []; + } + } + + public getConnectedDevicesWithDetails(): Array< + AndroidDevice & { version: string; name: string } + > { + try { + const names = execFileSync(getAdbPath(), ['devices']) + .toString() + .split('\n') + .map((line) => line.trim()) + .filter((line) => line !== '') + .filter((line) => !line.startsWith('List of devices attached')) + .filter((line) => line.split('\t')[1]?.trim() === 'device') // Only include devices that are online and ready + .map((line) => line.split('\t')[0]); + + return names.map((deviceId) => ({ + deviceId, + deviceType: this.getDeviceType(deviceId), + version: this.getDeviceVersion(deviceId), + name: this.getDeviceName(deviceId), + })); + } catch (error) { + console.error( + 'Could not execute adb command, maybe ANDROID_HOME is not set?', + ); + return []; + } + } +} diff --git a/packages/mobile-mcp/src/coord-norm.ts b/packages/mobile-mcp/src/coord-norm.ts new file mode 100644 index 0000000000..c34c1219b7 --- /dev/null +++ b/packages/mobile-mcp/src/coord-norm.ts @@ -0,0 +1,186 @@ +// coord-norm.ts — Opt-in 0–1000 relative-coordinate shim for mobile-mcp. +// +// Mirrors packages/cua-driver's coord_norm.rs design. Default off = pixel +// passthrough (zero behavior change). When on, input coordinates are +// denormalized from 0–scale to device pixels/points before reaching the +// backend, and output coordinates are normalized back. +// +// Env vars: +// MOBILE_MCP_COORDINATE_SPACE "0" (default, off) | "1" (on) +// MOBILE_MCP_COORDINATE_SCALE default 1000 + +// ── Global config (read once at module load) ───────────────────────────────── + +export function isNormalized(): boolean { + return process.env.MOBILE_MCP_COORDINATE_SPACE === '1'; +} + +export function coordinateScale(): number { + const raw = parseInt(process.env.MOBILE_MCP_COORDINATE_SCALE || '1000', 10); + return isNaN(raw) || raw <= 0 ? 1000 : raw; +} + +// ── Scalar conversion ──────────────────────────────────────────────────────── + +export function normToPx(norm: number, dim: number, scale: number): number { + return Math.round((norm / scale) * dim); +} + +export function pxToNorm(px: number, dim: number, scale: number): number { + if (dim === 0) return 0; + return Math.round((px / dim) * scale); +} + +// ── Per-tool coordinate field mapping ──────────────────────────────────────── + +interface CoordField { + field: string; + isX: boolean; // true = scale by width, false = scale by height +} + +const INPUT_COORD_FIELDS: Record = { + mobile_click_on_screen_at_coordinates: [ + { field: 'x', isX: true }, + { field: 'y', isX: false }, + ], + mobile_double_tap_on_screen: [ + { field: 'x', isX: true }, + { field: 'y', isX: false }, + ], + mobile_long_press_on_screen_at_coordinates: [ + { field: 'x', isX: true }, + { field: 'y', isX: false }, + ], + mobile_swipe_on_screen: [ + { field: 'x', isX: true }, + { field: 'y', isX: false }, + ], +}; + +// ── Per-device screen size cache ───────────────────────────────────────────── + +const screenSizeCache = new Map(); + +export function cacheScreenSize( + deviceId: string, + width: number, + height: number, +): void { + screenSizeCache.set(deviceId, { width, height }); +} + +export function invalidateScreenSize(deviceId: string): void { + screenSizeCache.delete(deviceId); +} + +export function getCachedScreenSize( + deviceId: string, +): { width: number; height: number } | undefined { + return screenSizeCache.get(deviceId); +} + +// ── Input: denormalize 0–scale → pixels/points ────────────────────────────── + +export function denormalizeArgs( + toolName: string, + args: Record, + screenWidth: number, + screenHeight: number, +): void { + const scale = coordinateScale(); + const fields = INPUT_COORD_FIELDS[toolName]; + if (!fields) return; + + for (const { field, isX } of fields) { + if (typeof args[field] === 'number') { + const dim = isX ? screenWidth : screenHeight; + args[field] = normToPx(args[field], dim, scale); + } + } + + // swipe distance: scale by the axis matching the swipe direction + if ( + toolName === 'mobile_swipe_on_screen' && + typeof args.distance === 'number' + ) { + const dir = args.direction; + const dim = dir === 'up' || dir === 'down' ? screenHeight : screenWidth; + args.distance = normToPx(args.distance, dim, scale); + } +} + +// ── Output: normalize element coordinates and screen size ──────────────────── + +export function normalizeElementResult( + toolName: string, + response: string, + screenWidth: number, + screenHeight: number, +): string { + if (toolName !== 'mobile_list_elements_on_screen') return response; + + const scale = coordinateScale(); + const prefix = 'Found these elements on screen: '; + if (!response.startsWith(prefix)) return response; + + try { + const elements = JSON.parse(response.substring(prefix.length)); + for (const el of elements) { + if (el.coordinates) { + el.coordinates.x = pxToNorm(el.coordinates.x, screenWidth, scale); + el.coordinates.y = pxToNorm(el.coordinates.y, screenHeight, scale); + el.coordinates.width = pxToNorm( + el.coordinates.width, + screenWidth, + scale, + ); + el.coordinates.height = pxToNorm( + el.coordinates.height, + screenHeight, + scale, + ); + } + } + return prefix + JSON.stringify(elements); + } catch (err) { + console.error('[coord-norm] Failed to normalize element coordinates:', err); + return response; + } +} + +export function normalizeScreenSizeResult( + toolName: string, + response: string, +): string { + if (toolName !== 'mobile_get_screen_size') return response; + const scale = coordinateScale(); + return `Screen size is ${scale}x${scale} (normalized 0-${scale} coordinate space)`; +} + +// ── Ingest: extract screen size from get_screen_size response ──────────────── + +export function ingestScreenSizeFromResult( + deviceId: string, + response: string, +): void { + // Response format: "Screen size is WIDTHxHEIGHT pixels" + const match = response.match(/Screen size is (\d+)x(\d+)/); + if (match) { + cacheScreenSize(deviceId, parseInt(match[1], 10), parseInt(match[2], 10)); + } +} + +// ── Description rewriting ──────────────────────────────────────────────────── + +export function rewriteDescription(description: string): string { + const scale = coordinateScale(); + return description.replace( + /\bin pixels\b/g, + `in 0-${scale} normalized coordinates`, + ); +} + +export function coordParamDesc(baseDesc: string): string { + if (!isNormalized()) return baseDesc; + return rewriteDescription(baseDesc); +} diff --git a/packages/mobile-mcp/src/image-utils.ts b/packages/mobile-mcp/src/image-utils.ts new file mode 100644 index 0000000000..e4ce545390 --- /dev/null +++ b/packages/mobile-mcp/src/image-utils.ts @@ -0,0 +1,164 @@ +import { execFileSync, spawnSync } from "child_process"; +import os from "node:os"; +import fs from "node:fs"; +import path from "node:path"; +import { trace } from "./logger"; + +const DEFAULT_JPEG_QUALITY = 75; + +export class ImageTransformer { + + private newWidth: number = 0; + private newFormat: "jpg" | "png" = "png"; + private jpegOptions: { quality: number } = { quality: DEFAULT_JPEG_QUALITY }; + + constructor(private buffer: Buffer) {} + + public resize(width: number): ImageTransformer { + this.newWidth = width; + return this; + } + + public jpeg(options: { quality: number }): ImageTransformer { + this.newFormat = "jpg"; + this.jpegOptions = options; + return this; + } + + public png(): ImageTransformer { + this.newFormat = "png"; + return this; + } + + public toBuffer(): Buffer { + if (isSipsInstalled()) { + try { + return this.toBufferWithSips(); + } catch (error) { + trace(`Sips failed, falling back to ImageMagick: ${error}`); + } + } + + try { + return this.toBufferWithImageMagick(); + } catch (error) { + trace(`ImageMagick failed: ${error}`); + throw new Error("Image scaling unavailable (requires Sips or ImageMagick)."); + } + } + + private qualityToSips(q: number): "low" | "normal" | "high" | "best" { + if (q >= 90) { + return "best"; + } + + if (q >= 75) { + return "high"; + } + + if (q >= 50) { + return "normal"; + } + + return "low"; + } + + private toBufferWithSips(): Buffer { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "image-")); + const inputFile = path.join(tempDir, "input"); + const outputFile = path.join(tempDir, `output.${this.newFormat === "jpg" ? "jpg" : "png"}`); + + try { + fs.writeFileSync(inputFile, this.buffer); + + const args = ["-s", "format", this.newFormat === "jpg" ? "jpeg" : "png"]; + if (this.newFormat === "jpg") { + args.push("-s", "formatOptions", this.qualityToSips(this.jpegOptions.quality)); + } + + args.push("-Z", `${this.newWidth}`); + args.push("--out", outputFile); + args.push(inputFile); + + trace(`Running sips command: /usr/bin/sips ${args.join(" ")}`); + const proc = spawnSync("/usr/bin/sips", args, { + maxBuffer: 8 * 1024 * 1024 + }); + + if (proc.status !== 0) { + throw new Error(`Sips failed with status ${proc.status}`); + } + + const outputBuffer = fs.readFileSync(outputFile); + trace("Sips returned buffer of size: " + outputBuffer.length); + return outputBuffer; + } finally { + try { + fs.rmSync(tempDir, { recursive: true, force: true }); + } catch (error) { + // Ignore cleanup errors + } + } + } + + private toBufferWithImageMagick(): Buffer { + const magickArgs = ["-", "-resize", `${this.newWidth}x`, "-quality", `${this.jpegOptions.quality}`, `${this.newFormat}:-`]; + trace(`Running magick command: magick ${magickArgs.join(" ")}`); + + const proc = spawnSync("magick", magickArgs, { + maxBuffer: 8 * 1024 * 1024, + input: this.buffer + }); + + return proc.stdout; + } +} + +export class Image { + constructor(private buffer: Buffer) {} + + public static fromBuffer(buffer: Buffer): Image { + return new Image(buffer); + } + + public resize(width: number): ImageTransformer { + return new ImageTransformer(this.buffer).resize(width); + } + + public jpeg(options: { quality: number }): ImageTransformer { + return new ImageTransformer(this.buffer).jpeg(options); + } +} + +const isDarwin = (): boolean => { + return os.platform() === "darwin"; +}; + +export const isSipsInstalled = (): boolean => { + if (!isDarwin()) { + return false; + } + + try { + execFileSync("/usr/bin/sips", ["--version"]); + return true; + } catch (error) { + return false; + } +}; + +export const isImageMagickInstalled = (): boolean => { + try { + return execFileSync("magick", ["--version"]) + .toString() + .split("\n") + .filter(line => line.includes("Version: ImageMagick")) + .length > 0; + } catch (error) { + return false; + } +}; + +export const isScalingAvailable = (): boolean => { + return isImageMagickInstalled() || isSipsInstalled(); +}; diff --git a/packages/mobile-mcp/src/index.ts b/packages/mobile-mcp/src/index.ts new file mode 100644 index 0000000000..d851fdf18f --- /dev/null +++ b/packages/mobile-mcp/src/index.ts @@ -0,0 +1,132 @@ +#!/usr/bin/env node +import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { createMcpServer, getAgentVersion } from "./server"; +import { error } from "./logger"; +import express from "express"; +import { program } from "commander"; + +const startSseServer = async (host: string, port: number) => { + const app = express(); + const server = createMcpServer(); + + const authToken = process.env.MOBILEMCP_AUTH; + if (!authToken) { + error("WARNING: MOBILEMCP_AUTH is not set. The SSE server will accept unauthenticated connections. Set MOBILEMCP_AUTH to require Bearer token authentication."); + } + + if (authToken) { + app.use((req, res, next) => { + if (req.headers.authorization !== `Bearer ${authToken}`) { + res.status(401).json({ error: "Unauthorized" }); + return; + } + + next(); + }); + } + + // Block cross-origin requests — MCP clients are not browsers + app.use((req, res, next) => { + if (req.headers.origin) { + res.status(403).json({ error: "Cross-origin requests are not allowed" }); + return; + } + + if (req.method === "OPTIONS") { + res.status(403).end(); + return; + } + + next(); + }); + + let transport: SSEServerTransport | null = null; + + app.post("/mcp", (req, res) => { + if (transport) { + transport.handlePostMessage(req, res); + } + }); + + app.get("/mcp", (req, res) => { + if (transport) { + res.status(409).json({ error: "Another client is already connected. Disconnect the existing client first." }); + return; + } + + transport = new SSEServerTransport("/mcp", res); + + transport.onclose = () => { + transport = null; + }; + + server.connect(transport); + }); + + app.listen(port, host, () => { + error(`mobile-mcp ${getAgentVersion()} sse server listening on http://${host}:${port}/mcp`); + }); +}; + +const startStdioServer = async () => { + try { + const transport = new StdioServerTransport(); + + const server = createMcpServer(); + await server.connect(transport); + + // Exit cleanly on termination signals so node flushes pending work + // (including NODE_V8_COVERAGE output). Node's default SIGINT/SIGTERM + // handling terminates the process without writing the coverage file, + // which makes the `test:mcp` report come back all zeros. + const shutdown = () => { + process.exit(0); + }; + + process.on("SIGINT", shutdown); + process.on("SIGTERM", shutdown); + + error("mobile-mcp server running on stdio"); + } catch (err: any) { + console.error("Fatal error in main():", err); + error("Fatal error in main(): " + JSON.stringify(err.stack)); + process.exit(1); + } +}; + +const main = async () => { + program + .version(getAgentVersion()) + .option("--listen ", "Start SSE server on [host:]port") + .option("--stdio", "Start stdio server (default)") + .parse(process.argv); + + const options = program.opts(); + + if (options.listen) { + const listen = (options.listen as string).trim(); + const lastColon = listen.lastIndexOf(":"); + let host = "localhost"; + let rawPort: string; + + if (lastColon > 0) { + host = listen.substring(0, lastColon); + rawPort = listen.substring(lastColon + 1); + } else { + rawPort = listen; + } + + const port = Number.parseInt(rawPort, 10); + if (!host || !rawPort || !Number.isInteger(port) || port < 1 || port > 65535) { + error(`Invalid --listen value "${listen}". Expected [host:]port with port 1-65535.`); + process.exit(1); + } + + await startSseServer(host, port); + } else { + await startStdioServer(); + } +}; + +main().then(); diff --git a/packages/mobile-mcp/src/ios.ts b/packages/mobile-mcp/src/ios.ts new file mode 100644 index 0000000000..e17149b81b --- /dev/null +++ b/packages/mobile-mcp/src/ios.ts @@ -0,0 +1,348 @@ +import { Socket } from 'node:net'; +import { execFileSync } from 'node:child_process'; + +import { WebDriverAgent } from './webdriver-agent'; +import { + ActionableError, + Button, + InstalledApp, + InstallOptions, + Robot, + ScreenSize, + SwipeDirection, + ScreenElement, + Orientation, +} from './robot'; +import { validatePackageName, validateLocale } from './utils'; + +const WDA_PORT = 8100; +const IOS_TUNNEL_PORT = 60105; + +interface ListCommandOutput { + deviceList: string[]; +} + +interface VersionCommandOutput { + version: string; +} + +interface InfoCommandOutput { + DeviceClass: string; + DeviceName: string; + ProductName: string; + ProductType: string; + ProductVersion: string; + PhoneNumber: string; + TimeZone: string; +} + +export interface IosDevice { + deviceId: string; + deviceName: string; +} + +const getGoIosPath = (): string => { + if (process.env.GO_IOS_PATH) { + return process.env.GO_IOS_PATH; + } + + // fallback to go-ios in PATH via `npm install -g go-ios` + return 'ios'; +}; + +export class IosRobot implements Robot { + public constructor(private deviceId: string) {} + + private isListeningOnPort(port: number): Promise { + return new Promise((resolve, reject) => { + const client = new Socket(); + client.connect(port, 'localhost', () => { + client.destroy(); + resolve(true); + }); + + client.on('error', (err: any) => { + resolve(false); + }); + }); + } + + private async isTunnelRunning(): Promise { + return await this.isListeningOnPort(IOS_TUNNEL_PORT); + } + + private async isWdaForwardRunning(): Promise { + return await this.isListeningOnPort(WDA_PORT); + } + + private async assertTunnelRunning(): Promise { + if (await this.isTunnelRequired()) { + if (!(await this.isTunnelRunning())) { + throw new ActionableError( + 'iOS tunnel is not running, please see https://github.com/mobile-next/mobile-mcp/wiki/', + ); + } + } + } + + private async wda(): Promise { + await this.assertTunnelRunning(); + + if (!(await this.isWdaForwardRunning())) { + throw new ActionableError( + 'Port forwarding to WebDriverAgent is not running (tunnel okay), please see https://github.com/mobile-next/mobile-mcp/wiki/', + ); + } + + const wda = new WebDriverAgent('localhost', WDA_PORT); + + if (!(await wda.isRunning())) { + throw new ActionableError( + 'WebDriverAgent is not running on device (tunnel okay, port forwarding okay), please see https://github.com/mobile-next/mobile-mcp/wiki/', + ); + } + + return wda; + } + + private async ios(...args: string[]): Promise { + return execFileSync( + getGoIosPath(), + ['--udid', this.deviceId, ...args], + {}, + ).toString(); + } + + public async getIosVersion(): Promise { + const output = await this.ios('info'); + const json = JSON.parse(output); + return json.ProductVersion; + } + + private async isTunnelRequired(): Promise { + const version = await this.getIosVersion(); + const args = version.split('.'); + return parseInt(args[0], 10) >= 17; + } + + public async getScreenSize(): Promise { + const wda = await this.wda(); + return await wda.getScreenSize(); + } + + public async swipe(direction: SwipeDirection): Promise { + const wda = await this.wda(); + await wda.swipe(direction); + } + + public async swipeFromCoordinate( + x: number, + y: number, + direction: SwipeDirection, + distance?: number, + ): Promise { + const wda = await this.wda(); + await wda.swipeFromCoordinate(x, y, direction, distance); + } + + public async listApps(): Promise { + await this.assertTunnelRunning(); + + const output = await this.ios('apps', '--all', '--list'); + return output.split('\n').map((line) => { + const [packageName, appName] = line.split(' '); + return { + packageName, + appName, + }; + }); + } + + public async launchApp(packageName: string, locale?: string): Promise { + validatePackageName(packageName); + await this.assertTunnelRunning(); + const args = ['launch', packageName]; + if (locale) { + validateLocale(locale); + const locales = locale.split(',').map((l) => l.trim()); + args.push('-AppleLanguages', `(${locales.join(', ')})`); + args.push('-AppleLocale', locales[0]); + } + + await this.ios(...args); + } + + public async terminateApp(packageName: string): Promise { + validatePackageName(packageName); + await this.assertTunnelRunning(); + await this.ios('kill', packageName); + } + + public async installApp( + path: string, + _options?: InstallOptions, + ): Promise { + await this.assertTunnelRunning(); + try { + await this.ios('install', '--path', path); + } catch (error: any) { + const stdout = error.stdout ? error.stdout.toString() : ''; + const stderr = error.stderr ? error.stderr.toString() : ''; + const output = (stdout + stderr).trim(); + throw new ActionableError(output || error.message); + } + } + + public async uninstallApp(bundleId: string): Promise { + await this.assertTunnelRunning(); + try { + await this.ios('uninstall', '--bundleid', bundleId); + } catch (error: any) { + const stdout = error.stdout ? error.stdout.toString() : ''; + const stderr = error.stderr ? error.stderr.toString() : ''; + const output = (stdout + stderr).trim(); + throw new ActionableError(output || error.message); + } + } + + public async openUrl(url: string): Promise { + const wda = await this.wda(); + await wda.openUrl(url); + } + + public async sendKeys(text: string): Promise { + const wda = await this.wda(); + await wda.sendKeys(text); + } + + public async pressButton(button: Button): Promise { + const wda = await this.wda(); + await wda.pressButton(button); + } + + public async tap(x: number, y: number): Promise { + const wda = await this.wda(); + await wda.tap(x, y); + } + + public async doubleTap(x: number, y: number): Promise { + const wda = await this.wda(); + await wda.doubleTap(x, y); + } + + public async longPress( + x: number, + y: number, + duration: number, + ): Promise { + const wda = await this.wda(); + await wda.longPress(x, y, duration); + } + + public async getElementsOnScreen(): Promise { + const wda = await this.wda(); + return await wda.getElementsOnScreen(); + } + + public async getScreenshot(): Promise { + const wda = await this.wda(); + return await wda.getScreenshot(); + + /* alternative: + await this.assertTunnelRunning(); + const tmpFilename = path.join(tmpdir(), `screenshot-${randomBytes(8).toString("hex")}.png`); + await this.ios("screenshot", "--output", tmpFilename); + const buffer = readFileSync(tmpFilename); + unlinkSync(tmpFilename); + return buffer; + */ + } + + public async setOrientation(orientation: Orientation): Promise { + const wda = await this.wda(); + await wda.setOrientation(orientation); + } + + public async getOrientation(): Promise { + const wda = await this.wda(); + return await wda.getOrientation(); + } +} + +export class IosManager { + public isGoIosInstalled(): boolean { + try { + const output = execFileSync(getGoIosPath(), ['version'], { + stdio: ['pipe', 'pipe', 'ignore'], + }).toString(); + const json: VersionCommandOutput = JSON.parse(output); + return ( + json.version !== undefined && + (/^v?\d+\.\d+\.\d+/.test(json.version) || + json.version === 'local-build') + ); + } catch (error) { + return false; + } + } + + public getDeviceName(deviceId: string): string { + const output = execFileSync(getGoIosPath(), [ + 'info', + '--udid', + deviceId, + ]).toString(); + const json: InfoCommandOutput = JSON.parse(output); + return json.DeviceName; + } + + public getDeviceInfo(deviceId: string): InfoCommandOutput { + const output = execFileSync(getGoIosPath(), [ + 'info', + '--udid', + deviceId, + ]).toString(); + const json: InfoCommandOutput = JSON.parse(output); + return json; + } + + public listDevices(): IosDevice[] { + if (!this.isGoIosInstalled()) { + console.error( + 'go-ios is not installed, no physical iOS devices can be detected', + ); + return []; + } + + const output = execFileSync(getGoIosPath(), ['list']).toString(); + const json: ListCommandOutput = JSON.parse(output); + const devices = json.deviceList.map((device) => ({ + deviceId: device, + deviceName: this.getDeviceName(device), + })); + + return devices; + } + + public listDevicesWithDetails(): Array { + if (!this.isGoIosInstalled()) { + console.error( + 'go-ios is not installed, no physical iOS devices can be detected', + ); + return []; + } + + const output = execFileSync(getGoIosPath(), ['list']).toString(); + const json: ListCommandOutput = JSON.parse(output); + const devices = json.deviceList.map((device) => { + const info = this.getDeviceInfo(device); + return { + deviceId: device, + deviceName: info.DeviceName, + version: info.ProductVersion, + }; + }); + + return devices; + } +} diff --git a/packages/mobile-mcp/src/iphone-simulator.ts b/packages/mobile-mcp/src/iphone-simulator.ts new file mode 100644 index 0000000000..b9854ad841 --- /dev/null +++ b/packages/mobile-mcp/src/iphone-simulator.ts @@ -0,0 +1,312 @@ +import { execFileSync } from 'node:child_process'; +import { mkdtempSync, readdirSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join, basename, extname } from 'node:path'; + +import { trace } from './logger'; +import { WebDriverAgent } from './webdriver-agent'; +import { + ActionableError, + Button, + InstalledApp, + InstallOptions, + Robot, + ScreenElement, + ScreenSize, + SwipeDirection, + Orientation, +} from './robot'; +import { validatePackageName, validateLocale } from './utils'; + +export interface Simulator { + name: string; + uuid: string; + state: string; +} + +interface AppInfo { + ApplicationType: string; + Bundle: string; + CFBundleDisplayName: string; + CFBundleExecutable: string; + CFBundleIdentifier: string; + CFBundleName: string; + CFBundleVersion: string; + DataContainer: string; + Path: string; +} + +const TIMEOUT = 30000; +const WDA_PORT = 8100; +const MAX_BUFFER_SIZE = 1024 * 1024 * 8; + +export class Simctl implements Robot { + constructor(private readonly simulatorUuid: string) {} + + private async isWdaInstalled(): Promise { + const apps = await this.listApps(); + return apps + .map((app) => app.packageName) + .includes('com.facebook.WebDriverAgentRunner.xctrunner'); + } + + private async startWda(): Promise { + if (!(await this.isWdaInstalled())) { + // wda is not even installed, won't attempt to start it + return; + } + + trace('Starting WebDriverAgent'); + const webdriverPackageName = 'com.facebook.WebDriverAgentRunner.xctrunner'; + this.simctl('launch', this.simulatorUuid, webdriverPackageName); + + // now we wait for wda to have a successful status + const wda = new WebDriverAgent('localhost', WDA_PORT); + + // wait up to 10 seconds for wda to start + const timeout = +new Date() + 10 * 1000; + while (+new Date() < timeout) { + // cross fingers and see if wda is already running + if (await wda.isRunning()) { + trace('WebDriverAgent is now running'); + return; + } + + // wait 100ms before trying again + await new Promise((resolve) => setTimeout(resolve, 100)); + } + + trace('Could not start WebDriverAgent in time, giving up'); + } + + private async wda(): Promise { + const wda = new WebDriverAgent('localhost', WDA_PORT); + + if (!(await wda.isRunning())) { + await this.startWda(); + if (!(await wda.isRunning())) { + throw new ActionableError( + 'WebDriverAgent is not running on simulator, please see https://github.com/mobile-next/mobile-mcp/wiki/', + ); + } + + // was successfully started + } + + return wda; + } + + private simctl(...args: string[]): Buffer { + return execFileSync('xcrun', ['simctl', ...args], { + timeout: TIMEOUT, + maxBuffer: MAX_BUFFER_SIZE, + }); + } + + public async getScreenshot(): Promise { + const wda = await this.wda(); + return await wda.getScreenshot(); + // alternative: return this.simctl("io", this.simulatorUuid, "screenshot", "-"); + } + + public async openUrl(url: string) { + const wda = await this.wda(); + await wda.openUrl(url); + // alternative: this.simctl("openurl", this.simulatorUuid, url); + } + + public async launchApp(packageName: string, locale?: string) { + validatePackageName(packageName); + const args = ['launch', this.simulatorUuid, packageName]; + if (locale) { + validateLocale(locale); + const locales = locale.split(',').map((l) => l.trim()); + args.push('-AppleLanguages', `(${locales.join(', ')})`); + args.push('-AppleLocale', locales[0]); + } + + this.simctl(...args); + } + + public async terminateApp(packageName: string) { + validatePackageName(packageName); + this.simctl('terminate', this.simulatorUuid, packageName); + } + + private findAppBundle(dir: string): string | null { + const entries = readdirSync(dir, { withFileTypes: true }); + + for (const entry of entries) { + if (entry.isDirectory() && entry.name.endsWith('.app')) { + return join(dir, entry.name); + } + } + + return null; + } + + private validateZipPaths(zipPath: string): void { + const output = execFileSync('/usr/bin/zipinfo', ['-1', zipPath], { + timeout: TIMEOUT, + maxBuffer: MAX_BUFFER_SIZE, + }).toString(); + + const invalidPath = output + .split('\n') + .map((s) => s.trim()) + .filter((s) => s) + .find((s) => s.startsWith('/') || s.includes('..')); + + if (invalidPath) { + throw new ActionableError( + `Security violation: File path '${invalidPath}' contains invalid characters`, + ); + } + } + + public async installApp( + path: string, + _options?: InstallOptions, + ): Promise { + let tempDir: string | null = null; + let installPath = path; + + try { + // zip files need to be extracted prior to installation + if (extname(path).toLowerCase() === '.zip') { + trace(`Detected .zip file, validating contents`); + + // before extracting, let's make sure there's no zip-slip bombs here + this.validateZipPaths(path); + + tempDir = mkdtempSync(join(tmpdir(), 'ios-app-')); + + try { + execFileSync('unzip', ['-q', path, '-d', tempDir], { + timeout: TIMEOUT, + }); + } catch (error: any) { + throw new ActionableError(`Failed to unzip file: ${error.message}`); + } + + const appBundle = this.findAppBundle(tempDir); + if (!appBundle) { + throw new ActionableError( + 'No .app bundle found in the .zip file, please visit wiki at https://github.com/mobile-next/mobile-mcp/wiki for assistance.', + ); + } + + installPath = appBundle; + trace(`Found .app bundle at: ${basename(appBundle)}`); + } + + // continue with installation + this.simctl('install', this.simulatorUuid, installPath); + } catch (error: any) { + const stdout = error.stdout ? error.stdout.toString() : ''; + const stderr = error.stderr ? error.stderr.toString() : ''; + const output = (stdout + stderr).trim(); + throw new ActionableError(output || error.message); + } finally { + // Clean up temporary directory if it was created + if (tempDir) { + try { + trace(`Cleaning up temporary directory`); + rmSync(tempDir, { recursive: true, force: true }); + } catch (cleanupError) { + trace( + `Warning: Failed to cleanup temporary directory: ${cleanupError}`, + ); + } + } + } + } + + public async uninstallApp(bundleId: string): Promise { + try { + this.simctl('uninstall', this.simulatorUuid, bundleId); + } catch (error: any) { + const stdout = error.stdout ? error.stdout.toString() : ''; + const stderr = error.stderr ? error.stderr.toString() : ''; + const output = (stdout + stderr).trim(); + throw new ActionableError(output || error.message); + } + } + + public async listApps(): Promise { + const text = this.simctl('listapps', this.simulatorUuid).toString(); + const result = execFileSync( + 'plutil', + ['-convert', 'json', '-o', '-', '-r', '-'], + { + input: text, + }, + ); + + const output = JSON.parse(result.toString()) as Record; + return Object.values(output).map((app) => ({ + packageName: app.CFBundleIdentifier, + appName: app.CFBundleDisplayName, + })); + } + + public async getScreenSize(): Promise { + const wda = await this.wda(); + return wda.getScreenSize(); + } + + public async sendKeys(keys: string) { + const wda = await this.wda(); + return wda.sendKeys(keys); + } + + public async swipe(direction: SwipeDirection): Promise { + const wda = await this.wda(); + return wda.swipe(direction); + } + + public async swipeFromCoordinate( + x: number, + y: number, + direction: SwipeDirection, + distance?: number, + ): Promise { + const wda = await this.wda(); + return wda.swipeFromCoordinate(x, y, direction, distance); + } + + public async tap(x: number, y: number) { + const wda = await this.wda(); + return wda.tap(x, y); + } + + public async doubleTap(x: number, y: number): Promise { + const wda = await this.wda(); + await wda.doubleTap(x, y); + } + + public async longPress(x: number, y: number, duration: number) { + const wda = await this.wda(); + return wda.longPress(x, y, duration); + } + + public async pressButton(button: Button) { + const wda = await this.wda(); + return wda.pressButton(button); + } + + public async getElementsOnScreen(): Promise { + const wda = await this.wda(); + return wda.getElementsOnScreen(); + } + + public async setOrientation(orientation: Orientation): Promise { + const wda = await this.wda(); + return wda.setOrientation(orientation); + } + + public async getOrientation(): Promise { + const wda = await this.wda(); + return wda.getOrientation(); + } +} diff --git a/packages/mobile-mcp/src/logger.ts b/packages/mobile-mcp/src/logger.ts new file mode 100644 index 0000000000..3a76009946 --- /dev/null +++ b/packages/mobile-mcp/src/logger.ts @@ -0,0 +1,21 @@ +import { appendFileSync } from "node:fs"; + +const writeLog = (message: string) => { + if (process.env.LOG_FILE) { + const logfile = process.env.LOG_FILE; + const timestamp = new Date().toISOString(); + const levelStr = "INFO"; + const logMessage = `[${timestamp}] ${levelStr} ${message}`; + appendFileSync(logfile, logMessage + "\n"); + } + + console.error(message); +}; + +export const trace = (message: string) => { + writeLog(message); +}; + +export const error = (message: string) => { + writeLog(message); +}; diff --git a/packages/mobile-mcp/src/mobile-device.ts b/packages/mobile-mcp/src/mobile-device.ts new file mode 100644 index 0000000000..49a6c3f4a5 --- /dev/null +++ b/packages/mobile-mcp/src/mobile-device.ts @@ -0,0 +1,263 @@ +import { Mobilecli } from './mobilecli'; +import { + Button, + InstalledApp, + InstallOptions, + Orientation, + Robot, + ScreenElement, + ScreenSize, + SwipeDirection, +} from './robot'; + +interface InstalledAppsResponse { + status: 'ok'; + data: Array<{ + packageName: string; + appName?: string; // ios + version?: string; // ios + }>; +} + +interface DeviceInfoResponse { + status: 'ok'; + data: { + device: { + id: string; + name: string; + platform: string; + type: string; + version: string; + state: string; + screenSize?: { + width: number; + height: number; + scale: number; + }; + }; + }; +} + +interface UIElementResponse { + type: string; + label?: string; + text?: string; + name?: string; + value?: string; + identifier?: string; + rect: { + x: number; + y: number; + width: number; + height: number; + }; + focused?: boolean; +} + +interface DumpUIResponse { + status: 'ok'; + data: { + elements: UIElementResponse[]; + }; +} + +interface OrientationResponse { + status: 'ok'; + data: { + orientation: Orientation; + }; +} + +export class MobileDevice implements Robot { + private mobilecli: Mobilecli; + + public constructor(private deviceId: string) { + this.mobilecli = new Mobilecli(); + } + + private runCommand(args: string[]): string { + const fullArgs = [...args, '--device', this.deviceId]; + return this.mobilecli.executeCommand(fullArgs); + } + + public async getScreenSize(): Promise { + const response = JSON.parse( + this.runCommand(['device', 'info']), + ) as DeviceInfoResponse; + if (response.data.device.screenSize) { + return response.data.device.screenSize; + } + return { width: 0, height: 0, scale: 1.0 }; + } + + public async swipe(direction: SwipeDirection): Promise { + const screenSize = await this.getScreenSize(); + const centerX = Math.floor(screenSize.width / 2); + const centerY = Math.floor(screenSize.height / 2); + const distance = 400; // Default distance in pixels + + let startX = centerX; + let startY = centerY; + let endX = centerX; + let endY = centerY; + + switch (direction) { + case 'up': + startY = centerY + distance / 2; + endY = centerY - distance / 2; + break; + case 'down': + startY = centerY - distance / 2; + endY = centerY + distance / 2; + break; + case 'left': + startX = centerX + distance / 2; + endX = centerX - distance / 2; + break; + case 'right': + startX = centerX - distance / 2; + endX = centerX + distance / 2; + break; + } + + this.runCommand(['io', 'swipe', `${startX},${startY},${endX},${endY}`]); + } + + public async swipeFromCoordinate( + x: number, + y: number, + direction: SwipeDirection, + distance?: number, + ): Promise { + const swipeDistance = distance || 400; + let endX = x; + let endY = y; + + switch (direction) { + case 'up': + endY = y - swipeDistance; + break; + case 'down': + endY = y + swipeDistance; + break; + case 'left': + endX = x - swipeDistance; + break; + case 'right': + endX = x + swipeDistance; + break; + } + + this.runCommand(['io', 'swipe', `${x},${y},${endX},${endY}`]); + } + + public async getScreenshot(): Promise { + const fullArgs = [ + 'screenshot', + '--device', + this.deviceId, + '--format', + 'png', + '--output', + '-', + ]; + return this.mobilecli.executeCommandBuffer(fullArgs); + } + + public async listApps(): Promise { + const response = JSON.parse( + this.runCommand(['apps', 'list']), + ) as InstalledAppsResponse; + return response.data.map((app) => ({ + appName: app.appName || app.packageName, + packageName: app.packageName, + })) as InstalledApp[]; + } + + public async launchApp(packageName: string, locale?: string): Promise { + const args = ['apps', 'launch', packageName]; + if (locale) { + args.push('--locale', locale); + } + + this.runCommand(args); + } + + public async terminateApp(packageName: string): Promise { + this.runCommand(['apps', 'terminate', packageName]); + } + + public async installApp( + path: string, + _options?: InstallOptions, + ): Promise { + this.runCommand(['apps', 'install', path]); + } + + public async uninstallApp(bundleId: string): Promise { + this.runCommand(['apps', 'uninstall', bundleId]); + } + + public async openUrl(url: string): Promise { + this.runCommand(['url', url]); + } + + public async sendKeys(text: string): Promise { + this.runCommand(['io', 'text', text]); + } + + public async pressButton(button: Button): Promise { + this.runCommand(['io', 'button', button]); + } + + public async tap(x: number, y: number): Promise { + this.runCommand(['io', 'tap', `${x},${y}`]); + } + + public async doubleTap(x: number, y: number): Promise { + // TODO: should move into mobilecli itself as "io doubletap" + await this.tap(x, y); + await this.tap(x, y); + } + + public async longPress( + x: number, + y: number, + duration: number, + ): Promise { + this.runCommand([ + 'io', + 'longpress', + `${x},${y}`, + '--duration', + `${duration}`, + ]); + } + + public async getElementsOnScreen(): Promise { + const response = JSON.parse( + this.runCommand(['dump', 'ui']), + ) as DumpUIResponse; + return response.data.elements.map((element) => ({ + type: element.type, + label: element.label, + text: element.text, + name: element.name, + value: element.value, + identifier: element.identifier, + rect: element.rect, + focused: element.focused, + })); + } + + public async setOrientation(orientation: Orientation): Promise { + this.runCommand(['device', 'orientation', 'set', orientation]); + } + + public async getOrientation(): Promise { + const response = JSON.parse( + this.runCommand(['device', 'orientation', 'get']), + ) as OrientationResponse; + return response.data.orientation; + } +} diff --git a/packages/mobile-mcp/src/mobilecli.ts b/packages/mobile-mcp/src/mobilecli.ts new file mode 100644 index 0000000000..f5394f84f8 --- /dev/null +++ b/packages/mobile-mcp/src/mobilecli.ts @@ -0,0 +1,207 @@ +import { existsSync } from "node:fs"; +import { dirname, join, sep } from "node:path"; +import { execFileSync, spawn, ChildProcess } from "node:child_process"; + +export interface MobilecliCrashEntry { + processName: string; + timestamp: string; + id: string; +} + +export interface MobilecliCrashesListResponse { + status: "ok"; + data: MobilecliCrashEntry[]; +} + +export interface MobilecliCrashGetResponse { + status: "ok"; + data: { + content: string; + id: string; + }; +} + +export interface MobilecliAgentStatusResponse { + status: "ok" | "fail"; + data: { + message: string; + }; +} + +export interface MobilecliDevicesOptions { + includeOffline?: boolean; + platform?: "ios" | "android"; + type?: "real" | "emulator" | "simulator"; +} + +export interface MobilecliDeviceProvider { + type: string; // e.g. "mobilefleet" for remote devices + allocationId?: string; +} + +export interface MobilecliDevice { + id: string; + name: string; + platform: "android" | "ios"; + type: "real" | "emulator" | "simulator"; + version: string; + provider?: MobilecliDeviceProvider; +} + +export interface MobilecliDevicesResponse { + status: "ok"; + data: { + devices: MobilecliDevice[]; + }; +} + +const TIMEOUT = 30000; +const MAX_BUFFER_SIZE = 1024 * 1024 * 8; + +export class Mobilecli { + private path: string | null = null; + + constructor() { } + + private getPath(): string { + if (!this.path) { + this.path = Mobilecli.getMobilecliPath(); + } + return this.path; + } + + public executeCommand(args: string[]): string { + const path = this.getPath(); + return execFileSync(path, args, { encoding: "utf8" }).toString().trim(); + } + + public spawnCommand(args: string[]): ChildProcess { + const binaryPath = this.getPath(); + return spawn(binaryPath, args, { + stdio: ["ignore", "ignore", "ignore"], + }); + } + + public executeCommandBuffer(args: string[]): Buffer { + const path = this.getPath(); + return execFileSync(path, args, { + encoding: "buffer", + maxBuffer: MAX_BUFFER_SIZE, + timeout: TIMEOUT, + }) as Buffer; + } + + private static getMobilecliPath(): string { + if (process.env.MOBILECLI_PATH) { + return process.env.MOBILECLI_PATH; + } + + const platform = process.platform; + const arch = process.arch; + + const normalizedPlatform = platform === "win32" ? "windows" : platform; + const normalizedArch = arch === "arm64" ? "arm64" : "amd64"; + const ext = platform === "win32" ? ".exe" : ""; + const binaryName = `mobilecli-${normalizedPlatform}-${normalizedArch}${ext}`; + + // Check if mobile-mcp is installed as a package + const currentPath = __filename; + const pathParts = currentPath.split(sep); + const lastNodeModulesIndex = pathParts.lastIndexOf("node_modules"); + + if (lastNodeModulesIndex !== -1) { + // We're inside node_modules, go to the last node_modules in the path + const nodeModulesParts = pathParts.slice(0, lastNodeModulesIndex + 1); + const lastNodeModulesPath = nodeModulesParts.join(sep); + const mobilecliPath = join(lastNodeModulesPath, "mobilecli", "bin", binaryName); + + if (existsSync(mobilecliPath)) { + return mobilecliPath; + } + } + + // Not in node_modules, look one directory up from current script + const scriptDir = dirname(__filename); + const parentDir = dirname(scriptDir); + const mobilecliPath = join(parentDir, "node_modules", "mobilecli", "bin", binaryName); + + if (existsSync(mobilecliPath)) { + return mobilecliPath; + } + + throw new Error(`Could not find mobilecli binary for platform: ${platform}`); + } + + getVersion(): string { + try { + const output = this.executeCommand(["--version"]); + if (output.startsWith("mobilecli version ")) { + return output.substring("mobilecli version ".length); + } + + return "failed"; + } catch (error: any) { + return "failed " + error.message; + } + } + + remoteListDevices(): string { + return this.executeCommand(["remote", "list-devices"]); + } + + remoteAllocate(platform: "ios" | "android"): string { + return this.executeCommand(["remote", "allocate", "--platform", platform]); + } + + remoteRelease(deviceId: string): string { + return this.executeCommand(["remote", "release", "--device", deviceId]); + } + + crashesList(deviceId: string): MobilecliCrashesListResponse { + const output = this.executeCommand(["device", "crashes", "list", "--device", deviceId]); + return JSON.parse(output) as MobilecliCrashesListResponse; + } + + crashesGet(deviceId: string, id: string): MobilecliCrashGetResponse { + const output = this.executeCommandBuffer(["device", "crashes", "get", id, "--device", deviceId]); + return JSON.parse(output.toString().trim()) as MobilecliCrashGetResponse; + } + + agentStatus(deviceId: string): MobilecliAgentStatusResponse { + const output = this.executeCommand(["agent", "status", "--device", deviceId]); + return JSON.parse(output) as MobilecliAgentStatusResponse; + } + + agentInstall(deviceId: string): void { + this.executeCommand(["agent", "install", "--device", deviceId]); + } + + getDevices(options?: MobilecliDevicesOptions): MobilecliDevicesResponse { + const args = ["devices"]; + + if (options) { + if (options.includeOffline) { + args.push("--include-offline"); + } + + if (options.platform) { + if (options.platform !== "ios" && options.platform !== "android") { + throw new Error(`Invalid platform: ${options.platform}. Must be "ios" or "android"`); + } + + args.push("--platform", options.platform); + } + + if (options.type) { + if (options.type !== "real" && options.type !== "emulator" && options.type !== "simulator") { + throw new Error(`Invalid type: ${options.type}. Must be "real", "emulator", or "simulator"`); + } + + args.push("--type", options.type); + } + } + + const mobilecliOutput = this.executeCommand(args); + return JSON.parse(mobilecliOutput) as MobilecliDevicesResponse; + } +} diff --git a/packages/mobile-mcp/src/png.ts b/packages/mobile-mcp/src/png.ts new file mode 100644 index 0000000000..dc87a9062b --- /dev/null +++ b/packages/mobile-mcp/src/png.ts @@ -0,0 +1,20 @@ +export interface PngDimensions { + width: number; + height: number; +} + +export class PNG { + public constructor(private readonly buffer: Buffer) { + } + + public getDimensions(): PngDimensions { + const pngSignature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]); + if (!this.buffer.subarray(0, 8).equals(pngSignature)) { + throw new Error("Not a valid PNG file"); + } + + const width = this.buffer.readUInt32BE(16); + const height = this.buffer.readUInt32BE(20); + return { width, height }; + } +} diff --git a/packages/mobile-mcp/src/robot.ts b/packages/mobile-mcp/src/robot.ts new file mode 100644 index 0000000000..1c0e1e1cf7 --- /dev/null +++ b/packages/mobile-mcp/src/robot.ts @@ -0,0 +1,172 @@ +export interface Dimensions { + width: number; + height: number; +} + +export interface ScreenSize extends Dimensions { + scale: number; +} + +export interface InstalledApp { + packageName: string; + appName: string; +} + +export type SwipeDirection = 'up' | 'down' | 'left' | 'right'; + +export type Button = + | 'HOME' + | 'BACK' + | 'VOLUME_UP' + | 'VOLUME_DOWN' + | 'ENTER' + | 'DPAD_CENTER' + | 'DPAD_UP' + | 'DPAD_DOWN' + | 'DPAD_LEFT' + | 'DPAD_RIGHT'; + +export interface ScreenElementRect { + x: number; + y: number; + width: number; + height: number; +} + +export interface ScreenElement { + type: string; + label?: string; + text?: string; + name?: string; + value?: string; + identifier?: string; + rect: ScreenElementRect; + + // currently only on android tv + focused?: boolean; +} + +export class ActionableError extends Error { + constructor(message: string) { + super(message); + } +} + +export interface InstallOptions { + /** -r: replace existing application (default true for backward compatibility) */ + replace?: boolean; + /** -g: grant all runtime permissions on install */ + grantPermissions?: boolean; + /** -d: allow version code downgrade */ + allowDowngrade?: boolean; + /** -t: allow test APKs */ + allowTest?: boolean; +} + +export type Orientation = 'portrait' | 'landscape'; + +export interface Robot { + /** + * Get the screen size of the device in pixels. + */ + getScreenSize(): Promise; + + /** + * Swipe in a direction. + */ + swipe(direction: SwipeDirection): Promise; + + /** + * Swipe from a specific coordinate in a direction. + */ + swipeFromCoordinate( + x: number, + y: number, + direction: SwipeDirection, + distance?: number, + ): Promise; + + /** + * Get a screenshot of the screen. Returns a Buffer that contains + * a PNG image of the screen. Will be same dimensions as getScreenSize(). + */ + getScreenshot(): Promise; + + /** + * List all installed apps on the device. Returns an array of package names (or + * bundle identifiers in iOS) for all installed apps. + */ + listApps(): Promise; + + /** + * Launch an app. + */ + launchApp(packageName: string, locale?: string): Promise; + + /** + * Terminate an app. If app was already terminated (or non existent) then this + * is a no-op. + */ + terminateApp(packageName: string): Promise; + + /** + * Install an app on the device from a file path. + */ + installApp(path: string, options?: InstallOptions): Promise; + + /** + * Uninstall an app from the device. + */ + uninstallApp(bundleId: string): Promise; + + /** + * Open a URL in the device's web browser. Can be an https:// url, or a + * custom scheme (e.g. "myapp://"). + */ + openUrl(url: string): Promise; + + /** + * Send keys to the device, simulating keyboard input. + */ + sendKeys(text: string): Promise; + + /** + * Press a button on the device, simulating a physical button press. + */ + pressButton(button: Button): Promise; + + /** + * Tap on a specific coordinate on the screen. + */ + tap(x: number, y: number): Promise; + + /** + * Tap on a specific coordinate on the screen. + */ + doubleTap(x: number, y: number): Promise; + + /** + * Long press on a specific coordinate on the screen. + * @param x - The x coordinate to long press + * @param y - The y coordinate to long press + * @param duration - Duration of the long press in milliseconds + */ + longPress(x: number, y: number, duration: number): Promise; + + /** + * Get all elements on the screen. Works only on native apps (not webviews). Will + * return a filtered list of elements that make sense to interact with. + */ + getElementsOnScreen(): Promise; + + /** + * Change the screen orientation of the device. + * @param orientation The desired orientation ("portrait" or "landscape") + */ + setOrientation(orientation: Orientation): Promise; + + /** + * Get the current screen orientation. + */ + getOrientation(): Promise; +} diff --git a/packages/mobile-mcp/src/server.ts b/packages/mobile-mcp/src/server.ts new file mode 100644 index 0000000000..23d10ba561 --- /dev/null +++ b/packages/mobile-mcp/src/server.ts @@ -0,0 +1,1329 @@ +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { z } from 'zod'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import crypto from 'node:crypto'; +import { ChildProcess } from 'node:child_process'; + +import { error, trace } from './logger'; +import { AndroidRobot, AndroidDeviceManager } from './android'; +import { ActionableError, Robot, InstallOptions } from './robot'; +import { IosManager, IosRobot } from './ios'; +import { PNG } from './png'; +import { isScalingAvailable, Image } from './image-utils'; +import { Mobilecli } from './mobilecli'; +import { MobileDevice } from './mobile-device'; +import { validateOutputPath, validateFileExtension } from './utils'; +import { + isNormalized, + coordinateScale, + denormalizeArgs, + normalizeElementResult, + normalizeScreenSizeResult, + ingestScreenSizeFromResult, + getCachedScreenSize, + cacheScreenSize, + invalidateScreenSize, + rewriteDescription, + coordParamDesc, +} from './coord-norm'; + +const ALLOWED_SCREENSHOT_EXTENSIONS = ['.png', '.jpg', '.jpeg']; +const ALLOWED_RECORDING_EXTENSIONS = ['.mp4']; + +interface MobilecliDevice { + id: string; + name: string; + platform: 'android' | 'ios'; + type: 'real' | 'emulator' | 'simulator'; + version: string; + state: 'online' | 'offline'; +} + +interface MobilecliDevicesResponse { + devices: MobilecliDevice[]; +} + +interface ActiveRecording { + process: ChildProcess; + outputPath: string; + startedAt: number; +} + +export const getAgentVersion = (): string => { + const json = require('../package.json'); + return json.version; +}; + +export const createMcpServer = (): McpServer => { + const server = new McpServer({ + name: 'mobile-mcp', + version: getAgentVersion(), + }); + + const getClientName = (): string => { + try { + const clientInfo = server.server.getClientVersion(); + const clientName = clientInfo?.name || 'unknown'; + return clientName; + } catch (error: any) { + return 'unknown'; + } + }; + + type ZodSchemaShape = Record; + + interface ToolAnnotations { + readOnlyHint?: boolean; + destructiveHint?: boolean; + } + + const tool = ( + name: string, + title: string, + description: string, + paramsSchema: ZodSchemaShape, + annotations: ToolAnnotations, + cb: (args: any) => Promise, + ) => { + const finalDescription = isNormalized() + ? rewriteDescription(description) + : description; + server.registerTool( + name, + { + title, + description: finalDescription, + inputSchema: paramsSchema, + annotations, + }, + (async (args: any, _extra: any) => { + try { + trace(`Invoking ${name} with args: ${JSON.stringify(args)}`); + + // coord shim: denormalize input 0–scale → pixels + if (isNormalized() && args.device) { + const size = await ensureScreenSize(args.device); + if (size) { + denormalizeArgs(name, args, size.width, size.height); + } + } + + const start = +new Date(); + let response = await cb(args); + const duration = +new Date() - start; + + // coord shim: ingest screen size from get_screen_size before normalizing + if (name === 'mobile_get_screen_size' && args.device) { + ingestScreenSizeFromResult(args.device, response); + } + + // coord shim: normalize output pixels → 0–scale + if (isNormalized() && args.device) { + const size = getCachedScreenSize(args.device); + if (size) { + response = normalizeElementResult( + name, + response, + size.width, + size.height, + ); + } + response = normalizeScreenSizeResult(name, response); + } + + trace(`=> ${response}`); + posthog('tool_invoked', { + ToolName: name, + Duration: duration, + }).then(); + return { + content: [{ type: 'text', text: response }], + }; + } catch (error: any) { + posthog('tool_failed', { ToolName: name }).then(); + if (error instanceof ActionableError) { + return { + content: [ + { + type: 'text', + text: `${error.message}. Please fix the issue and try again.`, + }, + ], + }; + } else { + // a real exception + trace( + `Tool '${description}' failed: ${error.message} stack: ${error.stack}`, + ); + return { + content: [{ type: 'text', text: `Error: ${error.message}` }], + isError: true, + }; + } + } + }) as any, + ); + }; + + const posthog = async ( + event: string, + properties: Record, + ) => { + // Telemetry disabled in @qwen-code/mobile-mcp fork. + // Set MOBILEMCP_ENABLE_TELEMETRY=1 to re-enable upstream PostHog reporting. + if (!process.env.MOBILEMCP_ENABLE_TELEMETRY) { + return; + } + + try { + const url = 'https://us.i.posthog.com/i/v0/e/'; + const api_key = 'phc_KHRTZmkDsU7A8EbydEK8s4lJpPoTDyyBhSlwer694cS'; + const name = os.hostname() + process.execPath; + const distinct_id = crypto + .createHash('sha256') + .update(name) + .digest('hex'); + const systemProps: any = { + Platform: os.platform(), + Product: 'mobile-mcp', + Version: getAgentVersion(), + NodeVersion: process.version, + CI: process.env.CI || '0', + }; + + const clientName = getClientName(); + if (clientName !== 'unknown') { + systemProps.AgentName = clientName; + } + + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + api_key, + event, + properties: { + ...systemProps, + ...properties, + }, + distinct_id, + }), + }); + } catch (err: any) { + // ignore + } + }; + + const mobilecli = new Mobilecli(); + const activeRecordings = new Map(); + const agentVerifiedSimulators = new Set(); + posthog('launch', {}).then(); + + const ensureMobilecliAvailable = (): void => { + try { + const version = mobilecli.getVersion(); + if (version.startsWith('failed')) { + throw new Error('mobilecli version check failed'); + } + } catch (error: any) { + throw new ActionableError( + `mobilecli is not available or not working properly. Please review the documentation at https://github.com/mobile-next/mobile-mcp/wiki for installation instructions`, + ); + } + }; + + const getRobotFromDevice = (deviceId: string): Robot => { + // from now on, we must have mobilecli working + ensureMobilecliAvailable(); + + // Check if it's an iOS device + const iosManager = new IosManager(); + const iosDevices = iosManager.listDevices(); + const iosDevice = iosDevices.find((d) => d.deviceId === deviceId); + if (iosDevice) { + posthog('get_robot', { + DevicePlatform: 'ios', + DeviceType: 'real', + }).then(); + return new IosRobot(deviceId); + } + + // Check if it's an Android device + const androidManager = new AndroidDeviceManager(); + const androidDevices = androidManager.getConnectedDevices(); + const androidDevice = androidDevices.find((d) => d.deviceId === deviceId); + if (androidDevice) { + posthog('get_robot', { DevicePlatform: 'android' }).then(); + return new AndroidRobot(deviceId); + } + + // Check if it's a simulator (will later replace all other device types as well) + const response = mobilecli.getDevices({ + platform: 'ios', + type: 'simulator', + includeOffline: false, + }); + + if (response.status === 'ok' && response.data && response.data.devices) { + for (const device of response.data.devices) { + if (device.id === deviceId) { + if (!agentVerifiedSimulators.has(deviceId)) { + const agentStatus = mobilecli.agentStatus(deviceId); + if (agentStatus.status === 'fail') { + mobilecli.agentInstall(deviceId); + } + + agentVerifiedSimulators.add(deviceId); + } + + posthog('get_robot', { + DevicePlatform: 'ios', + DeviceType: 'simulator', + }).then(); + return new MobileDevice(deviceId); + } + } + } + + throw new ActionableError( + `Device "${deviceId}" not found. Use the mobile_list_available_devices tool to see available devices.`, + ); + }; + + const getAndroidRobotFromDevice = ( + deviceId: string, + toolName: string, + ): AndroidRobot => { + const androidManager = new AndroidDeviceManager(); + const androidDevices = androidManager.getConnectedDevices(); + const androidDevice = androidDevices.find((d) => d.deviceId === deviceId); + if (!androidDevice) { + throw new ActionableError( + `${toolName} is only supported on Android devices.`, + ); + } + return new AndroidRobot(deviceId); + }; + + async function ensureScreenSize( + deviceId: string, + ): Promise<{ width: number; height: number } | undefined> { + let size = getCachedScreenSize(deviceId); + if (size) return size; + try { + const robot = getRobotFromDevice(deviceId); + const screenSize = await robot.getScreenSize(); + cacheScreenSize(deviceId, screenSize.width, screenSize.height); + return { width: screenSize.width, height: screenSize.height }; + } catch (err: any) { + trace( + `[coord-norm] Failed to get screen size for ${deviceId}: ${err.message}. Coordinates will pass through without normalization.`, + ); + return undefined; + } + } + + tool( + 'mobile_list_available_devices', + 'List Devices', + 'List all available devices. This includes both physical mobile devices and mobile simulators and emulators. It returns both Android and iOS devices.', + {}, + { readOnlyHint: true }, + async ({}) => { + // from today onward, we must have mobilecli working + ensureMobilecliAvailable(); + + const iosManager = new IosManager(); + const androidManager = new AndroidDeviceManager(); + const devices: MobilecliDevice[] = []; + + // Get Android devices with details + const androidDevices = androidManager.getConnectedDevicesWithDetails(); + for (const device of androidDevices) { + devices.push({ + id: device.deviceId, + name: device.name, + platform: 'android', + type: 'emulator', + version: device.version, + state: 'online', + }); + } + + // Get iOS physical devices with details + try { + const iosDevices = iosManager.listDevicesWithDetails(); + for (const device of iosDevices) { + devices.push({ + id: device.deviceId, + name: device.deviceName, + platform: 'ios', + type: 'real', + version: device.version, + state: 'online', + }); + } + } catch (error: any) { + // If go-ios is not available, silently skip + } + + // Get iOS simulators from mobilecli (excluding offline devices) + const response = mobilecli.getDevices({ + platform: 'ios', + type: 'simulator', + includeOffline: false, + }); + if (response.status === 'ok' && response.data && response.data.devices) { + for (const device of response.data.devices) { + devices.push({ + id: device.id, + name: device.name, + platform: device.platform, + type: device.type, + version: device.version, + state: 'online', + }); + } + } + + const out: MobilecliDevicesResponse = { devices }; + return JSON.stringify(out); + }, + ); + + if (process.env.MOBILEFLEET_ENABLE === '1') { + tool( + 'mobile_list_remote_devices', + 'List Remote Devices', + 'List devices available in the remote fleet', + {}, + { readOnlyHint: true }, + async ({}) => { + ensureMobilecliAvailable(); + const result = mobilecli.remoteListDevices(); + return result; + }, + ); + + tool( + 'mobile_allocate_remote_device', + 'Allocate Remote Device', + 'Reserve a device from the remote fleet', + { + platform: z + .enum(['ios', 'android']) + .describe('The platform to allocate a device for'), + }, + { destructiveHint: true }, + async ({ platform }) => { + ensureMobilecliAvailable(); + const result = mobilecli.remoteAllocate(platform); + return result; + }, + ); + + tool( + 'mobile_release_remote_device', + 'Release Remote Device', + 'Release a device back to the remote fleet', + { + device: z + .string() + .describe( + 'The device identifier to release back to the remote fleet', + ), + }, + { destructiveHint: true }, + async ({ device }) => { + ensureMobilecliAvailable(); + const result = mobilecli.remoteRelease(device); + return result; + }, + ); + } + + tool( + 'mobile_list_apps', + 'List Apps', + 'List all the installed apps on the device', + { + device: z + .string() + .describe( + 'The device identifier to use. Use mobile_list_available_devices to find which devices are available to you.', + ), + }, + { readOnlyHint: true }, + async ({ device }) => { + const robot = getRobotFromDevice(device); + const result = await robot.listApps(); + return `Found these apps on device: ${result.map((app) => `${app.appName} (${app.packageName})`).join(', ')}`; + }, + ); + + tool( + 'mobile_launch_app', + 'Launch App', + 'Launch an app on mobile device. Use this to open a specific app. You can find the package name of the app by calling list_apps_on_device.', + { + device: z + .string() + .describe( + 'The device identifier to use. Use mobile_list_available_devices to find which devices are available to you.', + ), + packageName: z.string().describe('The package name of the app to launch'), + locale: z + .string() + .optional() + .describe( + 'Comma-separated BCP 47 locale tags to launch the app with (e.g., fr-FR,en-GB)', + ), + }, + { destructiveHint: true }, + async ({ device, packageName, locale }) => { + const robot = getRobotFromDevice(device); + await robot.launchApp(packageName, locale); + return `Launched app ${packageName}`; + }, + ); + + tool( + 'mobile_terminate_app', + 'Terminate App', + 'Stop and terminate an app on mobile device', + { + device: z + .string() + .describe( + 'The device identifier to use. Use mobile_list_available_devices to find which devices are available to you.', + ), + packageName: z + .string() + .describe('The package name of the app to terminate'), + }, + { destructiveHint: true }, + async ({ device, packageName }) => { + const robot = getRobotFromDevice(device); + await robot.terminateApp(packageName); + return `Terminated app ${packageName}`; + }, + ); + + tool( + 'mobile_install_app', + 'Install App', + 'Install an app on mobile device', + { + device: z + .string() + .describe( + 'The device identifier to use. Use mobile_list_available_devices to find which devices are available to you.', + ), + path: z + .string() + .describe( + 'The path to the app file to install. For iOS simulators, provide a .zip file or a .app directory. For Android provide an .apk file. For iOS real devices provide an .ipa file', + ), + grant_permissions: z + .boolean() + .optional() + .describe( + '(Android only) Grant all runtime permissions on install (-g flag). Defaults to false.', + ), + replace: z + .boolean() + .optional() + .describe( + '(Android only) Replace existing application (-r flag). Defaults to true.', + ), + allow_downgrade: z + .boolean() + .optional() + .describe( + '(Android only) Allow version code downgrade (-d flag). Defaults to false.', + ), + allow_test: z + .boolean() + .optional() + .describe( + '(Android only) Allow test APKs (-t flag). Defaults to false.', + ), + }, + { destructiveHint: true }, + async ({ + device, + path, + grant_permissions, + replace, + allow_downgrade, + allow_test, + }) => { + const robot = getRobotFromDevice(device); + const options: InstallOptions = { + grantPermissions: grant_permissions, + replace: replace, + allowDowngrade: allow_downgrade, + allowTest: allow_test, + }; + await robot.installApp(path, options); + return `Installed app from ${path}`; + }, + ); + + tool( + 'mobile_uninstall_app', + 'Uninstall App', + 'Uninstall an app from mobile device', + { + device: z + .string() + .describe( + 'The device identifier to use. Use mobile_list_available_devices to find which devices are available to you.', + ), + bundle_id: z + .string() + .describe( + 'Bundle identifier (iOS) or package name (Android) of the app to be uninstalled', + ), + }, + { destructiveHint: true }, + async ({ device, bundle_id }) => { + const robot = getRobotFromDevice(device); + await robot.uninstallApp(bundle_id); + return `Uninstalled app ${bundle_id}`; + }, + ); + + tool( + 'mobile_get_screen_size', + 'Get Screen Size', + 'Get the screen size of the mobile device in pixels', + { + device: z + .string() + .describe( + 'The device identifier to use. Use mobile_list_available_devices to find which devices are available to you.', + ), + }, + { readOnlyHint: true }, + async ({ device }) => { + const robot = getRobotFromDevice(device); + const screenSize = await robot.getScreenSize(); + return `Screen size is ${screenSize.width}x${screenSize.height} pixels`; + }, + ); + + tool( + 'mobile_click_on_screen_at_coordinates', + 'Click Screen', + 'Click on the screen at given x,y coordinates. If clicking on an element, use the list_elements_on_screen tool to find the coordinates.', + { + device: z + .string() + .describe( + 'The device identifier to use. Use mobile_list_available_devices to find which devices are available to you.', + ), + x: z.coerce + .number() + .describe( + coordParamDesc('The x coordinate to click on the screen, in pixels'), + ), + y: z.coerce + .number() + .describe( + coordParamDesc('The y coordinate to click on the screen, in pixels'), + ), + }, + { destructiveHint: true }, + async ({ device, x, y }) => { + const robot = getRobotFromDevice(device); + await robot.tap(x, y); + return `Clicked on screen at coordinates: ${x}, ${y}`; + }, + ); + + tool( + 'mobile_double_tap_on_screen', + 'Double Tap Screen', + 'Double-tap on the screen at given x,y coordinates.', + { + device: z + .string() + .describe( + 'The device identifier to use. Use mobile_list_available_devices to find which devices are available to you.', + ), + x: z.coerce + .number() + .describe(coordParamDesc('The x coordinate to double-tap, in pixels')), + y: z.coerce + .number() + .describe(coordParamDesc('The y coordinate to double-tap, in pixels')), + }, + { destructiveHint: true }, + async ({ device, x, y }) => { + const robot = getRobotFromDevice(device); + await robot!.doubleTap(x, y); + return `Double-tapped on screen at coordinates: ${x}, ${y}`; + }, + ); + + tool( + 'mobile_long_press_on_screen_at_coordinates', + 'Long Press Screen', + 'Long press on the screen at given x,y coordinates. If long pressing on an element, use the list_elements_on_screen tool to find the coordinates.', + { + device: z + .string() + .describe( + 'The device identifier to use. Use mobile_list_available_devices to find which devices are available to you.', + ), + x: z.coerce + .number() + .describe( + coordParamDesc( + 'The x coordinate to long press on the screen, in pixels', + ), + ), + y: z.coerce + .number() + .describe( + coordParamDesc( + 'The y coordinate to long press on the screen, in pixels', + ), + ), + duration: z.coerce + .number() + .min(1) + .max(10000) + .optional() + .describe( + 'Duration of the long press in milliseconds. Defaults to 500ms.', + ), + }, + { destructiveHint: true }, + async ({ device, x, y, duration }) => { + const robot = getRobotFromDevice(device); + const pressDuration = duration ?? 500; + await robot.longPress(x, y, pressDuration); + return `Long pressed on screen at coordinates: ${x}, ${y} for ${pressDuration}ms`; + }, + ); + + tool( + 'mobile_list_elements_on_screen', + 'List Screen Elements', + 'List elements on screen and their coordinates, with display text or accessibility label. Do not cache this result.', + { + device: z + .string() + .describe( + 'The device identifier to use. Use mobile_list_available_devices to find which devices are available to you.', + ), + }, + { readOnlyHint: true }, + async ({ device }) => { + const robot = getRobotFromDevice(device); + const elements = await robot.getElementsOnScreen(); + + const result = elements.map((element) => { + const out: any = { + type: element.type, + text: element.text, + label: element.label, + name: element.name, + value: element.value, + identifier: element.identifier, + coordinates: { + x: element.rect.x, + y: element.rect.y, + width: element.rect.width, + height: element.rect.height, + }, + }; + + if (element.focused) { + out.focused = true; + } + + return out; + }); + + return `Found these elements on screen: ${JSON.stringify(result)}`; + }, + ); + + tool( + 'mobile_press_button', + 'Press Button', + 'Press a button on device', + { + device: z + .string() + .describe( + 'The device identifier to use. Use mobile_list_available_devices to find which devices are available to you.', + ), + button: z + .string() + .describe( + 'The button to press. Supported buttons: BACK (android only), HOME, VOLUME_UP, VOLUME_DOWN, ENTER, DPAD_CENTER (android tv only), DPAD_UP (android tv only), DPAD_DOWN (android tv only), DPAD_LEFT (android tv only), DPAD_RIGHT (android tv only)', + ), + }, + { destructiveHint: true }, + async ({ device, button }) => { + const robot = getRobotFromDevice(device); + await robot.pressButton(button); + return `Pressed the button: ${button}`; + }, + ); + + tool( + 'mobile_open_url', + 'Open URL', + 'Open a URL in browser on device', + { + device: z + .string() + .describe( + 'The device identifier to use. Use mobile_list_available_devices to find which devices are available to you.', + ), + url: z.string().describe('The URL to open'), + }, + { destructiveHint: true }, + async ({ device, url }) => { + const allowUnsafeUrls = process.env.MOBILEMCP_ALLOW_UNSAFE_URLS === '1'; + if ( + !allowUnsafeUrls && + !url.startsWith('http://') && + !url.startsWith('https://') + ) { + throw new ActionableError( + 'Only http:// and https:// URLs are allowed. Set MOBILEMCP_ALLOW_UNSAFE_URLS=1 to allow other URL schemes.', + ); + } + + const robot = getRobotFromDevice(device); + await robot.openUrl(url); + return `Opened URL: ${url}`; + }, + ); + + tool( + 'mobile_swipe_on_screen', + 'Swipe Screen', + 'Swipe on the screen', + { + device: z + .string() + .describe( + 'The device identifier to use. Use mobile_list_available_devices to find which devices are available to you.', + ), + direction: z + .enum(['up', 'down', 'left', 'right']) + .describe('The direction to swipe'), + x: z.coerce + .number() + .optional() + .describe( + coordParamDesc( + 'The x coordinate to start the swipe from, in pixels. If not provided, uses center of screen', + ), + ), + y: z.coerce + .number() + .optional() + .describe( + coordParamDesc( + 'The y coordinate to start the swipe from, in pixels. If not provided, uses center of screen', + ), + ), + distance: z.coerce + .number() + .optional() + .describe( + coordParamDesc( + 'The distance to swipe in pixels. Defaults to 400 pixels for iOS or 30% of screen dimension for Android', + ), + ), + }, + { destructiveHint: true }, + async ({ device, direction, x, y, distance }) => { + const robot = getRobotFromDevice(device); + + if (x !== undefined && y !== undefined) { + // Use coordinate-based swipe + await robot.swipeFromCoordinate(x, y, direction, distance); + const distanceText = distance ? ` ${distance} pixels` : ''; + return `Swiped ${direction}${distanceText} from coordinates: ${x}, ${y}`; + } else { + // Use center-based swipe + await robot.swipe(direction); + return `Swiped ${direction} on screen`; + } + }, + ); + + tool( + 'mobile_type_keys', + 'Type Text', + 'Type text into the focused element', + { + device: z + .string() + .describe( + 'The device identifier to use. Use mobile_list_available_devices to find which devices are available to you.', + ), + text: z.string().describe('The text to type'), + submit: z + .boolean() + .describe( + 'Whether to submit the text. If true, the text will be submitted as if the user pressed the enter key.', + ), + }, + { destructiveHint: true }, + async ({ device, text, submit }) => { + const robot = getRobotFromDevice(device); + await robot.sendKeys(text); + + if (submit) { + await robot.pressButton('ENTER'); + } + + return `Typed text: ${text}`; + }, + ); + + tool( + 'mobile_save_screenshot', + 'Save Screenshot', + 'Save a screenshot of the mobile device to a file', + { + device: z + .string() + .describe( + 'The device identifier to use. Use mobile_list_available_devices to find which devices are available to you.', + ), + saveTo: z + .string() + .describe( + 'The path to save the screenshot to. Filename must end with .png, .jpg, or .jpeg', + ), + }, + { destructiveHint: true }, + async ({ device, saveTo }) => { + validateFileExtension( + saveTo, + ALLOWED_SCREENSHOT_EXTENSIONS, + 'save_screenshot', + ); + validateOutputPath(saveTo); + + const robot = getRobotFromDevice(device); + + const screenshot = await robot.getScreenshot(); + fs.writeFileSync(saveTo, screenshot); + return `Screenshot saved to: ${saveTo}`; + }, + ); + + server.registerTool( + 'mobile_take_screenshot', + { + title: 'Take Screenshot', + description: + "Take a screenshot of the mobile device. Use this to understand what's on screen, if you need to press an element that is available through view hierarchy then you must list elements on screen instead. Do not cache this result.", + inputSchema: { + device: z + .string() + .describe( + 'The device identifier to use. Use mobile_list_available_devices to find which devices are available to you.', + ), + }, + annotations: { + readOnlyHint: true, + }, + }, + async ({ device }) => { + try { + const robot = getRobotFromDevice(device); + const screenSize = await robot.getScreenSize(); + + let screenshot = await robot.getScreenshot(); + let mimeType = 'image/png'; + + // validate we received a png, will throw exception otherwise + const image = new PNG(screenshot); + const pngSize = image.getDimensions(); + if (pngSize.width <= 0 || pngSize.height <= 0) { + throw new ActionableError('Screenshot is invalid. Please try again.'); + } + + if (isScalingAvailable()) { + trace('Image scaling is available, resizing screenshot'); + const image = Image.fromBuffer(screenshot); + const beforeSize = screenshot.length; + screenshot = image + .resize(Math.floor(pngSize.width / screenSize.scale)) + .jpeg({ quality: 75 }) + .toBuffer(); + + const afterSize = screenshot.length; + trace( + `Screenshot resized from ${beforeSize} bytes to ${afterSize} bytes`, + ); + + mimeType = 'image/jpeg'; + } + + const screenshot64 = screenshot.toString('base64'); + trace(`Screenshot taken: ${screenshot.length} bytes`); + posthog('tool_invoked', { + ToolName: 'mobile_take_screenshot', + ScreenshotFilesize: screenshot64.length, + ScreenshotMimeType: mimeType, + ScreenshotWidth: pngSize.width, + ScreenshotHeight: pngSize.height, + }).then(); + + const content: any[] = [ + { type: 'image', data: screenshot64, mimeType }, + ]; + if (isNormalized()) { + const scale = coordinateScale(); + content.push({ + type: 'text', + text: `Screenshot dimensions: ${scale}x${scale} (normalized coordinate space)`, + }); + } + return { content }; + } catch (err: any) { + error(`Error taking screenshot: ${err.message} ${err.stack}`); + return { + content: [{ type: 'text', text: `Error: ${err.message}` }], + isError: true, + }; + } + }, + ); + + tool( + 'mobile_set_orientation', + 'Set Orientation', + 'Change the screen orientation of the device', + { + device: z + .string() + .describe( + 'The device identifier to use. Use mobile_list_available_devices to find which devices are available to you.', + ), + orientation: z + .enum(['portrait', 'landscape']) + .describe('The desired orientation'), + }, + { destructiveHint: true }, + async ({ device, orientation }) => { + const robot = getRobotFromDevice(device); + await robot.setOrientation(orientation); + invalidateScreenSize(device); + return `Changed device orientation to ${orientation}`; + }, + ); + + tool( + 'mobile_get_orientation', + 'Get Orientation', + 'Get the current screen orientation of the device', + { + device: z + .string() + .describe( + 'The device identifier to use. Use mobile_list_available_devices to find which devices are available to you.', + ), + }, + { readOnlyHint: true }, + async ({ device }) => { + const robot = getRobotFromDevice(device); + const orientation = await robot.getOrientation(); + return `Current device orientation is ${orientation}`; + }, + ); + + tool( + 'mobile_start_screen_recording', + 'Start Screen Recording', + 'Start recording the screen of a mobile device. The recording runs in the background until stopped with mobile_stop_screen_recording. Returns the path where the recording will be saved.', + { + device: z + .string() + .describe( + 'The device identifier to use. Use mobile_list_available_devices to find which devices are available to you.', + ), + output: z + .string() + .optional() + .describe( + 'The file path to save the recording to. Filename must end with .mp4. If not provided, a temporary path will be used.', + ), + timeLimit: z.coerce + .number() + .optional() + .describe( + 'Maximum recording duration in seconds. The recording will stop automatically after this time.', + ), + }, + { destructiveHint: true }, + async ({ device, output, timeLimit }) => { + if (output) { + validateFileExtension( + output, + ALLOWED_RECORDING_EXTENSIONS, + 'start_screen_recording', + ); + validateOutputPath(output); + } + + getRobotFromDevice(device); + + if (activeRecordings.has(device)) { + throw new ActionableError( + `Device "${device}" is already being recorded. Stop the current recording first with mobile_stop_screen_recording.`, + ); + } + + const outputPath = + output || path.join(os.tmpdir(), `screen-recording-${Date.now()}.mp4`); + + const args = [ + 'screenrecord', + '--device', + device, + '--output', + outputPath, + '--silent', + ]; + if (timeLimit !== undefined) { + args.push('--time-limit', String(timeLimit)); + } + + const child = mobilecli.spawnCommand(args); + + const cleanup = () => { + activeRecordings.delete(device); + }; + + child.on('error', cleanup); + child.on('exit', cleanup); + + activeRecordings.set(device, { + process: child, + outputPath, + startedAt: Date.now(), + }); + + return `Screen recording started. Output will be saved to: ${outputPath}`; + }, + ); + + tool( + 'mobile_stop_screen_recording', + 'Stop Screen Recording', + 'Stop an active screen recording on a mobile device. Returns the file path, size, and approximate duration of the recording.', + { + device: z + .string() + .describe( + 'The device identifier to use. Use mobile_list_available_devices to find which devices are available to you.', + ), + }, + { destructiveHint: true }, + async ({ device }) => { + const recording = activeRecordings.get(device); + if (!recording) { + throw new ActionableError( + `No active recording found for device "${device}". Start a recording first with mobile_start_screen_recording.`, + ); + } + + const { process: child, outputPath, startedAt } = recording; + activeRecordings.delete(device); + + child.kill('SIGINT'); + + await new Promise((resolve) => { + const timeout = setTimeout( + () => { + child.kill('SIGKILL'); + resolve(); + }, + 5 * 60 * 1000, + ); + + child.on('close', () => { + clearTimeout(timeout); + resolve(); + }); + }); + + const durationSeconds = Math.round((Date.now() - startedAt) / 1000); + + if (!fs.existsSync(outputPath)) { + return `Recording stopped after ~${durationSeconds}s but the output file was not found at: ${outputPath}`; + } + + const stats = fs.statSync(outputPath); + const fileSizeMB = (stats.size / (1024 * 1024)).toFixed(2); + + return `Recording stopped. File: ${outputPath} (${fileSizeMB} MB, ~${durationSeconds}s)`; + }, + ); + + tool( + 'mobile_list_crashes', + 'List Crash Reports', + 'List crash reports available on the device', + { + device: z + .string() + .describe( + 'The device identifier to use. Use mobile_list_available_devices to find which devices are available to you.', + ), + }, + { readOnlyHint: true }, + async ({ device }) => { + ensureMobilecliAvailable(); + const response = mobilecli.crashesList(device); + return JSON.stringify(response.data); + }, + ); + + tool( + 'mobile_get_crash', + 'Get Crash Report', + 'Get the full content of a crash report by its ID. Use mobile_list_crashes to find available crash IDs.', + { + device: z + .string() + .describe( + 'The device identifier to use. Use mobile_list_available_devices to find which devices are available to you.', + ), + id: z.string().describe('The crash report ID to retrieve'), + }, + { readOnlyHint: true }, + async ({ device, id }) => { + ensureMobilecliAvailable(); + const response = mobilecli.crashesGet(device, id); + return response.data.content; + }, + ); + + // --- Android-specific ADB tools --- + + tool( + 'mobile_ui_dump', + 'UI Hierarchy Dump', + '(Android only) Dump the full UI hierarchy as raw XML using uiautomator. Unlike mobile_list_elements_on_screen which returns a filtered flat list of interactive elements as JSON, this returns the complete unfiltered XML tree preserving parent-child hierarchy and all node attributes (class, resource-id, bounds, clickable, scrollable, enabled, etc.). Use when you need the full view tree for debugging, or when mobile_list_elements_on_screen misses an element you can see on screen. Supports --compressed to reduce output size.', + { + device: z + .string() + .describe( + 'The device identifier to use. Use mobile_list_available_devices to find which devices are available to you.', + ), + compressed: z + .boolean() + .optional() + .describe( + 'Whether to use --compressed flag to reduce XML output size. Defaults to false.', + ), + output_path: z + .string() + .optional() + .describe( + 'If provided, save the XML to this local file path instead of returning it in the response.', + ), + }, + { readOnlyHint: true }, + async ({ device, compressed, output_path }) => { + const robot = getAndroidRobotFromDevice(device, 'mobile_ui_dump'); + const xml = await robot.dumpUiHierarchy(compressed ?? false); + if (output_path) { + validateOutputPath(output_path); + fs.writeFileSync(output_path, xml, 'utf-8'); + return `UI hierarchy XML saved to: ${output_path}`; + } + return xml; + }, + ); + + tool( + 'mobile_adb_pull', + 'ADB Pull File', + '(Android only) Pull (download) a file from the Android device to the local filesystem.', + { + device: z + .string() + .describe( + 'The device identifier to use. Use mobile_list_available_devices to find which devices are available to you.', + ), + remote_path: z + .string() + .describe('The file path on the Android device to pull from'), + local_path: z + .string() + .describe('The local file path to save the pulled file to.'), + }, + { destructiveHint: true }, + async ({ device, remote_path, local_path }) => { + const robot = getAndroidRobotFromDevice(device, 'mobile_adb_pull'); + validateOutputPath(local_path); + robot.pullFile(remote_path, local_path); + return `Successfully pulled ${remote_path} to ${local_path}`; + }, + ); + + tool( + 'mobile_adb_push', + 'ADB Push File', + '(Android only) Push (upload) a file from local filesystem to the Android device. By default only allows pushing to /sdcard/. Set force=true to push to other paths.', + { + device: z + .string() + .describe( + 'The device identifier to use. Use mobile_list_available_devices to find which devices are available to you.', + ), + local_path: z + .string() + .describe('The local file path to push to the device'), + remote_path: z + .string() + .describe('The target file path on the Android device'), + force: z + .boolean() + .optional() + .describe( + 'Set to true to allow pushing to paths outside /sdcard/. Defaults to false.', + ), + }, + { destructiveHint: true }, + async ({ device, local_path, remote_path, force }) => { + const robot = getAndroidRobotFromDevice(device, 'mobile_adb_push'); + if (!force) { + const resolved = path.posix.resolve('/', remote_path); + if (!resolved.startsWith('/sdcard/')) { + throw new ActionableError( + `Push target path must resolve under /sdcard/ for safety. Got: "${remote_path}" (resolved: "${resolved}"). Set force=true to override.`, + ); + } + } + if (!fs.existsSync(local_path)) { + throw new ActionableError(`Local file not found: "${local_path}"`); + } + robot.pushFile(local_path, remote_path); + return `Successfully pushed ${local_path} to ${remote_path}`; + }, + ); + + return server; +}; diff --git a/packages/mobile-mcp/src/utils.ts b/packages/mobile-mcp/src/utils.ts new file mode 100644 index 0000000000..9ba30337d4 --- /dev/null +++ b/packages/mobile-mcp/src/utils.ts @@ -0,0 +1,88 @@ +import path from "node:path"; +import os from "node:os"; +import fs from "node:fs"; +import { ActionableError } from "./robot"; + +export function validatePackageName(packageName: string): void { + if (!/^[a-zA-Z0-9._]+$/.test(packageName)) { + throw new ActionableError(`Invalid package name: "${packageName}"`); + } +} + +export function validateLocale(locale: string): void { + if (!/^[a-zA-Z0-9,\- ]+$/.test(locale)) { + throw new ActionableError(`Invalid locale: "${locale}"`); + } +} + +function getAllowedRoots(): string[] { + const roots = [ + os.tmpdir(), + process.cwd(), + ]; + + // macOS /tmp is a symlink to /private/tmp, add both to be safe + if (process.platform === "darwin") { + roots.push("/tmp"); + roots.push("/private/tmp"); + } + + return roots.map(r => path.resolve(r)); +} + +function isPathUnderRoot(filePath: string, root: string): boolean { + const relative = path.relative(root, filePath); + if (relative === "") { + return false; + } + + if (path.isAbsolute(relative)) { + return false; + } + + if (relative.startsWith("..")) { + return false; + } + + return true; +} + +export function validateFileExtension(filePath: string, allowedExtensions: string[], toolName: string): void { + const ext = path.extname(filePath).toLowerCase(); + if (!allowedExtensions.includes(ext)) { + throw new ActionableError(`${toolName} requires a ${allowedExtensions.join(", ")} file extension, got: "${ext || "(none)"}"`); + } +} + +function resolveWithSymlinks(filePath: string): string { + const resolved = path.resolve(filePath); + const dir = path.dirname(resolved); + const filename = path.basename(resolved); + + try { + return path.join(fs.realpathSync(dir), filename); + } catch { + return resolved; + } +} + +export function validateOutputPath(filePath: string): void { + const resolved = resolveWithSymlinks(filePath); + const allowedRoots = getAllowedRoots(); + const isWindows = process.platform === "win32"; + + const isAllowed = allowedRoots.some(root => { + if (isWindows) { + return isPathUnderRoot(resolved.toLowerCase(), root.toLowerCase()); + } + + return isPathUnderRoot(resolved, root); + }); + + if (!isAllowed) { + const dir = path.dirname(resolved); + throw new ActionableError( + `"${dir}" is not in the list of allowed directories. Allowed directories include the current directory and the temp directory on this host.` + ); + } +} diff --git a/packages/mobile-mcp/src/webdriver-agent.ts b/packages/mobile-mcp/src/webdriver-agent.ts new file mode 100644 index 0000000000..a46a0c3fe3 --- /dev/null +++ b/packages/mobile-mcp/src/webdriver-agent.ts @@ -0,0 +1,454 @@ +import { ActionableError, SwipeDirection, ScreenSize, ScreenElement, Orientation } from "./robot"; + +export interface SourceTreeElementRect { + x: number; + y: number; + width: number; + height: number; +} + +export interface SourceTreeElement { + type: string; + label?: string; + name?: string; + value?: string; + rawIdentifier?: string; + rect: SourceTreeElementRect; + isVisible?: string; // "0" or "1" + children?: Array; +} + +export interface SourceTree { + value: SourceTreeElement; +} + +export class WebDriverAgent { + + constructor(private readonly host: string, private readonly port: number) { + } + + public async isRunning(): Promise { + const url = `http://${this.host}:${this.port}/status`; + try { + const response = await fetch(url); + const json = await response.json(); + return response.status === 200 && json.value?.ready === true; + } catch (error) { + // console.error(`Failed to connect to WebDriverAgent: ${error}`); + return false; + } + } + + public async createSession(): Promise { + const url = `http://${this.host}:${this.port}/session`; + const response = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ capabilities: { alwaysMatch: { platformName: "iOS" } } }), + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new ActionableError(`Failed to create WebDriver session: ${response.status} ${errorText}`); + } + + const json = await response.json(); + if (!json.value || !json.value.sessionId) { + throw new ActionableError(`Invalid session response: ${JSON.stringify(json)}`); + } + + return json.value.sessionId; + } + + public async deleteSession(sessionId: string) { + const url = `http://${this.host}:${this.port}/session/${sessionId}`; + const response = await fetch(url, { method: "DELETE" }); + return response.json(); + } + + public async withinSession(fn: (url: string) => Promise) { + const sessionId = await this.createSession(); + const url = `http://${this.host}:${this.port}/session/${sessionId}`; + const result = await fn(url); + await this.deleteSession(sessionId); + return result; + } + + public async getScreenSize(sessionUrl?: string): Promise { + if (sessionUrl) { + const url = `${sessionUrl}/wda/screen`; + const response = await fetch(url); + const json = await response.json(); + return { + width: json.value.screenSize.width, + height: json.value.screenSize.height, + scale: json.value.scale || 1, + }; + } else { + return this.withinSession(async sessionUrlInner => { + const url = `${sessionUrlInner}/wda/screen`; + const response = await fetch(url); + const json = await response.json(); + return { + width: json.value.screenSize.width, + height: json.value.screenSize.height, + scale: json.value.scale || 1, + }; + }); + } + } + + public async sendKeys(keys: string) { + await this.withinSession(async sessionUrl => { + const url = `${sessionUrl}/wda/keys`; + await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ value: [keys] }), + }); + }); + } + + public async pressButton(button: string) { + const _map = { + "HOME": "home", + "VOLUME_UP": "volumeup", + "VOLUME_DOWN": "volumedown", + }; + + if (button === "ENTER") { + await this.sendKeys("\n"); + return; + } + + // Type assertion to check if button is a key of _map + if (!(button in _map)) { + throw new ActionableError(`Button "${button}" is not supported`); + } + + await this.withinSession(async sessionUrl => { + const url = `${sessionUrl}/wda/pressButton`; + const response = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: button, + }), + }); + + return response.json(); + }); + } + + public async tap(x: number, y: number) { + await this.withinSession(async sessionUrl => { + const url = `${sessionUrl}/actions`; + await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + actions: [ + { + type: "pointer", + id: "finger1", + parameters: { pointerType: "touch" }, + actions: [ + { type: "pointerMove", duration: 0, x, y }, + { type: "pointerDown", button: 0 }, + { type: "pause", duration: 100 }, + { type: "pointerUp", button: 0 } + ] + } + ] + }), + }); + }); + } + + public async doubleTap(x: number, y: number) { + await this.withinSession(async sessionUrl => { + const url = `${sessionUrl}/actions`; + await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + actions: [ + { + type: "pointer", + id: "finger1", + parameters: { pointerType: "touch" }, + actions: [ + { type: "pointerMove", duration: 0, x, y }, + { type: "pointerDown", button: 0 }, + { type: "pause", duration: 50 }, + { type: "pointerUp", button: 0 }, + + { type: "pause", duration: 100 }, + + { type: "pointerDown", button: 0 }, + { type: "pause", duration: 50 }, + { type: "pointerUp", button: 0 } + ] + } + ] + }), + }); + }); + } + + public async longPress(x: number, y: number, duration: number) { + await this.withinSession(async sessionUrl => { + const url = `${sessionUrl}/actions`; + await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + actions: [ + { + type: "pointer", + id: "finger1", + parameters: { pointerType: "touch" }, + actions: [ + { type: "pointerMove", duration: 0, x, y }, + { type: "pointerDown", button: 0 }, + { type: "pause", duration }, + { type: "pointerUp", button: 0 } + ] + } + ] + }), + }); + }); + } + + private isVisible(rect: SourceTreeElementRect): boolean { + return rect.x >= 0 && rect.y >= 0; + } + + private filterSourceElements(source: SourceTreeElement): Array { + const output: ScreenElement[] = []; + + const acceptedTypes = ["TextField", "Button", "Switch", "Icon", "SearchField", "StaticText", "Image"]; + + if (acceptedTypes.includes(source.type)) { + if (source.isVisible === "1" && this.isVisible(source.rect)) { + if (source.label !== null || source.name !== null || source.rawIdentifier !== null) { + output.push({ + type: source.type, + label: source.label, + name: source.name, + value: source.value, + identifier: source.rawIdentifier, + rect: { + x: source.rect.x, + y: source.rect.y, + width: source.rect.width, + height: source.rect.height, + }, + }); + } + } + } + + if (source.children) { + for (const child of source.children) { + output.push(...this.filterSourceElements(child)); + } + } + + return output; + } + + public async getPageSource(): Promise { + const url = `http://${this.host}:${this.port}/source/?format=json`; + const response = await fetch(url); + const json = await response.json(); + return json as SourceTree; + } + + public async getElementsOnScreen(): Promise { + const source = await this.getPageSource(); + return this.filterSourceElements(source.value); + } + + public async openUrl(url: string): Promise { + await this.withinSession(async sessionUrl => { + await fetch(`${sessionUrl}/url`, { + method: "POST", + body: JSON.stringify({ url }), + }); + }); + } + + public async getScreenshot(): Promise { + const url = `http://${this.host}:${this.port}/screenshot`; + const response = await fetch(url); + const json = await response.json(); + return Buffer.from(json.value, "base64"); + } + + public async swipe(direction: SwipeDirection): Promise { + await this.withinSession(async sessionUrl => { + const screenSize = await this.getScreenSize(sessionUrl); + let x0: number, y0: number, x1: number, y1: number; + // Use 60% of the width/height for swipe distance + const verticalDistance = Math.floor(screenSize.height * 0.6); + const horizontalDistance = Math.floor(screenSize.width * 0.6); + const centerX = Math.floor(screenSize.width / 2); + const centerY = Math.floor(screenSize.height / 2); + + switch (direction) { + case "up": + x0 = x1 = centerX; + y0 = centerY + Math.floor(verticalDistance / 2); + y1 = centerY - Math.floor(verticalDistance / 2); + break; + case "down": + x0 = x1 = centerX; + y0 = centerY - Math.floor(verticalDistance / 2); + y1 = centerY + Math.floor(verticalDistance / 2); + break; + case "left": + y0 = y1 = centerY; + x0 = centerX + Math.floor(horizontalDistance / 2); + x1 = centerX - Math.floor(horizontalDistance / 2); + break; + case "right": + y0 = y1 = centerY; + x0 = centerX - Math.floor(horizontalDistance / 2); + x1 = centerX + Math.floor(horizontalDistance / 2); + break; + default: + throw new ActionableError(`Swipe direction "${direction}" is not supported`); + } + + const url = `${sessionUrl}/actions`; + const response = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + actions: [ + { + type: "pointer", + id: "finger1", + parameters: { pointerType: "touch" }, + actions: [ + { type: "pointerMove", duration: 0, x: x0, y: y0 }, + { type: "pointerDown", button: 0 }, + { type: "pointerMove", duration: 1000, x: x1, y: y1 }, + { type: "pointerUp", button: 0 } + ] + } + ] + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new ActionableError(`WebDriver actions request failed: ${response.status} ${errorText}`); + } + + // Clear actions to ensure they complete + await fetch(`${sessionUrl}/actions`, { + method: "DELETE", + }); + }); + } + + public async swipeFromCoordinate(x: number, y: number, direction: SwipeDirection, distance: number = 400): Promise { + await this.withinSession(async sessionUrl => { + // Use simple coordinates like the working swipe method + const x0 = x; + const y0 = y; + let x1 = x; + let y1 = y; + + // Calculate target position based on direction and distance + switch (direction) { + case "up": + y1 = y - distance; // Move up by specified distance + break; + case "down": + y1 = y + distance; // Move down by specified distance + break; + case "left": + x1 = x - distance; // Move left by specified distance + break; + case "right": + x1 = x + distance; // Move right by specified distance + break; + default: + throw new ActionableError(`Swipe direction "${direction}" is not supported`); + } + + const url = `${sessionUrl}/actions`; + const response = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + actions: [ + { + type: "pointer", + id: "finger1", + parameters: { pointerType: "touch" }, + actions: [ + { type: "pointerMove", duration: 0, x: x0, y: y0 }, + { type: "pointerDown", button: 0 }, + { type: "pointerMove", duration: 1000, x: x1, y: y1 }, + { type: "pointerUp", button: 0 } + ] + } + ] + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new ActionableError(`WebDriver actions request failed: ${response.status} ${errorText}`); + } + + // Clear actions to ensure they complete + await fetch(`${sessionUrl}/actions`, { + method: "DELETE", + }); + }); + } + + public async setOrientation(orientation: Orientation): Promise { + await this.withinSession(async sessionUrl => { + const url = `${sessionUrl}/orientation`; + await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + orientation: orientation.toUpperCase() + }) + }); + }); + } + + public async getOrientation(): Promise { + return this.withinSession(async sessionUrl => { + const url = `${sessionUrl}/orientation`; + const response = await fetch(url); + const json = await response.json(); + return json.value.toLowerCase() as Orientation; + }); + } +} diff --git a/packages/mobile-mcp/test/android.ts b/packages/mobile-mcp/test/android.ts new file mode 100644 index 0000000000..a0e0516d50 --- /dev/null +++ b/packages/mobile-mcp/test/android.ts @@ -0,0 +1,139 @@ +import { test, expect } from "@playwright/test"; + +import { PNG } from "../src/png"; +import { AndroidRobot, AndroidDeviceManager } from "../src/android"; + +const manager = new AndroidDeviceManager(); +const devices = manager.getConnectedDevices(); +const hasOneAndroidDevice = devices.length === 1; + +test.describe("android", () => { + + const android = new AndroidRobot(devices?.[0]?.deviceId || ""); + + test("should be able to get the screen size", async () => { + test.skip(!hasOneAndroidDevice, "requires exactly one android device"); + const screenSize = await android.getScreenSize(); + expect(screenSize.width).toBeGreaterThan(1024); + expect(screenSize.height).toBeGreaterThan(1024); + expect(screenSize.scale).toBe(1); + expect(Object.keys(screenSize).length, "screenSize should have exactly 3 properties").toBe(3); + }); + + test("should be able to take screenshot", async () => { + test.skip(!hasOneAndroidDevice, "requires exactly one android device"); + + const screenSize = await android.getScreenSize(); + const screenshot = await android.getScreenshot(); + expect(screenshot.length).toBeGreaterThan(64 * 1024); + + // must be a valid png image that matches the screen size + const image = new PNG(screenshot); + const pngSize = image.getDimensions(); + expect(pngSize.width).toBe(screenSize.width); + expect(pngSize.height).toBe(screenSize.height); + }); + + test("should be able to list apps", async () => { + test.skip(!hasOneAndroidDevice, "requires exactly one android device"); + const apps = await android.listApps(); + const packages = apps.map(app => app.packageName); + expect(packages).toContain("com.android.settings"); + }); + + test("should be able to open a url", async () => { + test.skip(!hasOneAndroidDevice, "requires exactly one android device"); + await android.adb("shell", "input", "keyevent", "HOME"); + await android.openUrl("https://www.example.com"); + }); + + test("should be able to list elements on screen", async () => { + test.skip(!hasOneAndroidDevice, "requires exactly one android device"); + await android.terminateApp("com.android.chrome"); + await android.adb("shell", "input", "keyevent", "HOME"); + await android.openUrl("https://www.example.com"); + const elements = await android.getElementsOnScreen(); + + // make sure title (TextView) is present + const foundTitle = elements.find(element => element.type === "android.widget.TextView" && element.text?.startsWith("This domain is for use in illustrative examples in documents")); + expect(foundTitle, "Title element not found").toBeTruthy(); + + // make sure navbar (EditText) is present + const foundNavbar = elements.find(element => element.type === "android.widget.EditText" && element.label === "Search or type URL" && element.text === "example.com"); + expect(foundNavbar, "Navbar element not found").toBeTruthy(); + + // this is an icon, but has accessibility label + const foundSecureIcon = elements.find(element => element.type === "android.widget.ImageButton" && element.text === "" && element.label === "New tab"); + expect(foundSecureIcon, "New tab icon not found").toBeTruthy(); + }); + + test("should be able to send keys and tap", async () => { + test.skip(!hasOneAndroidDevice, "requires exactly one android device"); + await android.terminateApp("com.google.android.deskclock"); + await android.adb("shell", "pm", "clear", "com.google.android.deskclock"); + await android.launchApp("com.google.android.deskclock"); + + // We probably start at Clock tab + await new Promise(resolve => setTimeout(resolve, 3000)); + let elements = await android.getElementsOnScreen(); + const timerElement = elements.find(e => e.label === "Timer" && e.type === "android.widget.FrameLayout"); + expect(timerElement).toBeDefined(); + await android.tap(timerElement.rect.x, timerElement.rect.y); + + // now we're in Timer tab + await new Promise(resolve => setTimeout(resolve, 3000)); + elements = await android.getElementsOnScreen(); + const currentTime = elements.find(e => e.text === "00h 00m 00s"); + expect(currentTime, "Expected time to be 00h 00m 00s").toBeDefined(); + await android.sendKeys("123456"); + + // now the title has changed with new timer + await new Promise(resolve => setTimeout(resolve, 3000)); + elements = await android.getElementsOnScreen(); + const newTime = elements.find(e => e.text === "12h 34m 56s"); + expect(newTime, "Expected time to be 12h 34m 56s").toBeDefined(); + + await android.terminateApp("com.google.android.deskclock"); + }); + + test("should be able to launch and terminate an app", async () => { + test.skip(!hasOneAndroidDevice, "requires exactly one android device"); + + // kill if running + await android.terminateApp("com.android.chrome"); + + await android.launchApp("com.android.chrome"); + await new Promise(resolve => setTimeout(resolve, 3000)); + const processes = await android.listRunningProcesses(); + expect(processes).toContain("com.android.chrome"); + + await android.terminateApp("com.android.chrome"); + const processes2 = await android.listRunningProcesses(); + expect(processes2).not.toContain("com.android.chrome"); + }); + + test("should handle orientation changes", async () => { + test.skip(!hasOneAndroidDevice, "requires exactly one android device"); + + // assume we start in portrait + const originalOrientation = await android.getOrientation(); + expect(originalOrientation).toBe("portrait"); + const screenSize1 = await android.getScreenSize(); + + // set to landscape + await android.setOrientation("landscape"); + await new Promise(resolve => setTimeout(resolve, 1500)); + const orientation = await android.getOrientation(); + expect(orientation).toBe("landscape"); + const screenSize2 = await android.getScreenSize(); + + // set to portrait + await android.setOrientation("portrait"); + await new Promise(resolve => setTimeout(resolve, 1500)); + const orientation2 = await android.getOrientation(); + expect(orientation2).toBe("portrait"); + + // screen size should not have changed + expect(screenSize1).toEqual(screenSize2); + }); +}); diff --git a/packages/mobile-mcp/test/coord-norm.test.ts b/packages/mobile-mcp/test/coord-norm.test.ts new file mode 100644 index 0000000000..6a030d15fd --- /dev/null +++ b/packages/mobile-mcp/test/coord-norm.test.ts @@ -0,0 +1,347 @@ +import { test, expect } from '@playwright/test'; + +// We need to test coord-norm functions. Since the module uses process.env, +// we manipulate env vars in each test. + +// Dynamic import to allow env var manipulation before module evaluation. +async function loadCoordNorm(env: Record = {}) { + const origEnv: Record = {}; + for (const [k, v] of Object.entries(env)) { + origEnv[k] = process.env[k]; + if (v === undefined) { + delete process.env[k]; + } else { + process.env[k] = v; + } + } + + // Force re-import by clearing require cache + const modulePath = require.resolve('../src/coord-norm'); + delete require.cache[modulePath]; + const mod = require('../src/coord-norm'); + + return { + mod, + restore: () => { + for (const [k, v] of Object.entries(origEnv)) { + if (v === undefined) { + delete process.env[k]; + } else { + process.env[k] = v; + } + } + delete require.cache[modulePath]; + }, + }; +} + +// ── Scalar conversion tests ── + +test('normToPx maps midpoint', async () => { + const { mod, restore } = await loadCoordNorm(); + try { + expect(mod.normToPx(500, 800, 1000)).toBe(400); + } finally { + restore(); + } +}); + +test('normToPx maps edges', async () => { + const { mod, restore } = await loadCoordNorm(); + try { + expect(mod.normToPx(0, 800, 1000)).toBe(0); + expect(mod.normToPx(1000, 800, 1000)).toBe(800); + } finally { + restore(); + } +}); + +test('normToPx rounds to nearest', async () => { + const { mod, restore } = await loadCoordNorm(); + try { + // 333/1000 * 800 = 266.4 → 266 + expect(mod.normToPx(333, 800, 1000)).toBe(266); + } finally { + restore(); + } +}); + +test('pxToNorm is inverse at midpoint', async () => { + const { mod, restore } = await loadCoordNorm(); + try { + expect(mod.pxToNorm(400, 800, 1000)).toBe(500); + expect(mod.pxToNorm(800, 800, 1000)).toBe(1000); + } finally { + restore(); + } +}); + +test('normToPx respects custom scale (999)', async () => { + const { mod, restore } = await loadCoordNorm(); + try { + expect(mod.normToPx(999, 800, 999)).toBe(800); + // Same input under scale 1000: 999/1000*800 = 799.2 → 799 + expect(mod.normToPx(999, 800, 1000)).toBe(799); + } finally { + restore(); + } +}); + +test('pxToNorm handles zero dim', async () => { + const { mod, restore } = await loadCoordNorm(); + try { + expect(mod.pxToNorm(400, 0, 1000)).toBe(0); + } finally { + restore(); + } +}); + +// ── denormalizeArgs tests ── + +test('denormalizeArgs click uses width for x, height for y', async () => { + const { mod, restore } = await loadCoordNorm(); + try { + const args: any = { device: 'd1', x: 500, y: 500 }; + mod.denormalizeArgs( + 'mobile_click_on_screen_at_coordinates', + args, + 800, + 600, + ); + expect(args.x).toBe(400); // 500/1000 * 800 + expect(args.y).toBe(300); // 500/1000 * 600 + } finally { + restore(); + } +}); + +test('denormalizeArgs swipe converts x, y, and distance by direction', async () => { + const { mod, restore } = await loadCoordNorm(); + try { + const argsDown: any = { + device: 'd1', + x: 500, + y: 500, + distance: 200, + direction: 'down', + }; + mod.denormalizeArgs('mobile_swipe_on_screen', argsDown, 800, 600); + expect(argsDown.x).toBe(400); + expect(argsDown.y).toBe(300); + expect(argsDown.distance).toBe(120); // 200/1000 * 600 (height for down) + + const argsRight: any = { + device: 'd1', + x: 500, + y: 500, + distance: 200, + direction: 'right', + }; + mod.denormalizeArgs('mobile_swipe_on_screen', argsRight, 800, 600); + expect(argsRight.distance).toBe(160); // 200/1000 * 800 (width for right) + } finally { + restore(); + } +}); + +test('denormalizeArgs leaves non-coord tools untouched', async () => { + const { mod, restore } = await loadCoordNorm(); + try { + const args: any = { device: 'd1', direction: 'down' }; + mod.denormalizeArgs('mobile_press_button', args, 800, 600); + expect(args).toEqual({ device: 'd1', direction: 'down' }); + } finally { + restore(); + } +}); + +test('denormalizeArgs ignores missing coord fields', async () => { + const { mod, restore } = await loadCoordNorm(); + try { + const args: any = { device: 'd1' }; + mod.denormalizeArgs( + 'mobile_click_on_screen_at_coordinates', + args, + 800, + 600, + ); + expect(args.x).toBeUndefined(); + expect(args.y).toBeUndefined(); + } finally { + restore(); + } +}); + +// ── Output normalization tests ── + +test('normalizeElementResult rewrites element coordinates', async () => { + const { mod, restore } = await loadCoordNorm(); + try { + const elements = [ + { + type: 'button', + coordinates: { x: 400, y: 300, width: 80, height: 60 }, + }, + ]; + const input = 'Found these elements on screen: ' + JSON.stringify(elements); + const result = mod.normalizeElementResult( + 'mobile_list_elements_on_screen', + input, + 800, + 600, + ); + const parsed = JSON.parse( + result.replace('Found these elements on screen: ', ''), + ); + expect(parsed[0].coordinates.x).toBe(500); // 400/800*1000 + expect(parsed[0].coordinates.y).toBe(500); // 300/600*1000 + expect(parsed[0].coordinates.width).toBe(100); // 80/800*1000 + expect(parsed[0].coordinates.height).toBe(100); // 60/600*1000 + } finally { + restore(); + } +}); + +test('normalizeScreenSizeResult rewrites to scale', async () => { + const { mod, restore } = await loadCoordNorm(); + try { + const result = mod.normalizeScreenSizeResult( + 'mobile_get_screen_size', + 'Screen size is 1080x2400 pixels', + ); + expect(result).toContain('1000x1000'); + } finally { + restore(); + } +}); + +test('normalizeScreenSizeResult leaves non-screen-size tools alone', async () => { + const { mod, restore } = await loadCoordNorm(); + try { + const result = mod.normalizeScreenSizeResult( + 'mobile_tap', + 'Tapped at 100, 200', + ); + expect(result).toBe('Tapped at 100, 200'); + } finally { + restore(); + } +}); + +// ── Screen size cache tests ── + +test('screen size cache round-trip', async () => { + const { mod, restore } = await loadCoordNorm(); + try { + mod.cacheScreenSize('test-device', 1080, 2400); + const size = mod.getCachedScreenSize('test-device'); + expect(size).toEqual({ width: 1080, height: 2400 }); + } finally { + restore(); + } +}); + +test('screen size cache returns undefined for unknown device', async () => { + const { mod, restore } = await loadCoordNorm(); + try { + expect(mod.getCachedScreenSize('nonexistent')).toBeUndefined(); + } finally { + restore(); + } +}); + +test('ingestScreenSizeFromResult parses response', async () => { + const { mod, restore } = await loadCoordNorm(); + try { + mod.ingestScreenSizeFromResult('dev1', 'Screen size is 1080x2400 pixels'); + const size = mod.getCachedScreenSize('dev1'); + expect(size).toEqual({ width: 1080, height: 2400 }); + } finally { + restore(); + } +}); + +// ── Config tests ── + +test('isNormalized returns false by default', async () => { + const { mod, restore } = await loadCoordNorm({ + MOBILE_MCP_COORDINATE_SPACE: undefined, + }); + try { + expect(mod.isNormalized()).toBe(false); + } finally { + restore(); + } +}); + +test('isNormalized returns true when MOBILE_MCP_COORDINATE_SPACE=1', async () => { + const { mod, restore } = await loadCoordNorm({ + MOBILE_MCP_COORDINATE_SPACE: '1', + }); + try { + expect(mod.isNormalized()).toBe(true); + } finally { + restore(); + } +}); + +test('coordinateScale defaults to 1000', async () => { + const { mod, restore } = await loadCoordNorm({ + MOBILE_MCP_COORDINATE_SCALE: undefined, + }); + try { + expect(mod.coordinateScale()).toBe(1000); + } finally { + restore(); + } +}); + +test('coordinateScale reads custom value', async () => { + const { mod, restore } = await loadCoordNorm({ + MOBILE_MCP_COORDINATE_SCALE: '999', + }); + try { + expect(mod.coordinateScale()).toBe(999); + } finally { + restore(); + } +}); + +// ── Description rewriting tests ── + +test("rewriteDescription replaces 'in pixels' with normalized wording", async () => { + const { mod, restore } = await loadCoordNorm(); + try { + const result = mod.rewriteDescription( + 'Click on the screen at x,y in pixels', + ); + expect(result).toContain('0-1000 normalized coordinates'); + expect(result).not.toContain('in pixels'); + } finally { + restore(); + } +}); + +test('coordParamDesc is passthrough when not normalized', async () => { + const { mod, restore } = await loadCoordNorm({ + MOBILE_MCP_COORDINATE_SPACE: '0', + }); + try { + const desc = 'The x coordinate, in pixels'; + expect(mod.coordParamDesc(desc)).toBe(desc); + } finally { + restore(); + } +}); + +test('coordParamDesc rewrites when normalized', async () => { + const { mod, restore } = await loadCoordNorm({ + MOBILE_MCP_COORDINATE_SPACE: '1', + }); + try { + const result = mod.coordParamDesc('The x coordinate, in pixels'); + expect(result).toContain('0-1000 normalized'); + } finally { + restore(); + } +}); diff --git a/packages/mobile-mcp/test/ios.ts b/packages/mobile-mcp/test/ios.ts new file mode 100644 index 0000000000..afb50cd799 --- /dev/null +++ b/packages/mobile-mcp/test/ios.ts @@ -0,0 +1,33 @@ +import { test, expect } from "@playwright/test"; + +import { IosManager, IosRobot } from "../src/ios"; +import { PNG } from "../src/png"; + +test.describe("ios", () => { + + let robot: IosRobot; + let hasOneDevice = false; + + test.beforeAll(async () => { + const manager = new IosManager(); + const devices = await manager.listDevices(); + hasOneDevice = devices.length === 1; + robot = new IosRobot(devices?.[0]?.deviceId || ""); + }); + + test("should be able to get screenshot", async () => { + test.skip(!hasOneDevice, "requires exactly one ios device"); + const screenshot = await robot.getScreenshot(); + // an black screenshot (screen is off) still consumes over 30KB + expect(screenshot.length).toBeGreaterThan(128 * 1024); + + // must be a valid png image that matches the screen size + const image = new PNG(screenshot); + const pngSize = image.getDimensions(); + const screenSize = await robot.getScreenSize(); + + // wda returns screen size as points, round up + expect(Math.ceil(pngSize.width / screenSize.scale)).toBe(screenSize.width); + expect(Math.ceil(pngSize.height / screenSize.scale)).toBe(screenSize.height); + }); +}); diff --git a/packages/mobile-mcp/test/iphone-simulator.ts b/packages/mobile-mcp/test/iphone-simulator.ts new file mode 100644 index 0000000000..37644ad783 --- /dev/null +++ b/packages/mobile-mcp/test/iphone-simulator.ts @@ -0,0 +1,189 @@ +import { test, expect } from "@playwright/test"; +import { randomBytes } from "node:crypto"; + +import { PNG } from "../src/png"; +import { MobileDevice } from "../src/mobile-device"; +import { Mobilecli } from "../src/mobilecli"; + +test.describe("iphone-simulator", () => { + + const mobilecli = new Mobilecli(); + const devicesResponse = mobilecli.getDevices({ + platform: "ios", + type: "simulator", + includeOffline: false, + }); + + const bootedSimulators = devicesResponse.data.devices; + const hasOneSimulator = bootedSimulators.length >= 1; + const device = new MobileDevice(bootedSimulators?.[0]?.id || ""); + + const restartApp = async (app: string) => { + await device.launchApp(app); + await device.terminateApp(app); + await device.launchApp(app); + }; + + const restartPreferencesApp = async () => { + await restartApp("com.apple.Preferences"); + }; + + const restartRemindersApp = async () => { + await restartApp("com.apple.reminders"); + }; + + test("should be able to swipe", async () => { + test.skip(!hasOneSimulator, "requires a booted ios simulator"); + await restartPreferencesApp(); + + // make sure "General" is present (since it's at the top of the list) + const elements1 = await device.getElementsOnScreen(); + expect(elements1.findIndex(e => e.name === "com.apple.settings.general")).not.toBe(-1); + + // swipe up (bottom of screen to top of screen) + await device.swipe("up"); + + // make sure "General" is not visible now + const elements2 = await device.getElementsOnScreen(); + expect(elements2.findIndex(e => e.name === "com.apple.settings.general")).toBe(-1); + + // swipe down + await device.swipe("down"); + + // make sure "General" is visible again + const elements3 = await device.getElementsOnScreen(); + expect(elements3.findIndex(e => e.name === "com.apple.settings.general")).not.toBe(-1); + }); + + test("should be able to send keys and press enter", async () => { + test.skip(!hasOneSimulator, "requires a booted ios simulator"); + await restartRemindersApp(); + + // find new reminder element + await new Promise(resolve => setTimeout(resolve, 3000)); + const elements = await device.getElementsOnScreen(); + const newElement = elements.find(e => e.label === "New Reminder"); + expect(newElement, "should have found New Reminder element").toBeDefined(); + + // click on new reminder + await device.tap(newElement.rect.x, newElement.rect.y); + + // wait for keyboard to appear + await new Promise(resolve => setTimeout(resolve, 1000)); + + // send keys with press button "Enter" + const random1 = randomBytes(8).toString("hex"); + await device.sendKeys(random1); + await device.pressButton("ENTER"); + + // send keys with "\n" + const random2 = randomBytes(8).toString("hex"); + await device.sendKeys(random2 + "\n"); + + const elements2 = await device.getElementsOnScreen(); + expect(elements2.findIndex(e => e.value === random1)).not.toBe(-1); + expect(elements2.findIndex(e => e.value === random2)).not.toBe(-1); + }); + + test("should be able to get the screen size", async () => { + test.skip(!hasOneSimulator, "requires a booted ios simulator"); + const screenSize = await device.getScreenSize(); + expect(screenSize.width).toBeGreaterThan(256); + expect(screenSize.height).toBeGreaterThan(256); + expect(screenSize.scale).toBeGreaterThanOrEqual(1); + expect(Object.keys(screenSize).length, "screenSize should have exactly 3 properties").toBe(3); + }); + + test("should be able to get screenshot", async () => { + test.skip(!hasOneSimulator, "requires a booted ios simulator"); + const screenshot = await device.getScreenshot(); + expect(screenshot.length).toBeGreaterThan(64 * 1024); + + // must be a valid png image that matches the screen size + const image = new PNG(screenshot); + const pngSize = image.getDimensions(); + const screenSize = await device.getScreenSize(); + + // wda returns screen size as points, round up + expect(Math.ceil(pngSize.width / screenSize.scale)).toBe(screenSize.width); + expect(Math.ceil(pngSize.height / screenSize.scale)).toBe(screenSize.height); + }); + + test("should be able to open url", async () => { + test.skip(!hasOneSimulator, "requires a booted ios simulator"); + // simply checking thato openurl with https:// launches safari + await device.openUrl("https://www.example.com"); + await new Promise(resolve => setTimeout(resolve, 1000)); + + const elements = await device.getElementsOnScreen(); + expect(elements.length).toBeGreaterThan(0); + + const addressBar = elements.find(element => element.type === "TextField" && element.name === "TabBarItemTitle" && element.label === "Address"); + expect(addressBar, "should have address bar").toBeDefined(); + }); + + test("should be able to list apps", async () => { + test.skip(!hasOneSimulator, "requires a booted ios simulator"); + const apps = await device.listApps(); + const packages = apps.map(app => app.packageName); + expect(packages).toContain("com.apple.mobilesafari"); + expect(packages).toContain("com.apple.reminders"); + expect(packages).toContain("com.apple.Preferences"); + }); + + test("should be able to get elements on screen", async () => { + test.skip(!hasOneSimulator, "requires a booted ios simulator"); + await device.pressButton("HOME"); + await new Promise(resolve => setTimeout(resolve, 2000)); + + const elements = await device.getElementsOnScreen(); + expect(elements.length).toBeGreaterThan(0); + + // must have News app in home screen + const element = elements.find(e => e.type === "Icon" && e.label === "News"); + expect(element, "should have News app in home screen").toBeDefined(); + }); + + test("should be able to launch and terminate app", async () => { + test.skip(!hasOneSimulator, "requires a booted ios simulator"); + await restartPreferencesApp(); + await new Promise(resolve => setTimeout(resolve, 2000)); + const elements = await device.getElementsOnScreen(); + + const buttons = elements.filter(e => e.type === "Button").map(e => e.label); + expect(buttons).toContain("General"); + expect(buttons).toContain("Accessibility"); + + // make sure app is terminated + await device.terminateApp("com.apple.Preferences"); + const elements2 = await device.getElementsOnScreen(); + const buttons2 = elements2.filter(e => e.type === "Button").map(e => e.label); + expect(buttons2).not.toContain("General"); + }); + + /* + test("should be able to get and set orientation", async () => { + test.skip(!hasOneSimulator, "requires a booted ios simulator"); + + // Set to portrait and verify + await device.setOrientation("portrait"); + const portrait = await device.getOrientation(); + expect(portrait).toBe("portrait"); + + // Set to landscape and verify + await device.setOrientation("landscape"); + const landscape = await device.getOrientation(); + expect(landscape).toBe("landscape"); + + // Return to portrait + await device.setOrientation("portrait"); + const portraitAgain = await device.getOrientation(); + expect(portraitAgain).toBe("portrait"); + }); + */ + + test("should throw an error if button is not supported", async () => { + test.skip(!hasOneSimulator, "requires a booted ios simulator"); + await expect(device.pressButton("NOT_A_BUTTON" as any)).rejects.toThrow("unsupported button: NOT_A_BUTTON"); + }); +}); diff --git a/packages/mobile-mcp/test/mobilecli.test.ts b/packages/mobile-mcp/test/mobilecli.test.ts new file mode 100644 index 0000000000..453c22f67c --- /dev/null +++ b/packages/mobile-mcp/test/mobilecli.test.ts @@ -0,0 +1,119 @@ +import { test, expect } from "@playwright/test"; +import { Mobilecli } from "../src/mobilecli"; + +type ExecuteCommandCall = { + args: string[]; +}; + +function createMockMobilecli(mockResponse: string): { mobilecli: Mobilecli; calls: ExecuteCommandCall[] } { + const mobilecli = new Mobilecli(); + const calls: ExecuteCommandCall[] = []; + + mobilecli.executeCommand = function(args: string[]): string { + calls.push({ args }); + return mockResponse; + }; + + return { mobilecli, calls }; +} + +test.describe("mobilecli", () => { + + const mobilecli = new Mobilecli(); + + test.describe("getVersion", () => { + test("should return a version string", () => { + const version = mobilecli.getVersion(); + expect(version.length).toBeGreaterThan(0); + expect(version).not.toContain("failed"); + }); + + test("should return version in correct format", () => { + const version = mobilecli.getVersion(); + // Version should be in format like "0.0.45" or similar + const versionPattern = /^\d+\.\d+\.\d+/; + expect(version, `Version "${version}" should match pattern X.Y.Z`).toMatch(versionPattern); + }); + + test("should return failed when MOBILECLI_PATH points to invalid location", () => { + try { + process.env.MOBILECLI_PATH = "/tmp"; + const mobilecli = new Mobilecli(); + const version = mobilecli.getVersion(); + expect(version, `Expected version to include "failed" but got: ${version}`).toContain("failed"); + } finally { + delete process.env.MOBILECLI_PATH; + } + }); + + test("should call executeCommand with --version argument", () => { + const { mobilecli, calls } = createMockMobilecli("mobilecli version 1.0.0"); + const version = mobilecli.getVersion(); + + expect(calls.length).toBe(1); + expect(calls[0].args).toEqual(["--version"]); + expect(version).toBe("1.0.0"); + }); + }); + + test.describe("getDevices", () => { + const mockDevicesResponse = JSON.stringify({ + status: "ok", + data: { + devices: [ + { + id: "device1", + name: "Test Device", + platform: "ios", + type: "simulator", + version: "17.0" + } + ] + } + }); + + test("should call executeCommand with devices argument when no options", () => { + const { mobilecli, calls } = createMockMobilecli(mockDevicesResponse); + mobilecli.getDevices(); + + expect(calls.length).toBe(1); + expect(calls[0].args).toEqual(["devices"]); + }); + + test("should call executeCommand with platform filter", () => { + const { mobilecli, calls } = createMockMobilecli(mockDevicesResponse); + mobilecli.getDevices({ platform: "ios" }); + + expect(calls.length).toBe(1); + expect(calls[0].args).toEqual(["devices", "--platform", "ios"]); + }); + + test("should call executeCommand with type filter", () => { + const { mobilecli, calls } = createMockMobilecli(mockDevicesResponse); + mobilecli.getDevices({ type: "simulator" }); + + expect(calls.length).toBe(1); + expect(calls[0].args).toEqual(["devices", "--type", "simulator"]); + }); + + test("should call executeCommand with includeOffline flag", () => { + const { mobilecli, calls } = createMockMobilecli(mockDevicesResponse); + mobilecli.getDevices({ includeOffline: true }); + + expect(calls.length).toBe(1); + expect(calls[0].args).toEqual(["devices", "--include-offline"]); + }); + + test("should call executeCommand with combined options", () => { + const { mobilecli, calls } = createMockMobilecli(mockDevicesResponse); + mobilecli.getDevices({ + platform: "android", + type: "emulator", + includeOffline: true + }); + + expect(calls.length).toBe(1); + expect(calls[0].args).toEqual(["devices", "--include-offline", "--platform", "android", "--type", "emulator"]); + }); + }); +}); diff --git a/packages/mobile-mcp/test/png.ts b/packages/mobile-mcp/test/png.ts new file mode 100644 index 0000000000..46434381cb --- /dev/null +++ b/packages/mobile-mcp/test/png.ts @@ -0,0 +1,18 @@ +import { test, expect } from "@playwright/test"; +import { PNG } from "../src/png"; + + +test.describe("png", () => { + test("should be able to parse png", () => { + const buffer = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGNgYAAAAAMAAWgmWQ0AAAAASUVORK5CYII="; + const png = new PNG(Buffer.from(buffer, "base64")); + expect(png.getDimensions().width).toBe(1); + expect(png.getDimensions().height).toBe(1); + }); + + test("should be able to detect an invalid png", () => { + const buffer = btoa("IAMADUCKIAMADUCKIAMADUCKIAMADUCKIAMADUCK"); + const png = new PNG(Buffer.from(buffer, "base64")); + expect(() => png.getDimensions()).toThrow(); + }); +}); diff --git a/packages/mobile-mcp/tsconfig.json b/packages/mobile-mcp/tsconfig.json new file mode 100644 index 0000000000..04116e1994 --- /dev/null +++ b/packages/mobile-mcp/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ESNext", + "skipLibCheck": true, + "esModuleInterop": true, + "moduleResolution": "node", + "strict": true, + "module": "CommonJS", + "outDir": "./lib" + }, + "include": [ + "src", + ], +} \ No newline at end of file