diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 0000000..2be13d4 --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json", + "changelog": "@changesets/cli/changelog", + "commit": false, + "fixed": [], + "linked": [], + "access": "public", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "ignore": [] +} diff --git a/.changeset/initial-release.md b/.changeset/initial-release.md new file mode 100644 index 0000000..7cbb20d --- /dev/null +++ b/.changeset/initial-release.md @@ -0,0 +1,5 @@ +--- +"@nebula-agents/electron-mcp": minor +--- + +Initial prerelease of the embedded Electron MCP server. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..3cf03ff --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,24 @@ +name: Bug Report +description: Report a reproducible problem. +title: "bug: " +labels: ["bug"] +body: + - type: textarea + id: summary + attributes: + label: Summary + validations: + required: true + - type: textarea + id: repro + attributes: + label: Reproduction + description: Include a minimal Electron setup or failing test when possible. + validations: + required: true + - type: input + id: version + attributes: + label: Version + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..b459578 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,17 @@ +name: Feature Request +description: Propose a focused change. +title: "feat: " +labels: ["enhancement"] +body: + - type: textarea + id: problem + attributes: + label: Problem + validations: + required: true + - type: textarea + id: proposal + attributes: + label: Proposal + validations: + required: true diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..484c80b --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,6 @@ +## Summary + +## Verification + +- [ ] `pnpm check` +- [ ] `pnpm test:electron` if Electron/CDP behavior changed diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..2aed686 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,32 @@ +name: CI + +on: + pull_request: + push: + branches: [main] + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 + with: + node-version: 24 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm check + + electron-smoke: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 + with: + node-version: 24 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm exec playwright install --with-deps chromium + - run: xvfb-run -a pnpm test:electron diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..3880de5 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,31 @@ +name: Release + +on: + push: + branches: [main] + +concurrency: ${{ github.workflow }}-${{ github.ref }} + +jobs: + release: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + id-token: write + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 + with: + node-version: 24 + cache: pnpm + registry-url: https://registry.npmjs.org + - run: pnpm install --frozen-lockfile + - run: pnpm check + - uses: changesets/action@ce079ea084e08a340947ed4d6ecedb2433c8f293 # v1 + with: + publish: pnpm changeset publish + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore index 3e91128..38f8d92 100644 --- a/.gitignore +++ b/.gitignore @@ -29,7 +29,6 @@ pnpm-debug.log* coverage/ .nyc_output/ test-results/ -playwright-report/ # Misc .cache/ diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..8a1a330 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,6 @@ +# Code of Conduct + +This project follows the Contributor Covenant Code of Conduct, version 2.1: +https://www.contributor-covenant.org/version/2/1/code_of_conduct/ + +Report conduct concerns to `security@agent-labs.dev`. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..0147073 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,39 @@ +# Contributing + +Thanks for considering a contribution. This package is maintained best-effort; +small, focused PRs are easiest to review. + +## Setup + +```sh +pnpm install +pnpm check +``` + +## Development + +- Source lives in `src/`. +- Unit tests are colocated as `*.test.ts` and are the only files vitest + picks up (see `vitest.config.ts`). +- The canonical Electron smoke test is `test/electron/smoke.spec.ts`, + driven by `pnpm test:electron`. CI runs this on Linux. +- `tests/smoke.electron.test.ts` is an alternate exploratory fixture + that talks to a CJS Electron main via stdout and is **not** wired + into vitest, playwright, or CI; it's kept for local experimentation + only and may be removed in a follow-up. +- Run `pnpm test` for unit tests. +- Run `pnpm test:electron` for the Electron smoke test. + +## Changesets + +User-visible changes need a changeset: + +```sh +pnpm changeset +``` + +Pick `patch`, `minor`, or `major` according to semver. For `0.x`, breaking API +changes generally use `minor`. + +Releases are handled by the Changesets release workflow. Do not publish from a +local machine unless a maintainer explicitly coordinates it. diff --git a/README.md b/README.md index 480c5aa..bc32d8e 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,139 @@ # `@nebula-agents/electron-mcp` -> Embedded MCP (Model Context Protocol) server for Electron apps — drive your renderers via Chrome DevTools Protocol. +Embedded MCP server for Electron apps. It runs inside your Electron main +process, exposes the `BrowserWindow` surfaces you choose, and lets MCP clients +drive those renderers through Chrome DevTools Protocol. -**Status:** 🚧 Pre-release. The implementation lives in [`agent-labs-dev/nebula-desktop`](https://github.com/agent-labs-dev/nebula-desktop) and is being extracted here. See [PRD #149](https://github.com/agent-labs-dev/nebula-desktop/issues/149) for the migration plan. +## Embedded vs External Attach -## What this is +This package is for apps that want MCP automation as an opt-in feature of the +app itself. Your main process decides when the server starts, which windows are +reachable, and which custom tools are registered. -A library for Electron app developers who want **first-class MCP integration as an opt-in feature of their own app**. Unlike external-attach approaches (which speak to Electron over its remote debugging port), this package runs the MCP server _inside_ your main process, giving you: +External-attach servers connect to an already-running Electron app through a +remote debugging port. That can be useful for local debugging, but it does not +let the app own surface naming, gating, or app-specific tools. If you want that +external model, this package is probably not the right fit. -- Full control over which surfaces (windows) are exposed -- Custom tool registration via `addTool()` — expose your app's domain commands alongside the bundled Electron primitives (screenshot, click, type, eval, DOM/AX queries, …) -- Explicit `start()` / `stop()` lifecycle so you decide when the server runs - -## Planned API +## Quickstart ```ts -import { createElectronMcpServer } from '@nebula-agents/electron-mcp'; +import { app, BrowserWindow } from "electron"; +import { createElectronMcpServer } from "@nebula-agents/electron-mcp"; + +let mainWindow: BrowserWindow | null = null; +const mcp = createElectronMcpServer({ + getSurfaces: () => ({ main: mainWindow }), +}); + +app.whenReady().then(async () => { + mainWindow = new BrowserWindow(); + + if (recommendedGuards()) { + await mcp.start(); + console.log(`MCP listening at ${mcp.url}`); + } +}); + +app.on("before-quit", () => { + void mcp.stop(); +}); + +function recommendedGuards(): boolean { + return !app.isPackaged && process.env.MY_APP_MCP === "1"; +} +``` + +Connect an MCP client to the logged HTTP URL. The default is +`http://127.0.0.1:9229/mcp`. + +## API + +```ts const mcp = createElectronMcpServer({ getSurfaces: () => ({ main: mainWindow, settings: settingsWindow }), + port: 9229, + host: "127.0.0.1", + path: "/mcp", + instructions: "Optional client-facing instructions.", }); +``` -mcp.addTool(myCustomTool); +`createElectronMcpServer(config)` returns a synchronous handle: -if (!app.isPackaged && process.env.MY_APP_MCP === '1') { - await mcp.start(); +- `addTool(toolDef)` registers a custom tool. Call it before `start()`. +- `start()` starts the loopback HTTP server. +- `stop()` stops the HTTP server and detaches CDP sessions. +- `isRunning` reports whether the server is active. +- `url` is the bound MCP endpoint once running. + +Bundled tools: + +- `list_surfaces` +- `show_surface` +- `hide_surface` +- `focus_surface` +- `reload_surface` +- `screenshot` +- `evaluate` +- `click` +- `type_text` +- `press_key` +- `hover` +- `query_dom` +- `ax_snapshot` +- `fill_form` +- `wait_for_load` + +Public types are available from the root export and from +`@nebula-agents/electron-mcp/types`. + +## Custom Tools + +```ts +import { z } from "zod"; +import type { ToolDef } from "@nebula-agents/electron-mcp/types"; + +const resetStateTool: ToolDef = { + name: "reset_state", + config: { + title: "Reset State", + description: "Reset the app's local demo state.", + inputSchema: { profile: z.enum(["empty", "demo"]) }, + }, + handler: async ({ profile }) => { + await resetLocalState(profile); + return { content: [{ type: "text", text: "ok" }] }; + }, +}; + +mcp.addTool(resetStateTool); +``` + +Tools must be registered before `start()`. Dynamic tool registration and +`tools/list_changed` are intentionally out of scope for `0.x`. + +## Security Model + +The server binds to loopback only by default: `127.0.0.1`. Attempts to bind a +non-loopback host throw. There is no authentication layer in `0.1.0`; if your +threat model requires more than loopback isolation, wrap this package in your +own gate or do not start it. + +Recommended production guard: + +```ts +function recommendedGuards(): boolean { + return !app.isPackaged && process.env.MY_APP_MCP === "1"; } ``` +## Maintenance + +This is open code with no support SLA. Agent Labs reviews issues and PRs on a +best-effort basis. `0.x` versions may change API shape based on real usage. + ## License -MIT — see [`LICENSE`](./LICENSE). +MIT. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..887def0 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,25 @@ +# Security + +## Supported Versions + +Only the latest published `0.x` version receives security consideration. + +## Reporting a Vulnerability + +Do not open a public issue for a vulnerability. Email `security@agent-labs.dev` +with: + +- affected version or commit +- reproduction steps +- impact +- any known mitigations + +We review reports best-effort and will coordinate disclosure when a fix is +available. + +## Model + +The MCP server binds to loopback by default and rejects non-loopback hosts. It +does not implement authentication in `0.1.0`. Host apps are responsible for +their own start gates and must not start the server in production unless they +have intentionally accepted that risk. diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..24b4725 --- /dev/null +++ b/biome.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.4.13/schema.json", + "files": { + "includes": [ + "**", + "!dist", + "!node_modules", + "!test-results", + "!test/electron/fixture/dist" + ] + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2 + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "assist": { + "actions": { + "source": { + "organizeImports": "on" + } + } + } +} diff --git a/docs/repo-setup.md b/docs/repo-setup.md new file mode 100644 index 0000000..bb6e0c0 --- /dev/null +++ b/docs/repo-setup.md @@ -0,0 +1,14 @@ +# Repository Setup + +Use this checklist for `agent-labs-dev/electron-mcp` and future Agent Labs OSS +repos with similar release mechanics. + +- Require 2FA for the `agent-labs-dev` organization. +- Enable Dependabot alerts and security updates. +- Enable secret scanning. +- Protect `main`. +- Require CI checks before merge. +- Require at least one approving review. +- Add `NPM_TOKEN` as an Actions secret for the release workflow. +- Install the Changesets GitHub app if release PRs are not created. +- Confirm package provenance settings before the first npm publish. diff --git a/package.json b/package.json index 473280f..761fd34 100644 --- a/package.json +++ b/package.json @@ -1,50 +1,79 @@ { "name": "@nebula-agents/electron-mcp", - "version": "0.1.0-pre", - "description": "Embedded Model Context Protocol server for Electron apps. Exposes screenshot/evaluate/list_surfaces and friends over loopback HTTP, driving real BrowserWindows via webContents.debugger (CDP).", + "version": "0.0.0", + "description": "Embedded MCP server for Electron apps.", "license": "MIT", + "author": "Agent Labs", + "homepage": "https://github.com/agent-labs-dev/electron-mcp#readme", "repository": { "type": "git", "url": "git+https://github.com/agent-labs-dev/electron-mcp.git" }, + "bugs": { + "url": "https://github.com/agent-labs-dev/electron-mcp/issues" + }, "type": "module", + "files": [ + "dist", + "README.md", + "LICENSE" + ], "exports": { ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" + "types": "./dist/index.d.mts", + "import": "./dist/index.mjs" }, "./types": { - "types": "./dist/types.d.ts", - "import": "./dist/types.js" + "types": "./dist/types.d.mts", + "import": "./dist/types.mjs" } }, - "files": [ - "dist", - "LICENSE", - "README.md" - ], + "main": "./dist/index.mjs", + "types": "./dist/index.d.mts", + "sideEffects": false, "scripts": { - "build": "tsc -p tsconfig.build.json", - "typecheck": "tsc --noEmit", - "pretest": "pnpm build", - "test": "playwright test", - "test:smoke": "pnpm build && playwright test tests/smoke.electron.test.ts" + "build": "tsdown", + "check": "pnpm run lint && pnpm run typecheck && pnpm run test && pnpm run build", + "format": "biome format --write .", + "lint": "biome check .", + "lint:fix": "biome check --write .", + "test": "vitest run", + "test:electron": "pnpm run build && playwright test -c test/electron/playwright.config.ts", + "typecheck": "tsc --noEmit" }, + "keywords": [ + "electron", + "mcp", + "model-context-protocol", + "cdp", + "automation" + ], "peerDependencies": { - "@modelcontextprotocol/sdk": ">=1.29.0", - "electron": ">=30", - "zod": "^4.3.0" - }, - "dependencies": { - "async-mutex": "^0.5.0" + "@modelcontextprotocol/sdk": ">=1.29.0 <2", + "electron": ">=41 <42", + "zod": ">=4 <5" }, "devDependencies": { + "@biomejs/biome": "^2.4.12", + "@changesets/cli": "^2.29.7", "@modelcontextprotocol/sdk": "^1.29.0", - "@playwright/test": "^1.49.0", - "@types/node": "^22.10.0", + "@playwright/test": "^1.57.0", + "@types/node": "^24.9.0", "electron": "^41.2.1", - "playwright": "^1.49.0", - "typescript": "^5.7.2", + "playwright": "^1.57.0", + "tsdown": "^0.21.9", + "typescript": "^6.0.3", + "vitest": "4.1.5", "zod": "^4.3.6" + }, + "dependencies": { + "async-mutex": "^0.5.0" + }, + "packageManager": "pnpm@10.19.0", + "engines": { + "node": ">=20" + }, + "publishConfig": { + "access": "public" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 731b19e..4dd01d6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,40 +12,222 @@ importers: specifier: ^0.5.0 version: 0.5.0 devDependencies: + '@biomejs/biome': + specifier: ^2.4.12 + version: 2.4.13 + '@changesets/cli': + specifier: ^2.29.7 + version: 2.31.0(@types/node@24.12.2) '@modelcontextprotocol/sdk': specifier: ^1.29.0 version: 1.29.0(zod@4.3.6) '@playwright/test': - specifier: ^1.49.0 + specifier: ^1.57.0 version: 1.59.1 '@types/node': - specifier: ^22.10.0 - version: 22.19.17 + specifier: ^24.9.0 + version: 24.12.2 electron: specifier: ^41.2.1 version: 41.3.0 playwright: - specifier: ^1.49.0 + specifier: ^1.57.0 version: 1.59.1 + tsdown: + specifier: ^0.21.9 + version: 0.21.10(typescript@6.0.3) typescript: - specifier: ^5.7.2 - version: 5.9.3 + specifier: ^6.0.3 + version: 6.0.3 + vitest: + specifier: 4.1.5 + version: 4.1.5(@types/node@24.12.2)(vite@8.0.10(@types/node@24.12.2)) zod: specifier: ^4.3.6 version: 4.3.6 packages: + '@babel/generator@8.0.0-rc.3': + resolution: {integrity: sha512-em37/13/nR320G4jab/nIIHZgc2Wz2y/D39lxnTyxB4/D/omPQncl/lSdlnJY1OhQcRGugTSIF2l/69o31C9dA==} + engines: {node: ^20.19.0 || >=22.12.0} + + '@babel/helper-string-parser@8.0.0-rc.3': + resolution: {integrity: sha512-AmwWFx1m8G/a5cXkxLxTiWl+YEoWuoFLUCwqMlNuWO1tqAYITQAbCRPUkyBHv1VOFgfjVOqEj6L3u15J5ZCzTA==} + engines: {node: ^20.19.0 || >=22.12.0} + + '@babel/helper-validator-identifier@8.0.0-rc.3': + resolution: {integrity: sha512-8AWCJ2VJJyDFlGBep5GpaaQ9AAaE/FjAcrqI7jyssYhtL7WGV0DOKpJsQqM037xDbpRLHXsY8TwU7zDma7coOw==} + engines: {node: ^20.19.0 || >=22.12.0} + + '@babel/parser@8.0.0-rc.3': + resolution: {integrity: sha512-B20dvP3MfNc/XS5KKCHy/oyWl5IA6Cn9YjXRdDlCjNmUFrjvLXMNUfQq/QUy9fnG2gYkKKcrto2YaF9B32ToOQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + '@babel/runtime@7.29.2': + resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} + engines: {node: '>=6.9.0'} + + '@babel/types@8.0.0-rc.3': + resolution: {integrity: sha512-mOm5ZrYmphGfqVWoH5YYMTITb3cDXsFgmvFlvkvWDMsR9X8RFnt7a0Wb6yNIdoFsiMO9WjYLq+U/FMtqIYAF8Q==} + engines: {node: ^20.19.0 || >=22.12.0} + + '@biomejs/biome@2.4.13': + resolution: {integrity: sha512-gLXOwkOBBg0tr7bDsqlkIh4uFeKuMjxvqsrb1Tukww1iDmHcfr4Uu8MoQxp0Rcte+69+osRNWXwHsu/zxT6XqA==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@2.4.13': + resolution: {integrity: sha512-2KImO1jhNFBa2oWConyr0x6flxbQpGKv6902uGXpYM62Xyem8U80j441SyUJ8KyngsmKbQjeIv1q2CQfDkNnYg==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@2.4.13': + resolution: {integrity: sha512-BKrJklbaFN4p1Ts4kPBczo+PkbsHQg57kmJ+vON9u2t6uN5okYHaSr7h/MutPCWQgg2lglaWoSmm+zhYW+oOkg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@2.4.13': + resolution: {integrity: sha512-U5MsuBQW25dXaYtqWWSPM3P96H6Y+fHuja3TQpMNnylocHW0tEbtFTDlUj6oM+YJLntvEkQy4grBvQNUD4+RCg==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-arm64@2.4.13': + resolution: {integrity: sha512-NzkUDSqfvMBrPplKgVr3aXLHZ2NEELvvF4vZxXulEylKWIGqlvNEcwUcj9OLrn75TD3lJ/GIqCVlBwd1MZCuYQ==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-x64-musl@2.4.13': + resolution: {integrity: sha512-Z601MienRgTBDza/+u2CH3RSrWoXo9rtr8NK6A4KJzqGgfxx+H3VlyLgTJ4sRo40T3pIsqpTmiOQEvYzQvBRvQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-linux-x64@2.4.13': + resolution: {integrity: sha512-Az3ZZedYRBo9EQzNnD9SxFcR1G5QsGo6VEc2hIyVPZ1rdKwee/7E9oeBBZFpE8Z44ekxsDQBqbiWGW5ShOhUSQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-win32-arm64@2.4.13': + resolution: {integrity: sha512-Px9PS2B5/Q183bUwy/5VHqp3J2lzdOCeVGzMpphYfl8oSa7VDCqenBdqWpy6DCy/en4Rbf/Y1RieZF6dJPcc9A==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@2.4.13': + resolution: {integrity: sha512-tTcMkXyBrmHi9BfrD2VNHs/5rYIUKETqsBlYOvSAABwBkJhSDVb5e7wPukftsQbO3WzQkXe6kaztC6WtUOXSoQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + + '@changesets/apply-release-plan@7.1.1': + resolution: {integrity: sha512-9qPCm/rLx/xoOFXIHGB229+4GOL76S4MC+7tyOuTsR6+1jYlfFDQORdvwR5hDA6y4FL2BPt3qpbcQIS+dW85LA==} + + '@changesets/assemble-release-plan@6.0.10': + resolution: {integrity: sha512-rSDcqdJ9KbVyjpBIuCidhvZNIiVt1XaIYp73ycVQRIA5n/j6wQaEk0ChRLMUQ1vkxZe51PTQ9OIhbg6HQMW45A==} + + '@changesets/changelog-git@0.2.1': + resolution: {integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==} + + '@changesets/cli@2.31.0': + resolution: {integrity: sha512-AhI4enNTgHu2IZr6K4WZyf0EPch4XVMn1yOMFmCD9gsfBGqMYaHXls5HyDv6/CL5axVQABz68eG30eCtbr2wFg==} + hasBin: true + + '@changesets/config@3.1.4': + resolution: {integrity: sha512-pf0bvD/v6WI2cRlZ6hzpjtZdSlXDXMAJ+Iz7xfFzV4ZxJ8OGGAON+1qYc99ZPrijnt4xp3VGG7eNvAOGS24V1Q==} + + '@changesets/errors@0.2.0': + resolution: {integrity: sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==} + + '@changesets/get-dependents-graph@2.1.4': + resolution: {integrity: sha512-ZsS00x6WvmHq3sQv8oCMwL0f/z3wbXCVuSVTJwCnnmbC/iBdNJGFx1EcbMG4PC6sXRyH69liM4A2WKXzn/kRPg==} + + '@changesets/get-release-plan@4.0.16': + resolution: {integrity: sha512-2K5Om6CrMPm45rtvckfzWo7e9jOVCKLCnXia5eUPaURH7/LWzri7pK1TycdzAuAtehLkW7VPbWLCSExTHmiI6g==} + + '@changesets/get-version-range-type@0.4.0': + resolution: {integrity: sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==} + + '@changesets/git@3.0.4': + resolution: {integrity: sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw==} + + '@changesets/logger@0.1.1': + resolution: {integrity: sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==} + + '@changesets/parse@0.4.3': + resolution: {integrity: sha512-ZDmNc53+dXdWEv7fqIUSgRQOLYoUom5Z40gmLgmATmYR9NbL6FJJHwakcCpzaeCy+1D0m0n7mT4jj2B/MQPl7A==} + + '@changesets/pre@2.0.2': + resolution: {integrity: sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug==} + + '@changesets/read@0.6.7': + resolution: {integrity: sha512-D1G4AUYGrBEk8vj8MGwf75k9GpN6XL3wg8i42P2jZZwFLXnlr2Pn7r9yuQNbaMCarP7ZQWNJbV6XLeysAIMhTA==} + + '@changesets/should-skip-package@0.1.2': + resolution: {integrity: sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw==} + + '@changesets/types@4.1.0': + resolution: {integrity: sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==} + + '@changesets/types@6.1.0': + resolution: {integrity: sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA==} + + '@changesets/write@0.4.0': + resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} + '@electron/get@2.0.3': resolution: {integrity: sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==} engines: {node: '>=12'} + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@hono/node-server@1.19.14': resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==} engines: {node: '>=18.14.1'} peerDependencies: hono: ^4 + '@inquirer/external-editor@1.0.3': + resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@manypkg/find-root@1.1.0': + resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} + + '@manypkg/get-packages@1.1.3': + resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} + '@modelcontextprotocol/sdk@1.29.0': resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} engines: {node: '>=18'} @@ -56,30 +238,164 @@ packages: '@cfworker/json-schema': optional: true + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@oxc-project/types@0.127.0': + resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==} + '@playwright/test@1.59.1': resolution: {integrity: sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==} engines: {node: '>=18'} hasBin: true + '@quansync/fs@1.0.0': + resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==} + + '@rolldown/binding-android-arm64@1.0.0-rc.17': + resolution: {integrity: sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-rc.17': + resolution: {integrity: sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-rc.17': + resolution: {integrity: sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-rc.17': + resolution: {integrity: sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17': + resolution: {integrity: sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17': + resolution: {integrity: sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.17': + resolution: {integrity: sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17': + resolution: {integrity: sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17': + resolution: {integrity: sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.17': + resolution: {integrity: sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.17': + resolution: {integrity: sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.17': + resolution: {integrity: sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.17': + resolution: {integrity: sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17': + resolution: {integrity: sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.17': + resolution: {integrity: sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-rc.17': + resolution: {integrity: sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==} + '@sindresorhus/is@4.6.0': resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} engines: {node: '>=10'} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@szmarczak/http-timer@4.0.6': resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} engines: {node: '>=10'} + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@types/cacheable-request@6.0.3': resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/http-cache-semantics@4.2.0': resolution: {integrity: sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==} + '@types/jsesc@2.5.1': + resolution: {integrity: sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==} + '@types/keyv@3.1.4': resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} - '@types/node@22.19.17': - resolution: {integrity: sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==} + '@types/node@12.20.55': + resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} '@types/node@24.12.2': resolution: {integrity: sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==} @@ -90,6 +406,35 @@ packages: '@types/yauzl@2.10.3': resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + '@vitest/expect@4.1.5': + resolution: {integrity: sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==} + + '@vitest/mocker@4.1.5': + resolution: {integrity: sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.5': + resolution: {integrity: sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==} + + '@vitest/runner@4.1.5': + resolution: {integrity: sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ==} + + '@vitest/snapshot@4.1.5': + resolution: {integrity: sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ==} + + '@vitest/spy@4.1.5': + resolution: {integrity: sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==} + + '@vitest/utils@4.1.5': + resolution: {integrity: sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==} + accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} @@ -105,9 +450,46 @@ packages: ajv@8.20.0: resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansis@4.2.0: + resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} + engines: {node: '>=14'} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + ast-kit@3.0.0-beta.1: + resolution: {integrity: sha512-trmleAnZ2PxN/loHWVhhx1qeOHSRXq4TDsBBxq3GqeJitfk3+jTQ+v/C1km/KYq9M7wKqCewMh+/NAvVH7m+bw==} + engines: {node: '>=20.19.0'} + async-mutex@0.5.0: resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==} + better-path-resolve@1.0.0: + resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} + engines: {node: '>=4'} + + birpc@4.0.0: + resolution: {integrity: sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw==} + body-parser@2.2.2: resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} @@ -116,6 +498,10 @@ packages: resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} @@ -123,6 +509,10 @@ packages: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} + cac@7.0.0: + resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==} + engines: {node: '>=20.19.0'} + cacheable-lookup@5.0.4: resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==} engines: {node: '>=10.6.0'} @@ -139,6 +529,13 @@ packages: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + chardet@2.1.1: + resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} + clone-response@1.0.3: resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} @@ -150,6 +547,9 @@ packages: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-signature@1.2.2: resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} engines: {node: '>=6.6.0'} @@ -191,13 +591,37 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} + defu@6.1.7: + resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + detect-indent@6.1.0: + resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} + engines: {node: '>=8'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + detect-node@2.1.0: resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + dts-resolver@2.1.3: + resolution: {integrity: sha512-bihc7jPC90VrosXNzK0LTE2cuLP6jr0Ro8jk+kMugHReJVLIpHz/xadeq3MhuwyO4TD4OA3L1Q8pBBFRc08Tsw==} + engines: {node: '>=20.19.0'} + peerDependencies: + oxc-resolver: '>=11.0.0' + peerDependenciesMeta: + oxc-resolver: + optional: true + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -210,6 +634,10 @@ packages: engines: {node: '>= 12.20.55'} hasBin: true + empathic@2.0.0: + resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} + engines: {node: '>=14'} + encodeurl@2.0.0: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} @@ -217,6 +645,10 @@ packages: end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + enquirer@2.4.1: + resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} + engines: {node: '>=8.6'} + env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -229,6 +661,9 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-module-lexer@2.1.0: + resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} + es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -243,6 +678,14 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + etag@1.8.1: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} @@ -255,6 +698,10 @@ packages: resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} engines: {node: '>=18.0.0'} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + express-rate-limit@8.4.1: resolution: {integrity: sha512-NGVYwQSAyEQgzxX1iCM978PP9AdO/hW93gMcF6ZwQCm+rFvLsBH6w4xcXWTcliS8La5EPRN3p9wzItqBwJrfNw==} engines: {node: '>= 16'} @@ -265,6 +712,9 @@ packages: resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} engines: {node: '>= 18'} + extendable-error@0.1.7: + resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} + extract-zip@2.0.1: resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} engines: {node: '>= 10.17.0'} @@ -273,16 +723,40 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + fd-slicer@1.1.0: resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + finalhandler@2.1.1: resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} engines: {node: '>= 18.0.0'} + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -291,6 +765,10 @@ packages: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} + fs-extra@7.0.1: + resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} + engines: {node: '>=6 <7 || >=8'} + fs-extra@8.1.0: resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} engines: {node: '>=6 <7 || >=8'} @@ -300,6 +778,11 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} @@ -315,6 +798,13 @@ packages: resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} engines: {node: '>=8'} + get-tsconfig@4.14.0: + resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + global-agent@3.0.0: resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==} engines: {node: '>=10.0'} @@ -323,6 +813,10 @@ packages: resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} engines: {node: '>= 0.4'} + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -349,6 +843,9 @@ packages: resolution: {integrity: sha512-qM0jDhFEaCBb4TxoW7f53Qrpv9RBiayUHo0S52JudprkhvpjIrGoU1mnnr29Fvd1U335ZFPZQY1wlkqgfGXyLg==} engines: {node: '>=16.9.0'} + hookable@6.1.1: + resolution: {integrity: sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==} + http-cache-semantics@4.2.0: resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} @@ -360,10 +857,22 @@ packages: resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==} engines: {node: '>=10.19.0'} + human-id@4.1.3: + resolution: {integrity: sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==} + hasBin: true + iconv-lite@0.7.2: resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + import-without-cache@0.3.3: + resolution: {integrity: sha512-bDxwDdF04gm550DfZHgffvlX+9kUlcz32UD0AeBTmVPFiWkrexF2XVmiuFFbDhiFuP8fQkrkvI2KdSNPYWAXkQ==} + engines: {node: '>=20.19.0'} + inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -375,15 +884,48 @@ packages: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-subdir@1.2.0: + resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} + engines: {node: '>=4'} + + is-windows@1.0.2: + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} jose@6.2.2: resolution: {integrity: sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==} + js-yaml@3.14.2: + resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} + hasBin: true + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} @@ -402,10 +944,90 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + lodash.startcase@4.4.0: + resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + lowercase-keys@2.0.0: resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} engines: {node: '>=8'} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + matcher@3.0.0: resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==} engines: {node: '>=10'} @@ -422,6 +1044,14 @@ packages: resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} engines: {node: '>=18'} + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + mime-db@1.54.0: resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} engines: {node: '>= 0.6'} @@ -438,9 +1068,18 @@ packages: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + negotiator@1.0.0: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} @@ -461,6 +1100,9 @@ packages: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} engines: {node: '>= 0.4'} + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -468,14 +1110,44 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + outdent@0.5.0: + resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} + p-cancelable@2.1.1: resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} engines: {node: '>=8'} + p-filter@2.1.0: + resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} + engines: {node: '>=8'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-map@2.1.0: + resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} + engines: {node: '>=6'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + package-manager-detector@0.2.11: + resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} + parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -483,9 +1155,31 @@ packages: path-to-regexp@8.4.2: resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pend@1.2.0: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + pify@4.0.1: + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} + pkce-challenge@5.0.1: resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} engines: {node: '>=16.20.0'} @@ -500,6 +1194,15 @@ packages: engines: {node: '>=18'} hasBin: true + postcss@8.5.12: + resolution: {integrity: sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==} + engines: {node: ^10 || ^12 || >=14} + + prettier@2.8.8: + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} + engines: {node: '>=10.13.0'} + hasBin: true + progress@2.0.3: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} engines: {node: '>=0.4.0'} @@ -515,6 +1218,15 @@ packages: resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==} engines: {node: '>=0.6'} + quansync@0.2.11: + resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + + quansync@1.0.0: + resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + quick-lru@5.1.1: resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} engines: {node: '>=10'} @@ -527,6 +1239,10 @@ packages: resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} engines: {node: '>= 0.10'} + read-yaml-file@1.1.0: + resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} + engines: {node: '>=6'} + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -534,17 +1250,55 @@ packages: resolve-alpn@1.2.1: resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + responselike@2.0.1: resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + roarr@2.15.4: resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} engines: {node: '>=8.0'} + rolldown-plugin-dts@0.23.2: + resolution: {integrity: sha512-PbSqLawLgZBGcOGT3yqWBGn4cX+wh2nt5FuBGdcMHyOhoukmjbhYAl8NT9sE4U38Cm9tqLOIQeOrvzeayM0DLQ==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@ts-macro/tsc': ^0.3.6 + '@typescript/native-preview': '>=7.0.0-dev.20260325.1' + rolldown: ^1.0.0-rc.12 + typescript: ^5.0.0 || ^6.0.0 + vue-tsc: ~3.2.0 + peerDependenciesMeta: + '@ts-macro/tsc': + optional: true + '@typescript/native-preview': + optional: true + typescript: + optional: true + vue-tsc: + optional: true + + rolldown@1.0.0-rc.17: + resolution: {integrity: sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + router@2.2.0: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -599,21 +1353,111 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + spawndamnit@3.0.1: + resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + sprintf-js@1.1.3: resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} + std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + sumchecker@3.0.1: resolution: {integrity: sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==} engines: {node: '>= 8.0'} + term-size@2.2.1: + resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} + engines: {node: '>=8'} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.1.1: + resolution: {integrity: sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==} + engines: {node: '>=18'} + + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + tsdown@0.21.10: + resolution: {integrity: sha512-3wk73yBhZe/wX7REqSdivNQ84TDs1mJ+IlnzrrEREP70xlJ/AEIzqaI04l/TzMKVIdkTdC3CPaADn2Lk/0SkdA==} + engines: {node: '>=20.19.0'} + hasBin: true + peerDependencies: + '@arethetypeswrong/core': ^0.18.1 + '@tsdown/css': 0.21.10 + '@tsdown/exe': 0.21.10 + '@vitejs/devtools': '*' + publint: ^0.3.0 + typescript: ^5.0.0 || ^6.0.0 + unplugin-unused: ^0.5.0 + peerDependenciesMeta: + '@arethetypeswrong/core': + optional: true + '@tsdown/css': + optional: true + '@tsdown/exe': + optional: true + '@vitejs/devtools': + optional: true + publint: + optional: true + typescript: + optional: true + unplugin-unused: + optional: true + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -625,13 +1469,13 @@ packages: resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} engines: {node: '>= 0.6'} - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} engines: {node: '>=14.17'} hasBin: true - undici-types@6.21.0: - resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + unconfig-core@7.5.0: + resolution: {integrity: sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==} undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} @@ -644,15 +1488,114 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} + unrun@0.2.37: + resolution: {integrity: sha512-AA7vDuYsgeSYVzJMm16UKA+aXFKhy7nFqW9z5l7q44K4ppFWZAMqYS58ePRZbugMLPH0fwwMzD5A8nP0avxwZQ==} + engines: {node: '>=20.19.0'} + hasBin: true + peerDependencies: + synckit: ^0.11.11 + peerDependenciesMeta: + synckit: + optional: true + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + vite@8.0.10: + resolution: {integrity: sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.1.5: + resolution: {integrity: sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.5 + '@vitest/browser-preview': 4.1.5 + '@vitest/browser-webdriverio': 4.1.5 + '@vitest/coverage-istanbul': 4.1.5 + '@vitest/coverage-v8': 4.1.5 + '@vitest/ui': 4.1.5 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -669,6 +1612,208 @@ packages: snapshots: + '@babel/generator@8.0.0-rc.3': + dependencies: + '@babel/parser': 8.0.0-rc.3 + '@babel/types': 8.0.0-rc.3 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + '@types/jsesc': 2.5.1 + jsesc: 3.1.0 + + '@babel/helper-string-parser@8.0.0-rc.3': {} + + '@babel/helper-validator-identifier@8.0.0-rc.3': {} + + '@babel/parser@8.0.0-rc.3': + dependencies: + '@babel/types': 8.0.0-rc.3 + + '@babel/runtime@7.29.2': {} + + '@babel/types@8.0.0-rc.3': + dependencies: + '@babel/helper-string-parser': 8.0.0-rc.3 + '@babel/helper-validator-identifier': 8.0.0-rc.3 + + '@biomejs/biome@2.4.13': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 2.4.13 + '@biomejs/cli-darwin-x64': 2.4.13 + '@biomejs/cli-linux-arm64': 2.4.13 + '@biomejs/cli-linux-arm64-musl': 2.4.13 + '@biomejs/cli-linux-x64': 2.4.13 + '@biomejs/cli-linux-x64-musl': 2.4.13 + '@biomejs/cli-win32-arm64': 2.4.13 + '@biomejs/cli-win32-x64': 2.4.13 + + '@biomejs/cli-darwin-arm64@2.4.13': + optional: true + + '@biomejs/cli-darwin-x64@2.4.13': + optional: true + + '@biomejs/cli-linux-arm64-musl@2.4.13': + optional: true + + '@biomejs/cli-linux-arm64@2.4.13': + optional: true + + '@biomejs/cli-linux-x64-musl@2.4.13': + optional: true + + '@biomejs/cli-linux-x64@2.4.13': + optional: true + + '@biomejs/cli-win32-arm64@2.4.13': + optional: true + + '@biomejs/cli-win32-x64@2.4.13': + optional: true + + '@changesets/apply-release-plan@7.1.1': + dependencies: + '@changesets/config': 3.1.4 + '@changesets/get-version-range-type': 0.4.0 + '@changesets/git': 3.0.4 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + detect-indent: 6.1.0 + fs-extra: 7.0.1 + lodash.startcase: 4.4.0 + outdent: 0.5.0 + prettier: 2.8.8 + resolve-from: 5.0.0 + semver: 7.7.4 + + '@changesets/assemble-release-plan@6.0.10': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.4 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + semver: 7.7.4 + + '@changesets/changelog-git@0.2.1': + dependencies: + '@changesets/types': 6.1.0 + + '@changesets/cli@2.31.0(@types/node@24.12.2)': + dependencies: + '@changesets/apply-release-plan': 7.1.1 + '@changesets/assemble-release-plan': 6.0.10 + '@changesets/changelog-git': 0.2.1 + '@changesets/config': 3.1.4 + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.4 + '@changesets/get-release-plan': 4.0.16 + '@changesets/git': 3.0.4 + '@changesets/logger': 0.1.1 + '@changesets/pre': 2.0.2 + '@changesets/read': 0.6.7 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@changesets/write': 0.4.0 + '@inquirer/external-editor': 1.0.3(@types/node@24.12.2) + '@manypkg/get-packages': 1.1.3 + ansi-colors: 4.1.3 + enquirer: 2.4.1 + fs-extra: 7.0.1 + mri: 1.2.0 + package-manager-detector: 0.2.11 + picocolors: 1.1.1 + resolve-from: 5.0.0 + semver: 7.7.4 + spawndamnit: 3.0.1 + term-size: 2.2.1 + transitivePeerDependencies: + - '@types/node' + + '@changesets/config@3.1.4': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.4 + '@changesets/logger': 0.1.1 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + fs-extra: 7.0.1 + micromatch: 4.0.8 + + '@changesets/errors@0.2.0': + dependencies: + extendable-error: 0.1.7 + + '@changesets/get-dependents-graph@2.1.4': + dependencies: + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + picocolors: 1.1.1 + semver: 7.7.4 + + '@changesets/get-release-plan@4.0.16': + dependencies: + '@changesets/assemble-release-plan': 6.0.10 + '@changesets/config': 3.1.4 + '@changesets/pre': 2.0.2 + '@changesets/read': 0.6.7 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + + '@changesets/get-version-range-type@0.4.0': {} + + '@changesets/git@3.0.4': + dependencies: + '@changesets/errors': 0.2.0 + '@manypkg/get-packages': 1.1.3 + is-subdir: 1.2.0 + micromatch: 4.0.8 + spawndamnit: 3.0.1 + + '@changesets/logger@0.1.1': + dependencies: + picocolors: 1.1.1 + + '@changesets/parse@0.4.3': + dependencies: + '@changesets/types': 6.1.0 + js-yaml: 4.1.1 + + '@changesets/pre@2.0.2': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + fs-extra: 7.0.1 + + '@changesets/read@0.6.7': + dependencies: + '@changesets/git': 3.0.4 + '@changesets/logger': 0.1.1 + '@changesets/parse': 0.4.3 + '@changesets/types': 6.1.0 + fs-extra: 7.0.1 + p-filter: 2.1.0 + picocolors: 1.1.1 + + '@changesets/should-skip-package@0.1.2': + dependencies: + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + + '@changesets/types@4.1.0': {} + + '@changesets/types@6.1.0': {} + + '@changesets/write@0.4.0': + dependencies: + '@changesets/types': 6.1.0 + fs-extra: 7.0.1 + human-id: 4.1.3 + prettier: 2.8.8 + '@electron/get@2.0.3': dependencies: debug: 4.4.3 @@ -683,10 +1828,63 @@ snapshots: transitivePeerDependencies: - supports-color + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + '@hono/node-server@1.19.14(hono@4.12.15)': dependencies: hono: 4.12.15 + '@inquirer/external-editor@1.0.3(@types/node@24.12.2)': + dependencies: + chardet: 2.1.1 + iconv-lite: 0.7.2 + optionalDependencies: + '@types/node': 24.12.2 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@manypkg/find-root@1.1.0': + dependencies: + '@babel/runtime': 7.29.2 + '@types/node': 12.20.55 + find-up: 4.1.0 + fs-extra: 8.1.0 + + '@manypkg/get-packages@1.1.3': + dependencies: + '@babel/runtime': 7.29.2 + '@changesets/types': 4.1.0 + '@manypkg/find-root': 1.1.0 + fs-extra: 8.1.0 + globby: 11.1.0 + read-yaml-file: 1.1.0 + '@modelcontextprotocol/sdk@1.29.0(zod@4.3.6)': dependencies: '@hono/node-server': 1.19.14(hono@4.12.15) @@ -709,32 +1907,124 @@ snapshots: transitivePeerDependencies: - supports-color + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@oxc-project/types@0.127.0': {} + '@playwright/test@1.59.1': dependencies: playwright: 1.59.1 + '@quansync/fs@1.0.0': + dependencies: + quansync: 1.0.0 + + '@rolldown/binding-android-arm64@1.0.0-rc.17': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.17': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-rc.17': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-rc.17': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.17': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.17': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.17': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.17': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.17': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.17': + optional: true + + '@rolldown/pluginutils@1.0.0-rc.17': {} + '@sindresorhus/is@4.6.0': {} + '@standard-schema/spec@1.1.0': {} + '@szmarczak/http-timer@4.0.6': dependencies: defer-to-connect: 2.0.1 + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + '@types/cacheable-request@6.0.3': dependencies: '@types/http-cache-semantics': 4.2.0 '@types/keyv': 3.1.4 - '@types/node': 22.19.17 + '@types/node': 24.12.2 '@types/responselike': 1.0.3 + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.8': {} + '@types/http-cache-semantics@4.2.0': {} + '@types/jsesc@2.5.1': {} + '@types/keyv@3.1.4': dependencies: - '@types/node': 22.19.17 + '@types/node': 24.12.2 - '@types/node@22.19.17': - dependencies: - undici-types: 6.21.0 + '@types/node@12.20.55': {} '@types/node@24.12.2': dependencies: @@ -742,13 +2032,54 @@ snapshots: '@types/responselike@1.0.3': dependencies: - '@types/node': 22.19.17 + '@types/node': 24.12.2 '@types/yauzl@2.10.3': dependencies: - '@types/node': 22.19.17 + '@types/node': 24.12.2 optional: true + '@vitest/expect@4.1.5': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.5 + '@vitest/utils': 4.1.5 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.5(vite@8.0.10(@types/node@24.12.2))': + dependencies: + '@vitest/spy': 4.1.5 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.0.10(@types/node@24.12.2) + + '@vitest/pretty-format@4.1.5': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.5': + dependencies: + '@vitest/utils': 4.1.5 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.5': + dependencies: + '@vitest/pretty-format': 4.1.5 + '@vitest/utils': 4.1.5 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.5': {} + + '@vitest/utils@4.1.5': + dependencies: + '@vitest/pretty-format': 4.1.5 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + accepts@2.0.0: dependencies: mime-types: 3.0.2 @@ -765,10 +2096,38 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + ansi-colors@4.1.3: {} + + ansi-regex@5.0.1: {} + + ansis@4.2.0: {} + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + array-union@2.1.0: {} + + assertion-error@2.0.1: {} + + ast-kit@3.0.0-beta.1: + dependencies: + '@babel/parser': 8.0.0-rc.3 + estree-walker: 3.0.3 + pathe: 2.0.3 + async-mutex@0.5.0: dependencies: tslib: 2.8.1 + better-path-resolve@1.0.0: + dependencies: + is-windows: 1.0.2 + + birpc@4.0.0: {} + body-parser@2.2.2: dependencies: bytes: 3.1.2 @@ -786,10 +2145,16 @@ snapshots: boolean@3.2.0: optional: true + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + buffer-crc32@0.2.13: {} bytes@3.1.2: {} + cac@7.0.0: {} + cacheable-lookup@5.0.4: {} cacheable-request@7.0.4: @@ -812,6 +2177,10 @@ snapshots: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 + chai@6.2.2: {} + + chardet@2.1.1: {} + clone-response@1.0.3: dependencies: mimic-response: 1.0.1 @@ -820,6 +2189,8 @@ snapshots: content-type@1.0.5: {} + convert-source-map@2.0.0: {} + cookie-signature@1.2.2: {} cookie@0.7.2: {} @@ -859,11 +2230,23 @@ snapshots: object-keys: 1.1.1 optional: true + defu@6.1.7: {} + depd@2.0.0: {} + detect-indent@6.1.0: {} + + detect-libc@2.1.2: {} + detect-node@2.1.0: optional: true + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + dts-resolver@2.1.3: {} + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -880,18 +2263,27 @@ snapshots: transitivePeerDependencies: - supports-color + empathic@2.0.0: {} + encodeurl@2.0.0: {} end-of-stream@1.4.5: dependencies: once: 1.4.0 + enquirer@2.4.1: + dependencies: + ansi-colors: 4.1.3 + strip-ansi: 6.0.1 + env-paths@2.2.1: {} es-define-property@1.0.1: {} es-errors@1.3.0: {} + es-module-lexer@2.1.0: {} + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -904,6 +2296,12 @@ snapshots: escape-string-regexp@4.0.0: optional: true + esprima@4.0.1: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + etag@1.8.1: {} eventsource-parser@3.0.8: {} @@ -912,6 +2310,8 @@ snapshots: dependencies: eventsource-parser: 3.0.8 + expect-type@1.3.0: {} + express-rate-limit@8.4.1(express@5.2.1): dependencies: express: 5.2.1 @@ -950,6 +2350,8 @@ snapshots: transitivePeerDependencies: - supports-color + extendable-error@0.1.7: {} + extract-zip@2.0.1: dependencies: debug: 4.4.3 @@ -962,12 +2364,32 @@ snapshots: fast-deep-equal@3.1.3: {} + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + fast-uri@3.1.0: {} + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + fd-slicer@1.1.0: dependencies: pend: 1.2.0 + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + finalhandler@2.1.1: dependencies: debug: 4.4.3 @@ -979,10 +2401,21 @@ snapshots: transitivePeerDependencies: - supports-color + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + forwarded@0.2.0: {} fresh@2.0.0: {} + fs-extra@7.0.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + fs-extra@8.1.0: dependencies: graceful-fs: 4.2.11 @@ -992,6 +2425,9 @@ snapshots: fsevents@2.3.2: optional: true + fsevents@2.3.3: + optional: true + function-bind@1.1.2: {} get-intrinsic@1.3.0: @@ -1016,6 +2452,14 @@ snapshots: dependencies: pump: 3.0.4 + get-tsconfig@4.14.0: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + global-agent@3.0.0: dependencies: boolean: 3.2.0 @@ -1032,6 +2476,15 @@ snapshots: gopd: 1.2.0 optional: true + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + gopd@1.2.0: {} got@11.8.6: @@ -1063,6 +2516,8 @@ snapshots: hono@4.12.15: {} + hookable@6.1.1: {} + http-cache-semantics@4.2.0: {} http-errors@2.0.1: @@ -1078,22 +2533,53 @@ snapshots: quick-lru: 5.1.1 resolve-alpn: 1.2.1 + human-id@4.1.3: {} + iconv-lite@0.7.2: dependencies: safer-buffer: 2.1.2 + ignore@5.3.2: {} + + import-without-cache@0.3.3: {} + inherits@2.0.4: {} ip-address@10.1.0: {} ipaddr.js@1.9.1: {} + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + is-promise@4.0.0: {} + is-subdir@1.2.0: + dependencies: + better-path-resolve: 1.0.0 + + is-windows@1.0.2: {} + isexe@2.0.0: {} jose@6.2.2: {} + js-yaml@3.14.2: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + json-buffer@3.0.1: {} json-schema-traverse@1.0.0: {} @@ -1111,8 +2597,67 @@ snapshots: dependencies: json-buffer: 3.0.1 + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + lodash.startcase@4.4.0: {} + lowercase-keys@2.0.0: {} + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + matcher@3.0.0: dependencies: escape-string-regexp: 4.0.0 @@ -1124,6 +2669,13 @@ snapshots: merge-descriptors@2.0.0: {} + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + mime-db@1.54.0: {} mime-types@3.0.2: @@ -1134,8 +2686,12 @@ snapshots: mimic-response@3.1.0: {} + mri@1.2.0: {} + ms@2.1.3: {} + nanoid@3.3.11: {} + negotiator@1.0.0: {} normalize-url@6.1.0: {} @@ -1147,6 +2703,8 @@ snapshots: object-keys@1.1.1: optional: true + obug@2.1.1: {} + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -1155,16 +2713,52 @@ snapshots: dependencies: wrappy: 1.0.2 + outdent@0.5.0: {} + p-cancelable@2.1.1: {} + p-filter@2.1.0: + dependencies: + p-map: 2.1.0 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-map@2.1.0: {} + + p-try@2.2.0: {} + + package-manager-detector@0.2.11: + dependencies: + quansync: 0.2.11 + parseurl@1.3.3: {} + path-exists@4.0.0: {} + path-key@3.1.1: {} path-to-regexp@8.4.2: {} + path-type@4.0.0: {} + + pathe@2.0.3: {} + pend@1.2.0: {} + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.4: {} + + pify@4.0.1: {} + pkce-challenge@5.0.1: {} playwright-core@1.59.1: {} @@ -1175,6 +2769,14 @@ snapshots: optionalDependencies: fsevents: 2.3.2 + postcss@8.5.12: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prettier@2.8.8: {} + progress@2.0.3: {} proxy-addr@2.0.7: @@ -1191,6 +2793,12 @@ snapshots: dependencies: side-channel: 1.1.0 + quansync@0.2.11: {} + + quansync@1.0.0: {} + + queue-microtask@1.2.3: {} + quick-lru@5.1.1: {} range-parser@1.2.1: {} @@ -1202,14 +2810,27 @@ snapshots: iconv-lite: 0.7.2 unpipe: 1.0.0 + read-yaml-file@1.1.0: + dependencies: + graceful-fs: 4.2.11 + js-yaml: 3.14.2 + pify: 4.0.1 + strip-bom: 3.0.0 + require-from-string@2.0.2: {} resolve-alpn@1.2.1: {} + resolve-from@5.0.0: {} + + resolve-pkg-maps@1.0.0: {} + responselike@2.0.1: dependencies: lowercase-keys: 2.0.0 + reusify@1.1.0: {} + roarr@2.15.4: dependencies: boolean: 3.2.0 @@ -1220,6 +2841,45 @@ snapshots: sprintf-js: 1.1.3 optional: true + rolldown-plugin-dts@0.23.2(rolldown@1.0.0-rc.17)(typescript@6.0.3): + dependencies: + '@babel/generator': 8.0.0-rc.3 + '@babel/helper-validator-identifier': 8.0.0-rc.3 + '@babel/parser': 8.0.0-rc.3 + '@babel/types': 8.0.0-rc.3 + ast-kit: 3.0.0-beta.1 + birpc: 4.0.0 + dts-resolver: 2.1.3 + get-tsconfig: 4.14.0 + obug: 2.1.1 + picomatch: 4.0.4 + rolldown: 1.0.0-rc.17 + optionalDependencies: + typescript: 6.0.3 + transitivePeerDependencies: + - oxc-resolver + + rolldown@1.0.0-rc.17: + dependencies: + '@oxc-project/types': 0.127.0 + '@rolldown/pluginutils': 1.0.0-rc.17 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.17 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.17 + '@rolldown/binding-darwin-x64': 1.0.0-rc.17 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.17 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.17 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.17 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.17 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.17 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.17 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.17 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.17 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.17 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.17 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.17 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.17 + router@2.2.0: dependencies: debug: 4.4.3 @@ -1230,6 +2890,10 @@ snapshots: transitivePeerDependencies: - supports-color + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + safer-buffer@2.1.2: {} semver-compare@1.0.0: @@ -1237,8 +2901,7 @@ snapshots: semver@6.3.1: {} - semver@7.7.4: - optional: true + semver@7.7.4: {} send@1.2.1: dependencies: @@ -1306,19 +2969,90 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} + + signal-exit@4.1.0: {} + + slash@3.0.0: {} + + source-map-js@1.2.1: {} + + spawndamnit@3.0.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + sprintf-js@1.0.3: {} + sprintf-js@1.1.3: optional: true + stackback@0.0.2: {} + statuses@2.0.2: {} + std-env@4.1.0: {} + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-bom@3.0.0: {} + sumchecker@3.0.1: dependencies: debug: 4.4.3 transitivePeerDependencies: - supports-color + term-size@2.2.1: {} + + tinybench@2.9.0: {} + + tinyexec@1.1.1: {} + + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tinyrainbow@3.1.0: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + toidentifier@1.0.1: {} + tree-kill@1.2.2: {} + + tsdown@0.21.10(typescript@6.0.3): + dependencies: + ansis: 4.2.0 + cac: 7.0.0 + defu: 6.1.7 + empathic: 2.0.0 + hookable: 6.1.1 + import-without-cache: 0.3.3 + obug: 2.1.1 + picomatch: 4.0.4 + rolldown: 1.0.0-rc.17 + rolldown-plugin-dts: 0.23.2(rolldown@1.0.0-rc.17)(typescript@6.0.3) + semver: 7.7.4 + tinyexec: 1.1.1 + tinyglobby: 0.2.16 + tree-kill: 1.2.2 + unconfig-core: 7.5.0 + unrun: 0.2.37 + optionalDependencies: + typescript: 6.0.3 + transitivePeerDependencies: + - '@ts-macro/tsc' + - '@typescript/native-preview' + - oxc-resolver + - synckit + - vue-tsc + tslib@2.8.1: {} type-fest@0.13.1: @@ -1330,9 +3064,12 @@ snapshots: media-typer: 1.1.0 mime-types: 3.0.2 - typescript@5.9.3: {} + typescript@6.0.3: {} - undici-types@6.21.0: {} + unconfig-core@7.5.0: + dependencies: + '@quansync/fs': 1.0.0 + quansync: 1.0.0 undici-types@7.16.0: {} @@ -1340,12 +3077,59 @@ snapshots: unpipe@1.0.0: {} + unrun@0.2.37: + dependencies: + rolldown: 1.0.0-rc.17 + vary@1.1.2: {} + vite@8.0.10(@types/node@24.12.2): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.12 + rolldown: 1.0.0-rc.17 + tinyglobby: 0.2.16 + optionalDependencies: + '@types/node': 24.12.2 + fsevents: 2.3.3 + + vitest@4.1.5(@types/node@24.12.2)(vite@8.0.10(@types/node@24.12.2)): + dependencies: + '@vitest/expect': 4.1.5 + '@vitest/mocker': 4.1.5(vite@8.0.10(@types/node@24.12.2)) + '@vitest/pretty-format': 4.1.5 + '@vitest/runner': 4.1.5 + '@vitest/snapshot': 4.1.5 + '@vitest/spy': 4.1.5 + '@vitest/utils': 4.1.5 + es-module-lexer: 2.1.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.1.1 + tinyglobby: 0.2.16 + tinyrainbow: 3.1.0 + vite: 8.0.10(@types/node@24.12.2) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.12.2 + transitivePeerDependencies: + - msw + which@2.0.2: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + wrappy@1.0.2: {} yauzl@2.10.0: diff --git a/src/cdp-helpers.ts b/src/cdp-helpers.ts index 46f53a2..b6ce253 100644 --- a/src/cdp-helpers.ts +++ b/src/cdp-helpers.ts @@ -1,7 +1,7 @@ // Small CDP helpers shared by the input/DOM tool modules. ~100 LOC // of selector wait + keyboard mapping rather than a Playwright dep. -import type { CdpSession } from "./cdp.js"; +import type { CdpSession } from "./cdp"; // Coordinates in CSS pixels — what `Input.dispatchMouseEvent` expects. interface ElementRect { diff --git a/src/cdp.ts b/src/cdp.ts index 7e532a5..e77e048 100644 --- a/src/cdp.ts +++ b/src/cdp.ts @@ -3,11 +3,10 @@ // session cached per surface; `detachAll()` runs at will-quit. // // DevTools collision: `debugger.attach()` fails if DevTools are -// already open, so we close them first (set NEBULA_OPEN_DEVTOOLS=0 -// for MCP-driven tests to skip the flicker). +// already open, so we close them first. import type { BrowserWindow, WebContents } from "electron"; -import { resolveSurface, type SurfaceGetter } from "./surfaces.js"; +import { resolveSurface, type SurfaceGetter } from "./surfaces"; const CDP_PROTOCOL_VERSION = "1.3"; @@ -48,32 +47,20 @@ export function getOrAttachSession( const wc = win.webContents; const existing = attached.get(surface); - if (existing && existing.win === win && wc.debugger.isAttached()) { + if ( + existing && + existing.win.webContents === wc && + !existing.win.isDestroyed() && + wc.debugger.isAttached() + ) { return buildSession(existing.surface, wc); } - // Surface name was rebound to a new window — the old record's debugger - // and detach listener are still pinned to the previous window. Clean - // them up before we mint a fresh attachment for the new `win`. - if (existing && existing.win !== win) { - existing.teardownListener(); - try { - if ( - !existing.win.isDestroyed() && - existing.win.webContents.debugger.isAttached() - ) { - existing.win.webContents.debugger.detach(); - } - } catch { - // Best-effort cleanup; old window may already be tearing down. - } - attached.delete(surface); - } // Close auto-opened DevTools to avoid the attach conflict. if (wc.isDevToolsOpened()) { console.warn( `[mcp] closing DevTools on "${surface}" surface so the CDP debugger can attach. ` + - `Set NEBULA_OPEN_DEVTOOLS=0 when running MCP-driven tests to skip the flicker.`, + "Disable auto-opened DevTools in MCP-driven tests to skip the flicker.", ); wc.closeDevTools(); } @@ -89,17 +76,17 @@ export function getOrAttachSession( // Drop the cache on external detach (developer DevTools, another // debugger, renderer crash) so the next call re-attaches cleanly. - // Self-unregister so reattaching the same surface doesn't pile up - // listeners on `wc.debugger`, and only clear the cache if the record - // still points at THIS webContents — a rebind to a different window - // would otherwise wipe the new attachment. + // Self-unregister first so a stale listener can never fire again, + // and only delete the cache entry if it still belongs to this + // webContents — a later attach for the same surface key may have + // already replaced it. const onDetach = (_event: Electron.Event, reason: string) => { wc.debugger.off("detach", onDetach); console.warn( `[mcp] debugger detached from "${surface}" (reason: ${reason})`, ); const rec = attached.get(surface); - if (rec && rec.win.webContents === wc) { + if (rec?.win.webContents === wc) { attached.delete(surface); } }; @@ -123,12 +110,11 @@ function buildSession(surface: string, wc: WebContents): CdpSession { return wc.debugger.sendCommand(method, params ?? {}); }, detach: () => { - // Stale `CdpSession` handle: a rebind may have already swapped - // the cache to a different window. Only tear down the map entry - // if it still points at THIS webContents; otherwise we'd clobber - // the active binding and leak the old debugger anyway. + // Only mutate the cache if it still belongs to this webContents. + // A stale CdpSession handle from a prior binding must not delete + // the entry now serving a different window. const rec = attached.get(surface); - if (rec && rec.win.webContents === wc) { + if (rec?.win.webContents === wc) { rec.teardownListener(); attached.delete(surface); } diff --git a/src/index.test.ts b/src/index.test.ts new file mode 100644 index 0000000..c512a10 --- /dev/null +++ b/src/index.test.ts @@ -0,0 +1,122 @@ +// Lifecycle tests for the explicit start()/stop() API. These exercise +// the real HTTP transport on an ephemeral loopback port — they don't +// reach into Electron and don't need a fake McpServer. + +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { createElectronMcpServer } from "./index"; + +// Ephemeral-port helper — the harness picks a free port via `port: 0` +// and the handle exposes the resolved URL. +async function startServer( + options: Parameters[0] = { + getSurfaces: () => ({}), + }, +) { + const handle = createElectronMcpServer({ port: 0, ...options }); + return handle; +} + +const handlesToStop: Array<{ stop: () => Promise }> = []; + +beforeEach(() => { + // Silence the `[mcp] listening on ...` startup log — fine in dev, + // distracting in test output. The shared `createLogger` routes + // info/warn through console.info/warn. + vi.spyOn(console, "info").mockImplementation(() => {}); +}); + +afterEach(async () => { + while (handlesToStop.length > 0) { + const h = handlesToStop.pop(); + if (h) await h.stop().catch(() => {}); + } + vi.restoreAllMocks(); +}); + +// Minimal ToolDef used in registration-ordering and integration tests. +function pingTool(): { + name: string; + config: { title: string; description: string }; + handler: () => Promise<{ content: Array<{ type: "text"; text: string }> }>; +} { + return { + name: "ping", + config: { title: "Ping", description: "Returns pong." }, + handler: async () => ({ content: [{ type: "text", text: "pong" }] }), + }; +} + +describe("createElectronMcpServer", () => { + it("isRunning is false before start, true after start, false after stop", async () => { + const handle = await startServer(); + handlesToStop.push(handle); + expect(handle.isRunning).toBe(false); + expect(handle.url).toBeNull(); + await handle.start(); + expect(handle.isRunning).toBe(true); + expect(handle.url).toMatch(/^http:\/\/127\.0\.0\.1:\d+\/mcp$/); + await handle.stop(); + expect(handle.isRunning).toBe(false); + }); + + it("addTool throws when called after start()", async () => { + const handle = await startServer(); + handlesToStop.push(handle); + await handle.start(); + expect(() => handle.addTool(pingTool())).toThrow(/addTool.*after start/i); + }); + + it("tools added before start() are reachable via the MCP client", async () => { + const handle = await startServer(); + handlesToStop.push(handle); + handle.addTool(pingTool()); + await handle.start(); + if (!handle.url) throw new Error("expected handle.url after start()"); + + const transport = new StreamableHTTPClientTransport(new URL(handle.url)); + const client = new Client({ name: "lifecycle-test", version: "0.0.0" }); + try { + await client.connect(transport); + const tools = await client.listTools(); + expect(tools.tools.map((t) => t.name)).toContain("ping"); + + const result = await client.callTool({ name: "ping", arguments: {} }); + const content = result.content as Array<{ type: string; text: string }>; + expect(content[0]).toEqual({ type: "text", text: "pong" }); + } finally { + await client.close().catch(() => {}); + await transport.close().catch(() => {}); + } + }); + + it("double start() and double stop() are safe", async () => { + const handle = await startServer(); + handlesToStop.push(handle); + await Promise.all([handle.start(), handle.start()]); + expect(handle.isRunning).toBe(true); + const url = handle.url; + await handle.start(); + expect(handle.url).toBe(url); + + await Promise.all([handle.stop(), handle.stop()]); + expect(handle.isRunning).toBe(false); + await handle.stop(); + expect(handle.isRunning).toBe(false); + }); + + it("interleaved start/stop calls run in order, not in parallel", async () => { + // Cross-direction race regression: with the previous split + // `starting` / `stopping` latches a `stop()` issued while + // `start()` was in flight returned early (running was still + // null) and the server stayed up after start resolved. The + // mutex now serialises both directions. + const handle = await startServer(); + handlesToStop.push(handle); + const startP = handle.start(); + const stopP = handle.stop(); + await Promise.all([startP, stopP]); + expect(handle.isRunning).toBe(false); + }); +}); diff --git a/src/index.ts b/src/index.ts index 54b1af7..38c2901 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,26 +1,21 @@ -// Public entry: synchronous factory returning an explicit-lifecycle -// handle. Gating (app.isPackaged, env opt-in, port parsing) lives in -// the consumer (main.ts) — this module makes no assumptions about the -// host process. - import { Mutex } from "async-mutex"; -import { type RunningMcpServer, startMcpServer } from "./server.js"; -import type { SurfaceGetter, SurfaceMap } from "./surfaces.js"; -import type { ToolDef } from "./tool-def.js"; - -export type { SurfaceGetter, SurfaceMap, ToolDef }; +import { + type McpLogger, + type RunningMcpServer, + startMcpServer, +} from "./server"; +import type { SurfaceGetter, SurfaceMap } from "./surfaces"; +import type { ToolDef } from "./tool-def"; -interface ElectronMcpServerConfig { +export interface ElectronMcpServerConfig { getSurfaces: SurfaceGetter; port?: number; host?: string; - // Override server identity advertised in MCP `initialize`. Defaults - // to `{ name: "@nebula-agents/electron-mcp", version: "0.1.0" }`. - serverInfo?: { name: string; version: string }; - // Override the `initialize.instructions` text shown to the client. - // Defaults to a generic "drives floating BrowserWindow surfaces" - // string baked into the package. + path?: string; + serverName?: string; + serverVersion?: string; instructions?: string; + logger?: Partial; } export interface ElectronMcpServerHandle { @@ -71,8 +66,11 @@ export function createElectronMcpServer( extraTools: tools, port: config.port, host: config.host, - serverInfo: config.serverInfo, + path: config.path, + serverName: config.serverName, + serverVersion: config.serverVersion, instructions: config.instructions, + logger: config.logger, }); }); }, @@ -94,3 +92,5 @@ export function createElectronMcpServer( return handle; } + +export type { McpLogger, SurfaceGetter, SurfaceMap, ToolDef }; diff --git a/src/server.ts b/src/server.ts index b923837..984eb97 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,7 +1,5 @@ -// Streamable HTTP transport on `127.0.0.1` (not `localhost` — that -// can resolve to IPv6). Bare `node:http` (no Express/Hono for one -// route). No Origin check — the only path here is a localhost MCP -// client and Claude Code doesn't send Origin anyway. +// Streamable HTTP transport on `127.0.0.1` (not `localhost`, which can +// resolve to IPv6). Bare `node:http`; no framework for a single route. import { randomUUID } from "node:crypto"; import { @@ -13,69 +11,61 @@ import { import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"; -import { detachAll } from "./cdp.js"; -import type { SurfaceGetter } from "./surfaces.js"; -import type { ToolDef } from "./tool-def.js"; -import { registerAllTools } from "./tools/index.js"; - -// Minimal stdout/stderr logger — extracted from nebula-desktop's -// `@util/log` so this package has no internal-tooling dep. Consumers -// that want structured logs can wrap stdout themselves. -const log = { - info: (msg: string) => { - console.log(`[mcp] ${msg}`); - }, - warn: (msg: string) => { - console.warn(`[mcp] ${msg}`); - }, - error: (msg: string) => { - console.error(`[mcp] ${msg}`); - }, -}; +import { detachAll } from "./cdp"; +import type { SurfaceGetter } from "./surfaces"; +import type { ToolDef } from "./tool-def"; +import { registerAllTools } from "./tools"; const DEFAULT_HOST = "127.0.0.1"; const DEFAULT_PORT = 9229; -const MCP_PATH = "/mcp"; +const DEFAULT_PATH = "/mcp"; /** Retry once on EADDRINUSE — the previous Electron may still be releasing. */ const EADDRINUSE_RETRY_DELAY_MS = 100; -// Default `initialize` instructions sent to MCP clients. Generic, so the -// embedded server is useful out of the box. Consumers can override via -// `config.instructions` when they want app-specific guidance (which -// surface names exist, when to reach for the tools, etc.). -const DEFAULT_SERVER_INSTRUCTIONS = `\ -This server drives the floating BrowserWindow surfaces of a running -Electron app. Reach for it to inspect or validate renderer-visible -state — not for planning, not for main-process changes. +const SERVER_INSTRUCTIONS = `\ +This server drives Electron BrowserWindow surfaces exposed by the host app. +Use it to validate renderer-visible work by inspecting state, querying the +DOM/accessibility tree, taking screenshots, and sending input. + +Use when: + • You need to see what an Electron surface paints. + • You need to reproduce or verify a renderer-visible bug. + • You need to inspect renderer state, DOM, accessibility data, or the + app's preload bridge. + +Skip when: + • The change is main-process only. These tools observe renderer surfaces. + • The change is types-only, tests-only, or docs-only. Typical flow: - 1. list_surfaces — discover which surfaces are live. - 2. show_surface { surface: "" } — bring the target into view. - 3. screenshot { surface: "" } to see the rendered result, or - evaluate { surface: "", expression: "…" } to inspect state. + 1. list_surfaces — confirm the app exposed the expected surfaces. + 2. show_surface { surface: "" } — bring a target surface into view. + 3. screenshot { surface: "" } to see the result, or evaluate/query_dom + to inspect state. Scope of \`evaluate\`: runs in the renderer MAIN WORLD. -Reachable: document, window, React tree, your stores, fetch. +Reachable: document, window, renderer globals, the app's preload bridge, fetch. NOT reachable: require("electron"), process, Node APIs. -If a tool errors with "surface not available", the consumer Electron -app isn't running or hasn't registered that surface yet.`; +If a tool errors with "surface not available", the host app did not expose +that surface key or the BrowserWindow was destroyed.`; + +export interface McpLogger { + info: (message: string) => void; + warn: (message: string) => void; + error: (message: string) => void; +} interface McpServerConfig { getSurfaces: SurfaceGetter; - // Consumer-registered tools (e.g. an app's `trigger_hotkey`). Bound - // to every per-session McpServer alongside the bundled tools. extraTools?: readonly ToolDef[]; - // Pass `0` for an ephemeral port (used by tests). port?: number; - // Only override to `::1` if you really want IPv6 loopback. host?: string; - // Override server identity advertised in `initialize`. Defaults to - // `{ name: "@nebula-agents/electron-mcp", version: }`. - serverInfo?: { name: string; version: string }; - // Override the `initialize.instructions` text. Defaults to - // `DEFAULT_SERVER_INSTRUCTIONS` above. + path?: string; + serverName?: string; + serverVersion?: string; instructions?: string; + logger?: Partial; } export interface RunningMcpServer { @@ -88,6 +78,14 @@ export async function startMcpServer( ): Promise { const host = config.host ?? DEFAULT_HOST; const port = config.port ?? DEFAULT_PORT; + const path = normalizePath(config.path ?? DEFAULT_PATH); + const logger = config.logger; + // Latch flipped in stop() so a racing initialize between the + // sessions-snapshot and httpServer.close() can't mint a fresh + // transport that's never included in the shutdown pass — that + // session would otherwise keep the connection alive and stop() + // would hang on httpServer.close(). + let shuttingDown = false; // Hard loopback gate — anything else exposes evaluate/screenshot/ // CDP over the network. @@ -102,21 +100,16 @@ export async function startMcpServer( // McpServer pair per `initialize` (matches the SDK's multi-session // example). const sessions = new Map(); - // Latch flipped at the start of `stop()` so concurrent initializes - // can't sneak a fresh session past the snapshot and then keep - // `httpServer.close()` waiting on its open stream. - let shuttingDown = false; async function createSession(): Promise { const sessionServer = new McpServer( - config.serverInfo ?? { - name: "@nebula-agents/electron-mcp", - version: "0.1.0", + { + name: config.serverName ?? "electron-mcp", + version: config.serverVersion ?? "0.1.0", }, { capabilities: { tools: {} }, - // Returned in `initialize`; shown every session. - instructions: config.instructions ?? DEFAULT_SERVER_INSTRUCTIONS, + instructions: config.instructions ?? SERVER_INSTRUCTIONS, }, ); registerAllTools(sessionServer, { getSurfaces: config.getSurfaces }); @@ -166,10 +159,17 @@ export async function startMcpServer( const httpServer = createServer(async (req, res) => { try { - await routeRequest(req, res, sessions, createSession, () => shuttingDown); + await routeRequest( + req, + res, + sessions, + createSession, + path, + () => shuttingDown, + ); } catch (err) { const msg = err instanceof Error ? err.message : String(err); - log.error(`request failed: ${msg}`); + logger?.error?.(`[mcp] request failed: ${msg}`); if (!res.headersSent) { res.statusCode = 500; res.setHeader("content-type", "application/json"); @@ -181,7 +181,7 @@ export async function startMcpServer( }); try { - await listenWithRetry(httpServer, port, host); + await listenWithRetry(httpServer, port, host, logger); } catch (err) { httpServer.close(); throw err; @@ -195,16 +195,12 @@ export async function startMcpServer( // IPv6 literals must be bracketed in a URL authority — `http://::1:9229/mcp` // is not parseable, `http://[::1]:9229/mcp` is. const urlHost = host.includes(":") ? `[${host}]` : host; - const url = `http://${urlHost}:${boundPort}${MCP_PATH}`; - log.info(`listening on ${url}`); + const url = `http://${urlHost}:${boundPort}${path}`; + logger?.info?.(`[mcp] listening on ${url}`); return { url, stop: async () => { - // Flip the gate first so `routeRequest`/`createSession` reject - // any in-flight `initialize` before we snapshot — otherwise a - // session minted after the snapshot keeps `httpServer.close()` - // hanging on its open stream. shuttingDown = true; detachAll(); // Snapshot — `server.close()` fires `transport.onclose` which @@ -237,6 +233,7 @@ async function routeRequest( res: ServerResponse, sessions: Map, createSession: () => Promise, + path: string, isShuttingDown: () => boolean, ): Promise { const url = new URL( @@ -244,16 +241,15 @@ async function routeRequest( `http://${req.headers.host ?? "localhost"}`, ); - if (url.pathname !== MCP_PATH) { + if (url.pathname !== path) { res.statusCode = 404; res.setHeader("content-type", "application/json"); res.end(JSON.stringify({ error: `not found: ${url.pathname}` })); return; } - // Reject everything once `stop()` has flipped the latch — sessions - // minted after the snapshot would leak open streams and stall - // `httpServer.close()`. + // Reject everything once stop() has begun — any new initialize + // would create a session past the shutdown snapshot. if (isShuttingDown()) { res.statusCode = 503; res.setHeader("content-type", "application/json"); @@ -261,7 +257,7 @@ async function routeRequest( res.end( JSON.stringify({ jsonrpc: "2.0", - error: { code: -32000, message: "Server is shutting down" }, + error: { code: -32000, message: "Server shutting down" }, id: null, }), ); @@ -282,10 +278,10 @@ async function routeRequest( if (parsed.reason === "too-large") { res.statusCode = 413; res.setHeader("content-type", "application/json"); - // Pair with the `socket.destroy()` callback below to terminate - // the upload only after the 413 response actually flushes — - // destroying synchronously after `res.end()` can race the - // flush and surface as ECONNRESET on the client. + // Pair with `socket.destroy()` from the `res.end` callback to + // terminate the upload only after the response body has + // flushed — destroying synchronously after `end()` can drop + // the JSON-RPC error and surface as ECONNRESET on the client. res.setHeader("connection", "close"); res.end( JSON.stringify({ @@ -296,7 +292,9 @@ async function routeRequest( }, id: null, }), - () => req.socket?.destroy(), + () => { + req.socket?.destroy(); + }, ); return; } @@ -381,6 +379,12 @@ async function routeRequest( res.end(JSON.stringify({ error: `method not allowed: ${req.method}` })); } +function normalizePath(path: string): string { + const trimmed = path.trim(); + if (trimmed.length === 0) return DEFAULT_PATH; + return trimmed.startsWith("/") ? trimmed : `/${trimmed}`; +} + // Branch on `ok`, not body shape — `"ping"` and `42` are valid // non-object bodies. Two failure reasons so the caller can pick // `400 -32700` (parse) vs `413` (too-large). @@ -418,6 +422,7 @@ function listenWithRetry( httpServer: Server, port: number, host: string, + logger?: Partial, ): Promise { return new Promise((resolve, reject) => { let retried = false; @@ -425,7 +430,7 @@ function listenWithRetry( const onError = (err: NodeJS.ErrnoException): void => { if (err.code === "EADDRINUSE" && !retried) { retried = true; - log.warn( + logger?.warn?.( `port ${port} busy, retrying in ${EADDRINUSE_RETRY_DELAY_MS}ms…`, ); setTimeout(() => { diff --git a/src/surfaces.ts b/src/surfaces.ts index b0c1a79..8168038 100644 --- a/src/surfaces.ts +++ b/src/surfaces.ts @@ -22,11 +22,10 @@ export function resolveSurface( getSurfaces: SurfaceGetter, surface: string, ): BrowserWindow { - // Guard against inherited keys (`__proto__`, `toString`, …) — a - // plain `surfaces[surface]` lookup would return `Object.prototype` - // for `"__proto__"`, which is not a `BrowserWindow` and would throw - // a confusing `isDestroyed is not a function` instead of the - // intended `SurfaceNotFoundError`. + // Own-property guard — without it, lookups like `"__proto__"` or + // `"constructor"` walk up Object's prototype chain and `isDestroyed()` + // throws a raw TypeError on a function value instead of surfacing + // the expected SurfaceNotFoundError. const surfaces = getSurfaces(); const win = Object.prototype.hasOwnProperty.call(surfaces, surface) ? surfaces[surface] diff --git a/src/testing.ts b/src/testing.ts new file mode 100644 index 0000000..969f35b --- /dev/null +++ b/src/testing.ts @@ -0,0 +1,326 @@ +// Shared test doubles for MCP tool factories. Tools register against +// an `McpServer` instance from `@modelcontextprotocol/sdk`; rather +// than spin up the SDK in node, the factories see this minimal +// `registerTool`-only surface and the test asserts on captured args. + +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { BrowserWindow, WebContents } from "electron"; +import { vi } from "vitest"; +import { type ZodRawShape, z } from "zod"; + +interface RegisteredTool { + name: string; + config: { + title?: string; + description?: string; + inputSchema?: ZodRawShape; + }; + handler: (input: Record) => Promise; +} + +interface FakeMcpServer { + tools: Map; + registerTool: ( + name: string, + config: RegisteredTool["config"], + handler: RegisteredTool["handler"], + ) => void; + // Cast helper so call sites read clean. + readonly asMcpServer: McpServer; +} + +export function createFakeMcpServer(): FakeMcpServer { + const tools = new Map(); + const fake = { + tools, + registerTool: ( + name: string, + config: RegisteredTool["config"], + handler: RegisteredTool["handler"], + ) => { + tools.set(name, { name, config, handler }); + }, + }; + Object.defineProperty(fake, "asMcpServer", { + get: () => fake as unknown as McpServer, + }); + return fake as FakeMcpServer; +} + +export function getTool(server: FakeMcpServer, name: string): RegisteredTool { + const tool = server.tools.get(name); + if (!tool) throw new Error(`tool "${name}" was not registered`); + return tool; +} + +// Wrap the registerTool input-schema (a `ZodRawShape`) in `z.object()` +// to drive a parse — the SDK does this internally before calling the +// handler. +export function parseInput(tool: RegisteredTool, input: unknown) { + const shape = tool.config.inputSchema ?? {}; + return z.object(shape).safeParse(input); +} + +interface FakeWebContentsOptions { + id?: number; + url?: string; + isLoading?: boolean; + isDevToolsOpened?: boolean; + isAttached?: boolean; + cdp?: Record< + string, + | unknown + | (( + params: Record | undefined, + ) => unknown | Promise) + >; + capturePage?: (rect?: { + x: number; + y: number; + width: number; + height: number; + }) => Promise; +} + +interface FakeNativeImage { + isEmpty: () => boolean; + toPNG: () => Buffer; + getSize: () => { width: number; height: number }; +} + +export function createFakeImage(opts: { + empty?: boolean; + png?: Buffer; + width?: number; + height?: number; +}): FakeNativeImage { + const empty = opts.empty ?? false; + const png = opts.png ?? Buffer.from([]); + const width = opts.width ?? 0; + const height = opts.height ?? 0; + return { + isEmpty: () => empty, + toPNG: () => png, + getSize: () => ({ width, height }), + }; +} + +type Listener = (...args: unknown[]) => void; + +interface FakeWebContents { + id: number; + debugger: { + isAttached: () => boolean; + attach: (version: string) => void; + detach: () => void; + sendCommand: ( + method: string, + params?: Record, + ) => Promise; + on: (event: string, listener: Listener) => void; + off: (event: string, listener: Listener) => void; + }; + isLoading: () => boolean; + isDevToolsOpened: () => boolean; + closeDevTools: () => void; + capturePage: FakeWebContentsOptions["capturePage"]; + reload: () => void; + reloadIgnoringCache: () => void; + getURL: () => string; + on: (event: string, listener: Listener) => void; + off: (event: string, listener: Listener) => void; + once: (event: string, listener: Listener) => void; + emit: (event: string, ...args: unknown[]) => void; + cdpCalls: Array<{ method: string; params?: Record }>; + // Test-only helpers for exercising debugger event flow. + emitDebugger: (event: string, ...args: unknown[]) => void; + debuggerListenerCount: (event: string) => number; + asWebContents: WebContents; +} + +export function createFakeWebContents( + opts: FakeWebContentsOptions = {}, +): FakeWebContents { + let attached = opts.isAttached ?? true; + const listeners = new Map>(); + const onceListeners = new Map>(); + const cdpCalls: FakeWebContents["cdpCalls"] = []; + + const cdpResponses = opts.cdp ?? {}; + + const on = (event: string, listener: Listener) => { + const bucket = listeners.get(event) ?? new Set(); + bucket.add(listener); + listeners.set(event, bucket); + }; + const off = (event: string, listener: Listener) => { + listeners.get(event)?.delete(listener); + onceListeners.get(event)?.delete(listener); + }; + const once = (event: string, listener: Listener) => { + const bucket = onceListeners.get(event) ?? new Set(); + bucket.add(listener); + onceListeners.set(event, bucket); + }; + const emit = (event: string, ...args: unknown[]) => { + // Match Node EventEmitter semantics: snapshot first, unregister + // once-listeners *before* invoking, so a handler that calls + // emit() recursively or adds new listeners doesn't see the + // already-firing batch and doesn't fire stale once-listeners. + const normal = [...(listeners.get(event) ?? [])]; + const onceSnapshot = [...(onceListeners.get(event) ?? [])]; + onceListeners.delete(event); + for (const l of normal) l(...args); + for (const l of onceSnapshot) l(...args); + }; + + // Track debugger listeners so tests can exercise the same + // attach/detach/cleanup contract production code follows. Using + // bare `vi.fn()` for on/off lets tools that skip getOrAttachSession + // (or break listener teardown) silently pass tests. + const debuggerListeners = new Map>(); + const debuggerOn = (event: string, listener: Listener) => { + const bucket = debuggerListeners.get(event) ?? new Set(); + bucket.add(listener); + debuggerListeners.set(event, bucket); + }; + const debuggerOff = (event: string, listener: Listener) => { + debuggerListeners.get(event)?.delete(listener); + }; + const emitDebugger = (event: string, ...args: unknown[]) => { + for (const l of debuggerListeners.get(event) ?? []) l(...args); + }; + + const fake = { + id: opts.id ?? 1, + debugger: { + isAttached: () => attached, + attach: vi.fn((_version: string) => { + attached = true; + }), + detach: vi.fn(() => { + attached = false; + }), + sendCommand: async (method: string, params?: Record) => { + // Reject post-detach calls so tests catch tools that mint a + // stale session handle and keep using it after teardown. + if (!attached) { + throw new Error( + `[fake-debugger] sendCommand("${method}") while not attached`, + ); + } + cdpCalls.push({ method, params }); + const response = cdpResponses[method]; + if (typeof response === "function") { + return await ( + response as (p: Record | undefined) => unknown + )(params); + } + return response ?? {}; + }, + on: vi.fn(debuggerOn), + off: vi.fn(debuggerOff), + }, + // Test-only escape hatch — fire a debugger event the way Electron + // does so tests can exercise CdpSession listener-cleanup paths. + emitDebugger, + debuggerListenerCount: (event: string) => + debuggerListeners.get(event)?.size ?? 0, + isLoading: () => opts.isLoading ?? false, + isDevToolsOpened: () => opts.isDevToolsOpened ?? false, + closeDevTools: vi.fn(), + capturePage: opts.capturePage, + reload: vi.fn(), + reloadIgnoringCache: vi.fn(), + getURL: () => opts.url ?? "http://localhost/", + on, + off, + once, + emit, + cdpCalls, + }; + Object.defineProperty(fake, "asWebContents", { + get: () => fake as unknown as WebContents, + }); + return fake as unknown as FakeWebContents; +} + +interface FakeBrowserWindowOptions { + visible?: boolean; + focused?: boolean; + focusable?: boolean; + alwaysOnTop?: boolean; + destroyed?: boolean; + bounds?: { x: number; y: number; width: number; height: number }; + webContents?: FakeWebContents; +} + +interface FakeBrowserWindow { + webContents: FakeWebContents; + isDestroyed: () => boolean; + isVisible: () => boolean; + isFocused: () => boolean; + isFocusable: () => boolean; + isAlwaysOnTop: () => boolean; + getBounds: () => { x: number; y: number; width: number; height: number }; + show: () => void; + hide: () => void; + focus: () => void; + __setVisible: (next: boolean) => void; + __setFocused: (next: boolean) => void; + __setFocusable: (next: boolean) => void; + __destroy: () => void; + showCalls: number; + hideCalls: number; + focusCalls: number; + asBrowserWindow: BrowserWindow; +} + +export function createFakeBrowserWindow( + opts: FakeBrowserWindowOptions = {}, +): FakeBrowserWindow { + let visible = opts.visible ?? true; + let focused = opts.focused ?? false; + let focusable = opts.focusable ?? true; + let destroyed = opts.destroyed ?? false; + const win = { + webContents: opts.webContents ?? createFakeWebContents(), + isDestroyed: () => destroyed, + isVisible: () => visible, + isFocused: () => focused, + isFocusable: () => focusable, + isAlwaysOnTop: () => opts.alwaysOnTop ?? false, + getBounds: () => opts.bounds ?? { x: 0, y: 0, width: 100, height: 100 }, + show: () => { + visible = true; + win.showCalls += 1; + }, + hide: () => { + visible = false; + win.hideCalls += 1; + }, + focus: () => { + focused = true; + win.focusCalls += 1; + }, + __setVisible: (n: boolean) => { + visible = n; + }, + __setFocused: (n: boolean) => { + focused = n; + }, + __setFocusable: (n: boolean) => { + focusable = n; + }, + __destroy: () => { + destroyed = true; + }, + showCalls: 0, + hideCalls: 0, + focusCalls: 0, + }; + Object.defineProperty(win, "asBrowserWindow", { + get: () => win as unknown as BrowserWindow, + }); + return win as unknown as FakeBrowserWindow; +} diff --git a/src/tool-def.ts b/src/tool-def.ts index 1d9bee0..cd89cc9 100644 --- a/src/tool-def.ts +++ b/src/tool-def.ts @@ -1,9 +1,7 @@ // Public tool-definition shape for `addTool()`. Mirrors the args of // `McpServer.registerTool` from `@modelcontextprotocol/sdk` so a // consumer can build a tool with the SDK's types and hand it to us -// without an extra adapter layer. This is the contract that will -// extract verbatim into the future `@nebula-agents/electron-mcp/types` -// sub-export. +// without an extra adapter layer. // // We can't lift the SDK's generic registerTool signature with // `Parameters<...>` (resolves to `never` for generic methods), and diff --git a/src/tools/ax-snapshot.test.ts b/src/tools/ax-snapshot.test.ts new file mode 100644 index 0000000..b03ffb7 --- /dev/null +++ b/src/tools/ax-snapshot.test.ts @@ -0,0 +1,83 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { detachAll } from "../cdp"; +import { + createFakeBrowserWindow, + createFakeMcpServer, + createFakeWebContents, + getTool, + parseInput, +} from "../testing"; +import { registerAxSnapshot } from "./ax-snapshot"; + +afterEach(() => detachAll()); + +describe("get_ax_snapshot", () => { + it("validates surface and rejects an empty root selector", () => { + const server = createFakeMcpServer(); + registerAxSnapshot(server.asMcpServer, () => ({})); + const tool = getTool(server, "get_ax_snapshot"); + expect(parseInput(tool, {}).success).toBe(false); + expect(parseInput(tool, { surface: "main", root: "" }).success).toBe(false); + expect(parseInput(tool, { surface: "main" }).success).toBe(true); + expect(parseInput(tool, { surface: "main", root: "#app" }).success).toBe( + true, + ); + }); + + it("returns the full AX tree, filtering ignored nodes by default", async () => { + const server = createFakeMcpServer(); + const wc = createFakeWebContents({ + cdp: { + "Accessibility.enable": {}, + "Accessibility.disable": {}, + "Accessibility.getFullAXTree": { + nodes: [ + { + nodeId: "1", + role: { value: "button" }, + name: { value: "Go" }, + childIds: ["2"], + }, + { nodeId: "2", ignored: true }, + ], + }, + }, + }); + const win = createFakeBrowserWindow({ webContents: wc }); + registerAxSnapshot(server.asMcpServer, () => ({ + ax_main: win.asBrowserWindow, + })); + const tool = getTool(server, "get_ax_snapshot"); + const result = (await tool.handler({ surface: "ax_main" })) as { + structuredContent: { nodes: Array<{ id: string; children: string[] }> }; + }; + // Ignored child filtered out + child reference scrubbed. + expect(result.structuredContent.nodes).toHaveLength(1); + expect(result.structuredContent.nodes[0]).toMatchObject({ + id: "1", + children: [], + }); + }); + + it("returns isError when the root selector cannot be resolved", async () => { + const server = createFakeMcpServer(); + const wc = createFakeWebContents({ + cdp: { + "Accessibility.enable": {}, + "Accessibility.disable": {}, + "Runtime.evaluate": { result: { type: "object" } }, // no objectId + }, + }); + const win = createFakeBrowserWindow({ webContents: wc }); + registerAxSnapshot(server.asMcpServer, () => ({ + ax_err: win.asBrowserWindow, + })); + const tool = getTool(server, "get_ax_snapshot"); + const result = (await tool.handler({ + surface: "ax_err", + root: "#missing", + })) as { isError: boolean; content: Array<{ text: string }> }; + expect(result.isError).toBe(true); + expect(result.content[0].text).toMatch(/root selector not resolvable/); + }); +}); diff --git a/src/tools/ax-snapshot.ts b/src/tools/ax-snapshot.ts index b792969..aba26ef 100644 --- a/src/tools/ax-snapshot.ts +++ b/src/tools/ax-snapshot.ts @@ -4,8 +4,8 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; -import { getOrAttachSession } from "../cdp.js"; -import type { SurfaceGetter } from "../surfaces.js"; +import { getOrAttachSession } from "../cdp"; +import type { SurfaceGetter } from "../surfaces"; const inputSchema = { surface: z.string().min(1).describe("Which surface to snapshot."), diff --git a/src/tools/click.test.ts b/src/tools/click.test.ts new file mode 100644 index 0000000..55470f3 --- /dev/null +++ b/src/tools/click.test.ts @@ -0,0 +1,91 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { detachAll } from "../cdp"; +import { + createFakeBrowserWindow, + createFakeMcpServer, + createFakeWebContents, + getTool, + parseInput, +} from "../testing"; +import { registerClick } from "./click"; + +afterEach(() => detachAll()); + +function rectResponse(x: number, y: number, width: number, height: number) { + return { + result: { type: "object", value: { x, y, width, height } }, + }; +} + +describe("click", () => { + it("requires non-empty surface + selector", () => { + const server = createFakeMcpServer(); + registerClick(server.asMcpServer, () => ({})); + const tool = getTool(server, "click"); + expect(parseInput(tool, { surface: "main" }).success).toBe(false); + expect(parseInput(tool, { selector: "#go" }).success).toBe(false); + expect(parseInput(tool, { surface: "main", selector: "" }).success).toBe( + false, + ); + expect(parseInput(tool, { surface: "main", selector: "#go" }).success).toBe( + true, + ); + }); + + it("dispatches mousePressed + mouseReleased at the element center", async () => { + const server = createFakeMcpServer(); + const wc = createFakeWebContents({ + cdp: { + "Runtime.evaluate": rectResponse(40, 60, 100, 40), + }, + }); + const win = createFakeBrowserWindow({ visible: false, webContents: wc }); + registerClick(server.asMcpServer, () => ({ + click_main: win.asBrowserWindow, + })); + + const tool = getTool(server, "click"); + const result = (await tool.handler({ + surface: "click_main", + selector: "#go", + })) as { + structuredContent: { rect: { centerX: number; centerY: number } }; + }; + + // hidden window is auto-shown + expect(win.showCalls).toBe(1); + expect(result.structuredContent.rect.centerX).toBe(90); + expect(result.structuredContent.rect.centerY).toBe(80); + const dispatched = wc.cdpCalls + .filter((c) => c.method === "Input.dispatchMouseEvent") + .map((c) => c.params?.type); + expect(dispatched).toEqual(["mousePressed", "mouseReleased"]); + }); + + it("propagates a renderer exception from the selector wait", async () => { + const server = createFakeMcpServer(); + const wc = createFakeWebContents({ + cdp: { + "Runtime.evaluate": () => ({ + result: { type: "object" }, + exceptionDetails: { + text: "SyntaxError: bad selector", + exception: { description: "SyntaxError: bad selector" }, + }, + }), + }, + }); + const win = createFakeBrowserWindow({ webContents: wc }); + registerClick(server.asMcpServer, () => ({ + click_err: win.asBrowserWindow, + })); + const tool = getTool(server, "click"); + await expect( + tool.handler({ + surface: "click_err", + selector: ":::not-a-selector", + timeoutMs: 50, + }), + ).rejects.toThrow(/SyntaxError/); + }); +}); diff --git a/src/tools/click.ts b/src/tools/click.ts index ab1b267..5cd72d6 100644 --- a/src/tools/click.ts +++ b/src/tools/click.ts @@ -1,8 +1,8 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; -import { getOrAttachSession } from "../cdp.js"; -import { waitForSelector } from "../cdp-helpers.js"; -import { resolveSurface, type SurfaceGetter } from "../surfaces.js"; +import { getOrAttachSession } from "../cdp"; +import { waitForSelector } from "../cdp-helpers"; +import { resolveSurface, type SurfaceGetter } from "../surfaces"; const inputSchema = { surface: z.string().min(1).describe("Which surface to click in."), diff --git a/src/tools/evaluate.test.ts b/src/tools/evaluate.test.ts new file mode 100644 index 0000000..73d69cf --- /dev/null +++ b/src/tools/evaluate.test.ts @@ -0,0 +1,94 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { detachAll } from "../cdp"; +import { + createFakeBrowserWindow, + createFakeMcpServer, + createFakeWebContents, + getTool, + parseInput, +} from "../testing"; +import { registerEvaluate } from "./evaluate"; + +afterEach(() => detachAll()); + +describe("evaluate", () => { + it("validates the surface + expression and bounds the timeout", () => { + const server = createFakeMcpServer(); + registerEvaluate(server.asMcpServer, () => ({})); + const tool = getTool(server, "evaluate"); + expect(parseInput(tool, { expression: "1+1" }).success).toBe(false); + expect(parseInput(tool, { surface: "main" }).success).toBe(false); + expect( + parseInput(tool, { surface: "main", expression: "1+1" }).success, + ).toBe(true); + expect( + parseInput(tool, { + surface: "main", + expression: "x", + timeoutMs: 0, + }).success, + ).toBe(false); + expect( + parseInput(tool, { + surface: "main", + expression: "x", + timeoutMs: 99_999, + }).success, + ).toBe(false); + }); + + it("returns the evaluated value as text when no exception is thrown", async () => { + const server = createFakeMcpServer(); + const wc = createFakeWebContents({ + cdp: { + "Runtime.evaluate": (params: Record) => { + expect(params).toMatchObject({ + expression: "1+1", + returnByValue: true, + }); + return { result: { type: "number", value: 2 } }; + }, + }, + }); + const win = createFakeBrowserWindow({ webContents: wc }); + registerEvaluate(server.asMcpServer, () => ({ + eval_main: win.asBrowserWindow, + })); + const tool = getTool(server, "evaluate"); + const result = (await tool.handler({ + surface: "eval_main", + expression: "1+1", + })) as { content: Array<{ text: string }> }; + expect(result.content[0].text).toBe("2"); + }); + + it("returns isError when the renderer throws", async () => { + const server = createFakeMcpServer(); + const wc = createFakeWebContents({ + cdp: { + "Runtime.evaluate": () => ({ + result: { type: "object" }, + exceptionDetails: { + exceptionId: 1, + text: "boom", + lineNumber: 4, + columnNumber: 12, + exception: { description: "ReferenceError: foo is not defined" }, + }, + }), + }, + }); + const win = createFakeBrowserWindow({ webContents: wc }); + registerEvaluate(server.asMcpServer, () => ({ + eval_err: win.asBrowserWindow, + })); + const tool = getTool(server, "evaluate"); + const result = (await tool.handler({ + surface: "eval_err", + expression: "foo()", + })) as { isError: boolean; content: Array<{ text: string }> }; + expect(result.isError).toBe(true); + expect(result.content[0].text).toMatch(/4:12/); + expect(result.content[0].text).toMatch(/ReferenceError/); + }); +}); diff --git a/src/tools/evaluate.ts b/src/tools/evaluate.ts index 0358572..a2d74ae 100644 --- a/src/tools/evaluate.ts +++ b/src/tools/evaluate.ts @@ -3,8 +3,8 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; -import { getOrAttachSession } from "../cdp.js"; -import type { SurfaceGetter } from "../surfaces.js"; +import { getOrAttachSession } from "../cdp"; +import type { SurfaceGetter } from "../surfaces"; const inputSchema = { surface: z @@ -45,7 +45,6 @@ interface RuntimeEvaluateResult { subtype?: string; value?: unknown; description?: string; - unserializableValue?: string; }; exceptionDetails?: { exceptionId: number; @@ -68,7 +67,7 @@ export function registerEvaluate( "Run JavaScript in the specified surface's renderer main world via " + "CDP Runtime.evaluate. Returns the value (JSON-serialized) or an " + "exception description. Main-world scope: DOM, React, Zustand, and " + - "window.nebula are reachable; Node/Electron APIs are not.", + "the app's preload bridge are reachable; Node/Electron APIs are not.", inputSchema, }, async ({ @@ -104,14 +103,10 @@ export function registerEvaluate( }; } - // CDP reports `NaN`, `Infinity`, `-0`, bigints, etc. via - // `unserializableValue` rather than `value` — fall back to it - // before the generic `description`/`type` so the agent sees the - // actual value instead of a "number" placeholder. - const { value, unserializableValue, description, type } = res.result; + const value = res.result.value; const text = value === undefined - ? (unserializableValue ?? description ?? String(type)) + ? (res.result.description ?? String(res.result.type)) : typeof value === "string" ? value : JSON.stringify(value, null, 2); diff --git a/src/tools/fill-form.test.ts b/src/tools/fill-form.test.ts new file mode 100644 index 0000000..d6b99b5 --- /dev/null +++ b/src/tools/fill-form.test.ts @@ -0,0 +1,122 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { detachAll } from "../cdp"; +import { + createFakeBrowserWindow, + createFakeMcpServer, + createFakeWebContents, + getTool, + parseInput, +} from "../testing"; +import { registerFillForm } from "./fill-form"; + +afterEach(() => detachAll()); + +describe("fill_form", () => { + it("validates surface + at-least-one field", () => { + const server = createFakeMcpServer(); + registerFillForm(server.asMcpServer, () => ({})); + const tool = getTool(server, "fill_form"); + expect(parseInput(tool, { surface: "main", fields: [] }).success).toBe( + false, + ); + expect( + parseInput(tool, { + surface: "main", + fields: [{ selector: "#a", value: "1" }], + }).success, + ).toBe(true); + expect( + parseInput(tool, { + surface: "main", + fields: [{ selector: "", value: "1" }], + }).success, + ).toBe(false); + }); + + it("fills each field in order via insertText", async () => { + const server = createFakeMcpServer(); + // Sequence of Runtime.evaluate calls per field: + // 1) waitForSelector → rect + // 2) focus assertion → { ok: true } + // 3) clearFirst select → { ok: true } + let evalCall = 0; + const wc = createFakeWebContents({ + cdp: { + "Runtime.evaluate": () => { + evalCall += 1; + const phase = ((evalCall - 1) % 3) + 1; + if (phase === 1) { + return { + result: { + type: "object", + value: { x: 0, y: 0, width: 60, height: 24 }, + }, + }; + } + return { + result: { type: "object", value: { ok: true } }, + }; + }, + }, + }); + const win = createFakeBrowserWindow({ visible: false, webContents: wc }); + registerFillForm(server.asMcpServer, () => ({ + ff_main: win.asBrowserWindow, + })); + const tool = getTool(server, "fill_form"); + const result = (await tool.handler({ + surface: "ff_main", + fields: [ + { selector: "#email", value: "a@b" }, + { selector: "#name", value: "Ada" }, + ], + })) as { structuredContent: { filledCount: number } }; + expect(result.structuredContent.filledCount).toBe(2); + const inserts = wc.cdpCalls + .filter((c) => c.method === "Input.insertText") + .map((c) => c.params?.text); + expect(inserts).toEqual(["a@b", "Ada"]); + }); + + it("fails fast with the failing field index when focus assertion returns ok=false", async () => { + const server = createFakeMcpServer(); + let evalCall = 0; + const wc = createFakeWebContents({ + cdp: { + "Runtime.evaluate": () => { + evalCall += 1; + if (evalCall === 1) { + return { + result: { + type: "object", + value: { x: 0, y: 0, width: 60, height: 24 }, + }, + }; + } + // Focus did not land on the target. + return { + result: { + type: "object", + value: { ok: false, reason: "intercepted by overlay" }, + }, + }; + }, + }, + }); + const win = createFakeBrowserWindow({ webContents: wc }); + registerFillForm(server.asMcpServer, () => ({ + ff_err: win.asBrowserWindow, + })); + const tool = getTool(server, "fill_form"); + const result = (await tool.handler({ + surface: "ff_err", + fields: [{ selector: "#blocked", value: "x" }], + })) as { + isError: boolean; + structuredContent: { failedIndex: number; filledCount: number }; + }; + expect(result.isError).toBe(true); + expect(result.structuredContent.failedIndex).toBe(0); + expect(result.structuredContent.filledCount).toBe(0); + }); +}); diff --git a/src/tools/fill-form.ts b/src/tools/fill-form.ts index 37f22ab..90d9f10 100644 --- a/src/tools/fill-form.ts +++ b/src/tools/fill-form.ts @@ -1,8 +1,8 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; -import { type CdpSession, getOrAttachSession } from "../cdp.js"; -import { waitForSelector } from "../cdp-helpers.js"; -import { resolveSurface, type SurfaceGetter } from "../surfaces.js"; +import { type CdpSession, getOrAttachSession } from "../cdp"; +import { waitForSelector } from "../cdp-helpers"; +import { resolveSurface, type SurfaceGetter } from "../surfaces"; interface RuntimeEvaluateAssertResult { result: { value?: { ok: boolean; reason?: string } }; diff --git a/src/tools/focus-surface.test.ts b/src/tools/focus-surface.test.ts new file mode 100644 index 0000000..2729675 --- /dev/null +++ b/src/tools/focus-surface.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, it } from "vitest"; +import { + createFakeBrowserWindow, + createFakeMcpServer, + getTool, + parseInput, +} from "../testing"; +import { registerFocusSurface } from "./focus-surface"; + +describe("focus_surface", () => { + it("requires a non-empty surface key", () => { + const server = createFakeMcpServer(); + registerFocusSurface(server.asMcpServer, () => ({})); + const tool = getTool(server, "focus_surface"); + expect(parseInput(tool, {}).success).toBe(false); + expect(parseInput(tool, { surface: "" }).success).toBe(false); + expect(parseInput(tool, { surface: "main" }).success).toBe(true); + }); + + it("focuses a focusable window", async () => { + const server = createFakeMcpServer(); + const win = createFakeBrowserWindow({ + visible: true, + focused: false, + focusable: true, + }); + registerFocusSurface(server.asMcpServer, () => ({ + main: win.asBrowserWindow, + })); + const tool = getTool(server, "focus_surface"); + const result = (await tool.handler({ surface: "main" })) as { + isError?: boolean; + structuredContent: { focused: boolean; focusable: boolean }; + }; + expect(result.isError).toBeUndefined(); + expect(win.focusCalls).toBe(1); + expect(result.structuredContent).toMatchObject({ + focused: true, + focusable: true, + }); + }); + + it("returns an error result without calling focus() when non-focusable", async () => { + const server = createFakeMcpServer(); + const win = createFakeBrowserWindow({ focusable: false }); + registerFocusSurface(server.asMcpServer, () => ({ + main: win.asBrowserWindow, + })); + const tool = getTool(server, "focus_surface"); + const result = (await tool.handler({ surface: "main" })) as { + isError: boolean; + structuredContent: { focused: boolean; focusable: boolean }; + }; + expect(result.isError).toBe(true); + expect(win.focusCalls).toBe(0); + expect(result.structuredContent).toMatchObject({ + focused: false, + focusable: false, + }); + }); +}); diff --git a/src/tools/focus-surface.ts b/src/tools/focus-surface.ts index b022bb4..139b067 100644 --- a/src/tools/focus-surface.ts +++ b/src/tools/focus-surface.ts @@ -1,6 +1,6 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; -import { resolveSurface, type SurfaceGetter } from "../surfaces.js"; +import { resolveSurface, type SurfaceGetter } from "../surfaces"; const inputSchema = { surface: z.string().min(1).describe("Which surface to focus."), diff --git a/src/tools/hide-surface.test.ts b/src/tools/hide-surface.test.ts new file mode 100644 index 0000000..6086477 --- /dev/null +++ b/src/tools/hide-surface.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from "vitest"; +import { + createFakeBrowserWindow, + createFakeMcpServer, + getTool, + parseInput, +} from "../testing"; +import { registerHideSurface } from "./hide-surface"; + +describe("hide_surface", () => { + it("requires a non-empty surface key", () => { + const server = createFakeMcpServer(); + registerHideSurface(server.asMcpServer, () => ({})); + const tool = getTool(server, "hide_surface"); + expect(parseInput(tool, {}).success).toBe(false); + expect(parseInput(tool, { surface: "" }).success).toBe(false); + expect(parseInput(tool, { surface: "popover" }).success).toBe(true); + }); + + it("hides the resolved window", async () => { + const server = createFakeMcpServer(); + const win = createFakeBrowserWindow({ visible: true }); + registerHideSurface(server.asMcpServer, () => ({ + popover: win.asBrowserWindow, + })); + + const tool = getTool(server, "hide_surface"); + const result = (await tool.handler({ surface: "popover" })) as { + structuredContent: { surface: string; visible: boolean }; + }; + expect(win.hideCalls).toBe(1); + expect(result.structuredContent).toEqual({ + surface: "popover", + visible: false, + }); + }); + + it("throws when the surface is destroyed", async () => { + const server = createFakeMcpServer(); + const win = createFakeBrowserWindow(); + win.__destroy(); + registerHideSurface(server.asMcpServer, () => ({ + popover: win.asBrowserWindow, + })); + const tool = getTool(server, "hide_surface"); + await expect(tool.handler({ surface: "popover" })).rejects.toThrow( + /surface "popover" is not available/, + ); + }); +}); diff --git a/src/tools/hide-surface.ts b/src/tools/hide-surface.ts index f7aa5f9..0a0e136 100644 --- a/src/tools/hide-surface.ts +++ b/src/tools/hide-surface.ts @@ -1,6 +1,6 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; -import { resolveSurface, type SurfaceGetter } from "../surfaces.js"; +import { resolveSurface, type SurfaceGetter } from "../surfaces"; const inputSchema = { surface: z.string().min(1).describe("Which surface to hide."), diff --git a/src/tools/hover.test.ts b/src/tools/hover.test.ts new file mode 100644 index 0000000..162ddbc --- /dev/null +++ b/src/tools/hover.test.ts @@ -0,0 +1,82 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { detachAll } from "../cdp"; +import { + createFakeBrowserWindow, + createFakeMcpServer, + createFakeWebContents, + getTool, + parseInput, +} from "../testing"; +import { registerHover } from "./hover"; + +afterEach(() => detachAll()); + +describe("hover", () => { + it("requires non-empty surface + selector", () => { + const server = createFakeMcpServer(); + registerHover(server.asMcpServer, () => ({})); + const tool = getTool(server, "hover"); + expect(parseInput(tool, { surface: "main" }).success).toBe(false); + expect(parseInput(tool, { selector: "#go" }).success).toBe(false); + expect(parseInput(tool, { surface: "main", selector: "" }).success).toBe( + false, + ); + expect(parseInput(tool, { surface: "main", selector: "#go" }).success).toBe( + true, + ); + }); + + it("dispatches mouseMoved at the resolved center", async () => { + const server = createFakeMcpServer(); + const wc = createFakeWebContents({ + cdp: { + "Runtime.evaluate": { + result: { + type: "object", + value: { x: 10, y: 10, width: 80, height: 40 }, + }, + }, + }, + }); + const win = createFakeBrowserWindow({ visible: false, webContents: wc }); + registerHover(server.asMcpServer, () => ({ + hov_main: win.asBrowserWindow, + })); + const tool = getTool(server, "hover"); + const result = (await tool.handler({ + surface: "hov_main", + selector: ".tip", + })) as { + structuredContent: { rect: { centerX: number; centerY: number } }; + }; + expect(win.showCalls).toBe(1); // hidden window auto-shown + expect(result.structuredContent.rect.centerX).toBe(50); + expect(result.structuredContent.rect.centerY).toBe(30); + const moves = wc.cdpCalls.filter( + (c) => c.method === "Input.dispatchMouseEvent", + ); + expect(moves[0].params).toMatchObject({ type: "mouseMoved", x: 50, y: 30 }); + }); + + it("times out when the selector never resolves to a sized element", async () => { + const server = createFakeMcpServer(); + const wc = createFakeWebContents({ + cdp: { + // Always returns null — never resolves a rect. + "Runtime.evaluate": { result: { type: "object", value: null } }, + }, + }); + const win = createFakeBrowserWindow({ webContents: wc }); + registerHover(server.asMcpServer, () => ({ + hov_err: win.asBrowserWindow, + })); + const tool = getTool(server, "hover"); + await expect( + tool.handler({ + surface: "hov_err", + selector: ".missing", + timeoutMs: 30, + }), + ).rejects.toThrow(/selector did not resolve/); + }); +}); diff --git a/src/tools/hover.ts b/src/tools/hover.ts index 7e0753c..4e95b0e 100644 --- a/src/tools/hover.ts +++ b/src/tools/hover.ts @@ -1,8 +1,8 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; -import { getOrAttachSession } from "../cdp.js"; -import { waitForSelector } from "../cdp-helpers.js"; -import { resolveSurface, type SurfaceGetter } from "../surfaces.js"; +import { getOrAttachSession } from "../cdp"; +import { waitForSelector } from "../cdp-helpers"; +import { resolveSurface, type SurfaceGetter } from "../surfaces"; const inputSchema = { surface: z.string().min(1).describe("Which surface to hover in."), diff --git a/src/tools/index.ts b/src/tools/index.ts index 61a615c..2619327 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -1,24 +1,22 @@ -// Generic MCP tool barrel — surface-agnostic, no app-specific -// callbacks. App-specific tools (`trigger_hotkey`, `trigger_tray_click`) -// live under `../app-tools/` and are registered by the consumer. +// Generic MCP tool barrel: surface-agnostic, no app-specific callbacks. import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { SurfaceGetter } from "../surfaces.js"; -import { registerAxSnapshot } from "./ax-snapshot.js"; -import { registerClick } from "./click.js"; -import { registerEvaluate } from "./evaluate.js"; -import { registerFillForm } from "./fill-form.js"; -import { registerFocusSurface } from "./focus-surface.js"; -import { registerHideSurface } from "./hide-surface.js"; -import { registerHover } from "./hover.js"; -import { registerListSurfaces } from "./list-surfaces.js"; -import { registerPressKey } from "./press-key.js"; -import { registerQueryDom } from "./query-dom.js"; -import { registerReloadSurface } from "./reload-surface.js"; -import { registerScreenshot } from "./screenshot.js"; -import { registerShowSurface } from "./show-surface.js"; -import { registerTypeText } from "./type-text.js"; -import { registerWaitForLoad } from "./wait-for-load.js"; +import type { SurfaceGetter } from "../surfaces"; +import { registerAxSnapshot } from "./ax-snapshot"; +import { registerClick } from "./click"; +import { registerEvaluate } from "./evaluate"; +import { registerFillForm } from "./fill-form"; +import { registerFocusSurface } from "./focus-surface"; +import { registerHideSurface } from "./hide-surface"; +import { registerHover } from "./hover"; +import { registerListSurfaces } from "./list-surfaces"; +import { registerPressKey } from "./press-key"; +import { registerQueryDom } from "./query-dom"; +import { registerReloadSurface } from "./reload-surface"; +import { registerScreenshot } from "./screenshot"; +import { registerShowSurface } from "./show-surface"; +import { registerTypeText } from "./type-text"; +import { registerWaitForLoad } from "./wait-for-load"; interface RegisterAllToolsOptions { getSurfaces: SurfaceGetter; diff --git a/src/tools/list-surfaces.test.ts b/src/tools/list-surfaces.test.ts new file mode 100644 index 0000000..ffeead1 --- /dev/null +++ b/src/tools/list-surfaces.test.ts @@ -0,0 +1,69 @@ +import { describe, expect, it } from "vitest"; +import { + createFakeBrowserWindow, + createFakeMcpServer, + getTool, +} from "../testing"; +import { registerListSurfaces } from "./list-surfaces"; + +describe("list_surfaces", () => { + it("registers a tool with no input schema", () => { + const server = createFakeMcpServer(); + registerListSurfaces(server.asMcpServer, () => ({})); + const tool = getTool(server, "list_surfaces"); + expect(tool.config.inputSchema).toBeUndefined(); + expect(tool.config.title).toBe("List surfaces"); + }); + + it("describes every consumer-defined surface, sorted by key", async () => { + const server = createFakeMcpServer(); + const main = createFakeBrowserWindow({ + visible: true, + focused: true, + bounds: { x: 10, y: 20, width: 300, height: 200 }, + }); + const settings = createFakeBrowserWindow({ visible: false }); + registerListSurfaces(server.asMcpServer, () => ({ + // order chosen to verify the impl sorts. + settings: settings.asBrowserWindow, + main: main.asBrowserWindow, + })); + + const tool = getTool(server, "list_surfaces"); + const result = (await tool.handler({})) as { + structuredContent: { surfaces: Array<{ surface: string }> }; + }; + expect(result.structuredContent.surfaces.map((s) => s.surface)).toEqual([ + "main", + "settings", + ]); + expect(result.structuredContent.surfaces[0]).toMatchObject({ + surface: "main", + present: true, + visible: true, + focused: true, + bounds: { x: 10, y: 20, width: 300, height: 200 }, + }); + }); + + it("reports destroyed or null surfaces as { present: false }", async () => { + const server = createFakeMcpServer(); + const dead = createFakeBrowserWindow(); + dead.__destroy(); + registerListSurfaces(server.asMcpServer, () => ({ + dead: dead.asBrowserWindow, + missing: null, + })); + + const tool = getTool(server, "list_surfaces"); + const result = (await tool.handler({})) as { + structuredContent: { + surfaces: Array<{ surface: string; present: boolean }>; + }; + }; + expect(result.structuredContent.surfaces).toEqual([ + { surface: "dead", present: false }, + { surface: "missing", present: false }, + ]); + }); +}); diff --git a/src/tools/list-surfaces.ts b/src/tools/list-surfaces.ts index 41ba6ea..770b04e 100644 --- a/src/tools/list-surfaces.ts +++ b/src/tools/list-surfaces.ts @@ -3,7 +3,7 @@ // "missing" instead of silently inferring it. import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { SurfaceGetter } from "../surfaces.js"; +import type { SurfaceGetter } from "../surfaces"; export function registerListSurfaces( server: McpServer, diff --git a/src/tools/press-key.test.ts b/src/tools/press-key.test.ts new file mode 100644 index 0000000..06b3000 --- /dev/null +++ b/src/tools/press-key.test.ts @@ -0,0 +1,71 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { detachAll } from "../cdp"; +import { + createFakeBrowserWindow, + createFakeMcpServer, + createFakeWebContents, + getTool, + parseInput, +} from "../testing"; +import { registerPressKey } from "./press-key"; + +afterEach(() => detachAll()); + +describe("press_key", () => { + it("validates surface + key and modifier whitelist", () => { + const server = createFakeMcpServer(); + registerPressKey(server.asMcpServer, () => ({})); + const tool = getTool(server, "press_key"); + expect(parseInput(tool, { surface: "main", key: "Enter" }).success).toBe( + true, + ); + expect( + parseInput(tool, { + surface: "main", + key: "a", + modifiers: ["cmd", "shift"], + }).success, + ).toBe(true); + expect( + parseInput(tool, { + surface: "main", + key: "a", + modifiers: ["super"], + }).success, + ).toBe(false); + expect(parseInput(tool, { surface: "main", key: "" }).success).toBe(false); + }); + + it("dispatches keyDown + keyUp with normalised modifiers", async () => { + const server = createFakeMcpServer(); + const wc = createFakeWebContents({}); + const win = createFakeBrowserWindow({ webContents: wc }); + registerPressKey(server.asMcpServer, () => ({ + press_main: win.asBrowserWindow, + })); + const tool = getTool(server, "press_key"); + const result = (await tool.handler({ + surface: "press_main", + key: "Enter", + modifiers: ["cmd", "command"], // duplicate alias collapses to `cmd` + })) as { structuredContent: { modifiers: string[] } }; + expect(result.structuredContent.modifiers).toEqual(["cmd"]); + const types = wc.cdpCalls + .filter((c) => c.method === "Input.dispatchKeyEvent") + .map((c) => c.params?.type); + expect(types).toEqual(["keyDown", "keyUp"]); + }); + + it("throws on unsupported keys", async () => { + const server = createFakeMcpServer(); + const wc = createFakeWebContents({}); + const win = createFakeBrowserWindow({ webContents: wc }); + registerPressKey(server.asMcpServer, () => ({ + press_err: win.asBrowserWindow, + })); + const tool = getTool(server, "press_key"); + await expect( + tool.handler({ surface: "press_err", key: "F13" }), + ).rejects.toThrow(/unsupported key/); + }); +}); diff --git a/src/tools/press-key.ts b/src/tools/press-key.ts index bee0dad..711e5e0 100644 --- a/src/tools/press-key.ts +++ b/src/tools/press-key.ts @@ -1,8 +1,8 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; -import { getOrAttachSession } from "../cdp.js"; -import { type KeyModifier, keyToCdp, modifiersToMask } from "../cdp-helpers.js"; -import type { SurfaceGetter } from "../surfaces.js"; +import { getOrAttachSession } from "../cdp"; +import { type KeyModifier, keyToCdp, modifiersToMask } from "../cdp-helpers"; +import type { SurfaceGetter } from "../surfaces"; const MODIFIER_VALUES = [ "alt", diff --git a/src/tools/query-dom.test.ts b/src/tools/query-dom.test.ts new file mode 100644 index 0000000..a765a2e --- /dev/null +++ b/src/tools/query-dom.test.ts @@ -0,0 +1,105 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { detachAll } from "../cdp"; +import { + createFakeBrowserWindow, + createFakeMcpServer, + createFakeWebContents, + getTool, + parseInput, +} from "../testing"; +import { registerQueryDom } from "./query-dom"; + +afterEach(() => detachAll()); + +describe("query_dom", () => { + it("validates surface + selector and bounds limit/attrs", () => { + const server = createFakeMcpServer(); + registerQueryDom(server.asMcpServer, () => ({})); + const tool = getTool(server, "query_dom"); + expect(parseInput(tool, { surface: "main" }).success).toBe(false); + expect(parseInput(tool, { surface: "main", selector: "" }).success).toBe( + false, + ); + expect( + parseInput(tool, { surface: "main", selector: "*", limit: 0 }).success, + ).toBe(false); + expect( + parseInput(tool, { surface: "main", selector: "*", limit: 201 }).success, + ).toBe(false); + expect( + parseInput(tool, { + surface: "main", + selector: "*", + attrs: new Array(21).fill("a"), + }).success, + ).toBe(false); + expect(parseInput(tool, { surface: "main", selector: "*" }).success).toBe( + true, + ); + }); + + it("returns matches and reports truncation when totalFound > matches.length", async () => { + const server = createFakeMcpServer(); + const matches = [ + { + tag: "button", + text: "Go", + attrs: { id: "go" }, + rect: { x: 0, y: 0, width: 40, height: 20 }, + visible: true, + }, + ]; + const wc = createFakeWebContents({ + cdp: { + "Runtime.evaluate": { + result: { type: "object", value: { totalFound: 5, matches } }, + }, + }, + }); + const win = createFakeBrowserWindow({ webContents: wc }); + registerQueryDom(server.asMcpServer, () => ({ + query_main: win.asBrowserWindow, + })); + const tool = getTool(server, "query_dom"); + const result = (await tool.handler({ + surface: "query_main", + selector: "button", + limit: 1, + })) as { + structuredContent: { + matches: unknown[]; + totalFound: number; + truncated: boolean; + }; + }; + expect(result.structuredContent.totalFound).toBe(5); + expect(result.structuredContent.matches.length).toBe(1); + expect(result.structuredContent.truncated).toBe(true); + }); + + it("returns isError when the renderer evaluate throws", async () => { + const server = createFakeMcpServer(); + const wc = createFakeWebContents({ + cdp: { + "Runtime.evaluate": { + result: { type: "object" }, + exceptionDetails: { + text: "DOMException: bad selector", + exception: { description: "DOMException: bad selector" }, + }, + }, + }, + }); + const win = createFakeBrowserWindow({ webContents: wc }); + registerQueryDom(server.asMcpServer, () => ({ + query_err: win.asBrowserWindow, + })); + const tool = getTool(server, "query_dom"); + const result = (await tool.handler({ + surface: "query_err", + selector: "::bad", + })) as { isError: boolean; content: Array<{ text: string }> }; + expect(result.isError).toBe(true); + expect(result.content[0].text).toMatch(/DOMException/); + }); +}); diff --git a/src/tools/query-dom.ts b/src/tools/query-dom.ts index 3e280d5..27d7096 100644 --- a/src/tools/query-dom.ts +++ b/src/tools/query-dom.ts @@ -3,8 +3,8 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; -import { getOrAttachSession } from "../cdp.js"; -import type { SurfaceGetter } from "../surfaces.js"; +import { getOrAttachSession } from "../cdp"; +import type { SurfaceGetter } from "../surfaces"; const inputSchema = { surface: z.string().min(1).describe("Which surface to query."), diff --git a/src/tools/reload-surface.test.ts b/src/tools/reload-surface.test.ts new file mode 100644 index 0000000..658008c --- /dev/null +++ b/src/tools/reload-surface.test.ts @@ -0,0 +1,66 @@ +import { describe, expect, it } from "vitest"; +import { + createFakeBrowserWindow, + createFakeMcpServer, + createFakeWebContents, + getTool, + parseInput, +} from "../testing"; +import { registerReloadSurface } from "./reload-surface"; + +describe("reload_surface", () => { + it("validates surface and timeout bounds", () => { + const server = createFakeMcpServer(); + registerReloadSurface(server.asMcpServer, () => ({})); + const tool = getTool(server, "reload_surface"); + expect(parseInput(tool, {}).success).toBe(false); + expect(parseInput(tool, { surface: "main" }).success).toBe(true); + expect( + parseInput(tool, { surface: "main", ignoreCache: true }).success, + ).toBe(true); + expect( + parseInput(tool, { surface: "main", timeoutMs: 60_001 }).success, + ).toBe(false); + }); + + it("calls reload() and resolves on did-finish-load", async () => { + const server = createFakeMcpServer(); + const wc = createFakeWebContents({ url: "http://app/" }); + const win = createFakeBrowserWindow({ webContents: wc }); + registerReloadSurface(server.asMcpServer, () => ({ + reload_main: win.asBrowserWindow, + })); + const tool = getTool(server, "reload_surface"); + + const handler = tool.handler({ surface: "reload_main" }); + // Yield so subscriptions register before we emit the load completion. + await Promise.resolve(); + wc.emit("did-finish-load"); + const result = (await handler) as { + structuredContent: { ignoreCache: boolean; url: string }; + }; + expect(wc.reload).toHaveBeenCalledTimes(1); + expect(wc.reloadIgnoringCache).not.toHaveBeenCalled(); + expect(result.structuredContent.ignoreCache).toBe(false); + expect(result.structuredContent.url).toBe("http://app/"); + }); + + it("uses reloadIgnoringCache when ignoreCache=true and rejects on did-fail-load", async () => { + const server = createFakeMcpServer(); + const wc = createFakeWebContents(); + const win = createFakeBrowserWindow({ webContents: wc }); + registerReloadSurface(server.asMcpServer, () => ({ + reload_err: win.asBrowserWindow, + })); + const tool = getTool(server, "reload_surface"); + + const handler = tool.handler({ + surface: "reload_err", + ignoreCache: true, + }); + await Promise.resolve(); + wc.emit("did-fail-load", {}, -105, "ADDRESS_UNREACHABLE", "x", true); + await expect(handler).rejects.toThrow(/ADDRESS_UNREACHABLE/); + expect(wc.reloadIgnoringCache).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/tools/reload-surface.ts b/src/tools/reload-surface.ts index fa9d989..e181383 100644 --- a/src/tools/reload-surface.ts +++ b/src/tools/reload-surface.ts @@ -1,7 +1,7 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; -import { resolveSurface, type SurfaceGetter } from "../surfaces.js"; -import { awaitNextLoad } from "./wait-for-load.js"; +import { resolveSurface, type SurfaceGetter } from "../surfaces"; +import { awaitNextLoad } from "./wait-for-load"; const inputSchema = { surface: z.string().min(1).describe("Which surface to reload."), diff --git a/src/tools/screenshot.test.ts b/src/tools/screenshot.test.ts new file mode 100644 index 0000000..ee463be --- /dev/null +++ b/src/tools/screenshot.test.ts @@ -0,0 +1,76 @@ +import { describe, expect, it } from "vitest"; +import { + createFakeBrowserWindow, + createFakeImage, + createFakeMcpServer, + createFakeWebContents, + getTool, + parseInput, +} from "../testing"; +import { registerScreenshot } from "./screenshot"; + +describe("screenshot", () => { + it("validates surface and optional rect", () => { + const server = createFakeMcpServer(); + registerScreenshot(server.asMcpServer, () => ({})); + const tool = getTool(server, "screenshot"); + expect(parseInput(tool, {}).success).toBe(false); + expect(parseInput(tool, { surface: "" }).success).toBe(false); + expect(parseInput(tool, { surface: "main" }).success).toBe(true); + expect( + parseInput(tool, { + surface: "main", + rect: { x: 0, y: 0, width: 0, height: 10 }, + }).success, + ).toBe(false); + expect( + parseInput(tool, { + surface: "main", + rect: { x: 0, y: 0, width: 100, height: 50 }, + }).success, + ).toBe(true); + }); + + it("returns the captured PNG as base64 image content", async () => { + const server = createFakeMcpServer(); + const png = Buffer.from([0x89, 0x50, 0x4e, 0x47]); + const wc = createFakeWebContents({ + capturePage: async () => + createFakeImage({ png, width: 200, height: 100 }), + }); + const win = createFakeBrowserWindow({ webContents: wc }); + registerScreenshot(server.asMcpServer, () => ({ + main: win.asBrowserWindow, + })); + const tool = getTool(server, "screenshot"); + const result = (await tool.handler({ surface: "main" })) as { + content: Array<{ type: string; data?: string }>; + structuredContent: { width: number; height: number; byteLength: number }; + }; + expect(result.content[0].type).toBe("image"); + expect(result.content[0].data).toBe(png.toString("base64")); + expect(result.structuredContent).toMatchObject({ + width: 200, + height: 100, + byteLength: png.byteLength, + }); + }); + + it("returns isError when the captured image is empty", async () => { + const server = createFakeMcpServer(); + const wc = createFakeWebContents({ + capturePage: async () => createFakeImage({ empty: true }), + }); + const win = createFakeBrowserWindow({ webContents: wc }); + registerScreenshot(server.asMcpServer, () => ({ + main: win.asBrowserWindow, + })); + const tool = getTool(server, "screenshot"); + const result = (await tool.handler({ surface: "main" })) as { + isError: boolean; + content: Array<{ type: string; text: string }>; + }; + expect(result.isError).toBe(true); + expect(result.content[0].text).toMatch(/empty image/); + }); +}); diff --git a/src/tools/screenshot.ts b/src/tools/screenshot.ts index 2b48515..483687d 100644 --- a/src/tools/screenshot.ts +++ b/src/tools/screenshot.ts @@ -4,7 +4,7 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; -import { resolveSurface, type SurfaceGetter } from "../surfaces.js"; +import { resolveSurface, type SurfaceGetter } from "../surfaces"; const rectSchema = z.object({ x: z.number().int().nonnegative(), diff --git a/src/tools/show-surface.test.ts b/src/tools/show-surface.test.ts new file mode 100644 index 0000000..9a5ccb1 --- /dev/null +++ b/src/tools/show-surface.test.ts @@ -0,0 +1,69 @@ +import { describe, expect, it } from "vitest"; +import { + createFakeBrowserWindow, + createFakeMcpServer, + getTool, + parseInput, +} from "../testing"; +import { registerShowSurface } from "./show-surface"; + +describe("show_surface", () => { + it("rejects missing/empty surface and accepts arbitrary string keys", () => { + const server = createFakeMcpServer(); + registerShowSurface(server.asMcpServer, () => ({})); + const tool = getTool(server, "show_surface"); + + expect(parseInput(tool, {}).success).toBe(false); + expect(parseInput(tool, { surface: "" }).success).toBe(false); + expect(parseInput(tool, { surface: "main" }).success).toBe(true); + expect( + parseInput(tool, { surface: "settings", focus: false }).success, + ).toBe(true); + }); + + it("shows and focuses the resolved window by default", async () => { + const server = createFakeMcpServer(); + const win = createFakeBrowserWindow({ visible: false, focused: false }); + registerShowSurface(server.asMcpServer, () => ({ + preview: win.asBrowserWindow, + })); + + const tool = getTool(server, "show_surface"); + const result = (await tool.handler({ surface: "preview" })) as { + structuredContent: { + surface: string; + visible: boolean; + focused: boolean; + }; + }; + expect(win.showCalls).toBe(1); + expect(win.focusCalls).toBe(1); + expect(result.structuredContent).toMatchObject({ + surface: "preview", + visible: true, + focused: true, + }); + }); + + it("skips focus when focus=false", async () => { + const server = createFakeMcpServer(); + const win = createFakeBrowserWindow({ visible: false, focused: false }); + registerShowSurface(server.asMcpServer, () => ({ + preview: win.asBrowserWindow, + })); + + const tool = getTool(server, "show_surface"); + await tool.handler({ surface: "preview", focus: false }); + expect(win.showCalls).toBe(1); + expect(win.focusCalls).toBe(0); + }); + + it("throws when the surface is not registered", async () => { + const server = createFakeMcpServer(); + registerShowSurface(server.asMcpServer, () => ({})); + const tool = getTool(server, "show_surface"); + await expect(tool.handler({ surface: "ghost" })).rejects.toThrow( + /surface "ghost" is not available/, + ); + }); +}); diff --git a/src/tools/show-surface.ts b/src/tools/show-surface.ts index b339589..65e6e3b 100644 --- a/src/tools/show-surface.ts +++ b/src/tools/show-surface.ts @@ -1,6 +1,6 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; -import { resolveSurface, type SurfaceGetter } from "../surfaces.js"; +import { resolveSurface, type SurfaceGetter } from "../surfaces"; const inputSchema = { surface: z.string().min(1).describe("Which surface to show."), diff --git a/src/tools/type-text.test.ts b/src/tools/type-text.test.ts new file mode 100644 index 0000000..b0cdfce --- /dev/null +++ b/src/tools/type-text.test.ts @@ -0,0 +1,81 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { detachAll } from "../cdp"; +import { + createFakeBrowserWindow, + createFakeMcpServer, + createFakeWebContents, + getTool, + parseInput, +} from "../testing"; +import { registerTypeText } from "./type-text"; + +afterEach(() => detachAll()); + +describe("type_text", () => { + it("validates surface + text and rejects empty selector", () => { + const server = createFakeMcpServer(); + registerTypeText(server.asMcpServer, () => ({})); + const tool = getTool(server, "type_text"); + expect(parseInput(tool, { surface: "main", text: "hi" }).success).toBe( + true, + ); + expect( + parseInput(tool, { surface: "main", text: "hi", selector: "" }).success, + ).toBe(false); + expect(parseInput(tool, { surface: "main" }).success).toBe(false); + expect(parseInput(tool, { text: "hi" }).success).toBe(false); + }); + + it("dispatches Input.insertText to the focused element when no selector is given", async () => { + const server = createFakeMcpServer(); + const wc = createFakeWebContents({ cdp: {} }); + const win = createFakeBrowserWindow({ webContents: wc }); + registerTypeText(server.asMcpServer, () => ({ + type_main: win.asBrowserWindow, + })); + const tool = getTool(server, "type_text"); + const result = (await tool.handler({ + surface: "type_main", + text: "hello", + })) as { structuredContent: { charsTyped: number } }; + expect(result.structuredContent.charsTyped).toBe(5); + expect(wc.cdpCalls).toEqual([ + { method: "Input.insertText", params: { text: "hello" } }, + ]); + }); + + it("throws when a selector is given but focus does not land on the element", async () => { + let evalCall = 0; + const server = createFakeMcpServer(); + const wc = createFakeWebContents({ + cdp: { + "Runtime.evaluate": () => { + evalCall += 1; + // First call: waitForSelector — return a visible rect. + if (evalCall === 1) { + return { + result: { + type: "object", + value: { x: 0, y: 0, width: 50, height: 20 }, + }, + }; + } + // Second call: focus assertion — return false (focus didn't land). + return { result: { type: "boolean", value: false } }; + }, + }, + }); + const win = createFakeBrowserWindow({ webContents: wc }); + registerTypeText(server.asMcpServer, () => ({ + type_err: win.asBrowserWindow, + })); + const tool = getTool(server, "type_text"); + await expect( + tool.handler({ + surface: "type_err", + text: "hi", + selector: "#no-focus", + }), + ).rejects.toThrow(/could not focus selector/); + }); +}); diff --git a/src/tools/type-text.ts b/src/tools/type-text.ts index b20c9fc..c686a73 100644 --- a/src/tools/type-text.ts +++ b/src/tools/type-text.ts @@ -1,8 +1,8 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; -import { getOrAttachSession } from "../cdp.js"; -import { waitForSelector } from "../cdp-helpers.js"; -import type { SurfaceGetter } from "../surfaces.js"; +import { getOrAttachSession } from "../cdp"; +import { waitForSelector } from "../cdp-helpers"; +import type { SurfaceGetter } from "../surfaces"; const inputSchema = { surface: z.string().min(1).describe("Which surface to type into."), diff --git a/src/tools/wait-for-load.test.ts b/src/tools/wait-for-load.test.ts new file mode 100644 index 0000000..0cc394d --- /dev/null +++ b/src/tools/wait-for-load.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, it } from "vitest"; +import { + createFakeBrowserWindow, + createFakeMcpServer, + createFakeWebContents, + getTool, + parseInput, +} from "../testing"; +import { registerWaitForLoad } from "./wait-for-load"; + +describe("wait_for_load", () => { + it("validates surface and bounds the timeout", () => { + const server = createFakeMcpServer(); + registerWaitForLoad(server.asMcpServer, () => ({})); + const tool = getTool(server, "wait_for_load"); + expect(parseInput(tool, {}).success).toBe(false); + expect(parseInput(tool, { surface: "main" }).success).toBe(true); + expect(parseInput(tool, { surface: "main", timeoutMs: 0 }).success).toBe( + false, + ); + expect( + parseInput(tool, { surface: "main", timeoutMs: 60_001 }).success, + ).toBe(false); + }); + + it("short-circuits when no load is in progress", async () => { + const server = createFakeMcpServer(); + const wc = createFakeWebContents({ isLoading: false }); + const win = createFakeBrowserWindow({ webContents: wc }); + registerWaitForLoad(server.asMcpServer, () => ({ + load_main: win.asBrowserWindow, + })); + const tool = getTool(server, "wait_for_load"); + const result = (await tool.handler({ surface: "load_main" })) as { + structuredContent: { status: string }; + }; + expect(result.structuredContent.status).toBe("already-loaded"); + }); + + it("rejects with a load-failure description when did-fail-load fires on the main frame", async () => { + const server = createFakeMcpServer(); + const wc = createFakeWebContents({ isLoading: true }); + const win = createFakeBrowserWindow({ webContents: wc }); + registerWaitForLoad(server.asMcpServer, () => ({ + load_err: win.asBrowserWindow, + })); + const tool = getTool(server, "wait_for_load"); + + const promise = tool.handler({ surface: "load_err" }); + // Trigger the failure synchronously after handler subscribed. + await Promise.resolve(); + wc.emit("did-fail-load", {}, -100, "DNS_FAILURE", "http://x", true); + await expect(promise).rejects.toThrow(/load failed.*DNS_FAILURE/); + }); +}); diff --git a/src/tools/wait-for-load.ts b/src/tools/wait-for-load.ts index 7b246b4..098c91c 100644 --- a/src/tools/wait-for-load.ts +++ b/src/tools/wait-for-load.ts @@ -5,7 +5,7 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import type { WebContents } from "electron"; import { z } from "zod"; -import { resolveSurface, type SurfaceGetter } from "../surfaces.js"; +import { resolveSurface, type SurfaceGetter } from "../surfaces"; const DEFAULT_TIMEOUT_MS = 10_000; diff --git a/src/types.ts b/src/types.ts index 977def0..f956715 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,10 +1,8 @@ -// Public types sub-export — `@nebula-agents/electron-mcp/types`. -// -// Consumers that build `ToolDef`s in a separate package (or want to -// strongly type the surface map their app exposes) can import from this -// path without pulling in the runtime entry. Kept to *types only* so -// the import is zero-cost at bundle time. - -export type { ElectronMcpServerHandle } from "./index.js"; -export type { SurfaceGetter, SurfaceMap } from "./surfaces.js"; -export type { ToolDef } from "./tool-def.js"; +export type { + ElectronMcpServerConfig, + ElectronMcpServerHandle, + McpLogger, + SurfaceGetter, + SurfaceMap, + ToolDef, +} from "./index"; diff --git a/test/electron/fixture/main.mjs b/test/electron/fixture/main.mjs new file mode 100644 index 0000000..fc0e01d --- /dev/null +++ b/test/electron/fixture/main.mjs @@ -0,0 +1,49 @@ +import { app, BrowserWindow } from "electron"; +import { createElectronMcpServer } from "../../../dist/index.mjs"; + +let mainWindow = null; +let mcp = null; + +app.whenReady().then(async () => { + mainWindow = new BrowserWindow({ + width: 640, + height: 420, + show: true, + webPreferences: { + contextIsolation: true, + nodeIntegration: false, + }, + }); + + await mainWindow.loadURL( + `data:text/html,${encodeURIComponent(` + + + electron-mcp smoke + +
+

Electron MCP Smoke

+ + +
+ + + `)}`, + ); + + mcp = createElectronMcpServer({ + getSurfaces: () => ({ main: mainWindow }), + port: 0, + }); + await mcp.start(); + globalThis.__electronMcpSmokeUrl = mcp.url; +}); + +app.on("before-quit", () => { + void mcp?.stop(); +}); diff --git a/test/electron/globals.d.ts b/test/electron/globals.d.ts new file mode 100644 index 0000000..5ec1df4 --- /dev/null +++ b/test/electron/globals.d.ts @@ -0,0 +1,5 @@ +declare global { + var __electronMcpSmokeUrl: string | undefined; +} + +export {}; diff --git a/test/electron/playwright.config.ts b/test/electron/playwright.config.ts new file mode 100644 index 0000000..b20782b --- /dev/null +++ b/test/electron/playwright.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "@playwright/test"; + +export default defineConfig({ + testDir: ".", + testMatch: ["*.spec.ts"], + reporter: "list", + timeout: 30_000, + use: { + trace: "retain-on-failure", + }, +}); diff --git a/test/electron/smoke.spec.ts b/test/electron/smoke.spec.ts new file mode 100644 index 0000000..2b40c6e --- /dev/null +++ b/test/electron/smoke.spec.ts @@ -0,0 +1,57 @@ +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; +import { _electron as electron, expect, test } from "@playwright/test"; + +test("drives a real Electron BrowserWindow through MCP", async () => { + test.skip( + process.platform === "darwin", + "Playwright _electron currently passes --remote-debugging-port=0, which Electron 41 rejects on macOS.", + ); + + const app = await electron.launch({ + args: ["test/electron/fixture/main.mjs"], + }); + + try { + const url = await app.evaluate(async () => { + for (let i = 0; i < 100; i += 1) { + const value = globalThis.__electronMcpSmokeUrl; + if (typeof value === "string") return value; + await new Promise((resolve) => setTimeout(resolve, 50)); + } + throw new Error("MCP server did not start"); + }); + + const transport = new StreamableHTTPClientTransport(new URL(url)); + const client = new Client({ name: "electron-smoke", version: "0.0.0" }); + try { + await client.connect(transport); + + const surfaces = await client.callTool({ + name: "list_surfaces", + arguments: {}, + }); + expect(JSON.stringify(surfaces.content)).toContain("main"); + + const evaluated = await client.callTool({ + name: "evaluate", + arguments: { + surface: "main", + expression: "document.querySelector('#title')?.textContent", + }, + }); + expect(JSON.stringify(evaluated.content)).toContain("Electron MCP Smoke"); + + const screenshot = await client.callTool({ + name: "screenshot", + arguments: { surface: "main" }, + }); + expect(JSON.stringify(screenshot.content)).toContain("image"); + } finally { + await client.close().catch(() => {}); + await transport.close().catch(() => {}); + } + } finally { + await app.close(); + } +}); diff --git a/tests/smoke.electron.test.ts b/tests/smoke.electron.test.ts index e748e84..a8a493e 100644 --- a/tests/smoke.electron.test.ts +++ b/tests/smoke.electron.test.ts @@ -17,7 +17,7 @@ import { fileURLToPath } from "node:url"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import { expect, test } from "@playwright/test"; -import { _electron as electron, type ElectronApplication } from "playwright"; +import { type ElectronApplication, _electron as electron } from "playwright"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -97,8 +97,9 @@ test("MCP server drives a real Electron surface end-to-end", async () => { arguments: { surface: "main", expression: "1 + 1" }, }); expect(evaluated.isError).toBeFalsy(); - const evalText = (evaluated.content as Array<{ type: string; text?: string }>) - .find((c) => c.type === "text")?.text; + const evalText = ( + evaluated.content as Array<{ type: string; text?: string }> + ).find((c) => c.type === "text")?.text; expect(evalText).toBe("2"); // --- screenshot ------------------------------------------------- @@ -107,8 +108,13 @@ test("MCP server drives a real Electron surface end-to-end", async () => { arguments: { surface: "main" }, }); expect(shot.isError).toBeFalsy(); - const image = (shot.content as Array<{ type: string; data?: string; mimeType?: string }>) - .find((c) => c.type === "image"); + const image = ( + shot.content as Array<{ + type: string; + data?: string; + mimeType?: string; + }> + ).find((c) => c.type === "image"); expect(image?.mimeType).toBe("image/png"); expect(image?.data).toBeTruthy(); // Base64 of a non-empty PNG decodes to at least the 8-byte signature diff --git a/tsconfig.json b/tsconfig.json index 31c1f0f..6285c23 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,22 +1,19 @@ { "compilerOptions": { "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "lib": ["ES2022", "DOM"], + "module": "ESNext", + "moduleResolution": "Bundler", "strict": true, - "esModuleInterop": true, "skipLibCheck": true, + "esModuleInterop": true, "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, "isolatedModules": true, - "noUncheckedIndexedAccess": true, "declaration": true, "declarationMap": true, "sourceMap": true, - "outDir": "./dist", - "rootDir": "./src" + "outDir": "dist", + "types": ["node", "electron"] }, - "include": ["src/**/*"], - "exclude": ["dist", "node_modules", "tests"] + "include": ["src/**/*.ts", "test/**/*.ts", "tsdown.config.ts"], + "exclude": ["dist", "node_modules"] } diff --git a/tsdown.config.ts b/tsdown.config.ts new file mode 100644 index 0000000..a128efd --- /dev/null +++ b/tsdown.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from "tsdown"; + +export default defineConfig({ + entry: ["src/index.ts", "src/types.ts"], + format: "esm", + dts: true, + sourcemap: true, + clean: true, + deps: { + neverBundle: [ + "@modelcontextprotocol/sdk", + "async-mutex", + "electron", + "zod", + /^node:/, + ], + }, +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..312d90b --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["src/**/*.test.ts"], + typecheck: { + enabled: true, + include: ["src/**/*.test.ts"], + }, + }, +});