From 2d13242f294556f6a6f0d0c9e3c38044426bac67 Mon Sep 17 00:00:00 2001 From: Guillaume ARM Date: Wed, 17 Jun 2026 00:26:04 +0200 Subject: [PATCH] refactor: remove old mcp-bridge --- .opencode/agent/atm10-expert.md | 4 +- .plans/remove-mcp-bridge-plan.md | 173 ++ DEVELOPMENT.md | 2 +- docs/adrs/adr-0016-js-tool-verification.md | 12 +- .../adrs/adr-0017-mcp-remote-lua-execution.md | 5 +- .../adrs/adr-0019-mcp-over-sandbox-gateway.md | 11 +- docs/public-ports.md | 4 +- just/install.just | 4 +- just/npm.just | 26 +- just/test.just | 2 +- manifest.json | 2 +- packages/index.json | 4 +- packages/trapos-ai/apis/libai.lua | 46 +- packages/trapos-ai/apis/libhttpws.lua | 140 -- packages/trapos-ai/ccpm.json | 3 +- packages/trapos-ai/programs/ai.lua | 12 +- packages/trapos-ai/tests/ai.lua | 214 -- packages/trapos/ccpm.json | 2 +- tools/mcp-bridge/README.md | 13 - tools/mcp-bridge/eslint.config.js | 26 - tools/mcp-bridge/package-lock.json | 1833 ----------------- tools/mcp-bridge/package.json | 38 - tools/mcp-bridge/src/index.ts | 45 - tools/mcp-bridge/src/link-server.ts | 212 -- tools/mcp-bridge/src/mcp-server.ts | 276 --- tools/mcp-bridge/src/opencode-proxy.ts | 73 - tools/mcp-bridge/src/protocol.ts | 135 -- .../test-integration/ai-cli.test.ts | 181 -- tools/mcp-bridge/test-integration/harness.ts | 228 -- .../test-integration/lua/ai-cli-check.lua | 27 - .../test-integration/lua/echo-client.lua | 42 - .../lua/multi-echo-client.lua | 44 - .../lua/opencode-proxy-check.lua | 15 - .../test-integration/lua/silent-client.lua | 28 - .../test-integration/opencode-proxy.test.ts | 70 - .../test-integration/probe-empty.test.ts | 12 - .../test-integration/probe-happy.test.ts | 21 - .../test-integration/probe-multi.test.ts | 25 - .../test-integration/probe-silent.test.ts | 21 - tools/mcp-bridge/test/opencode-proxy.test.ts | 163 -- tools/mcp-bridge/test/probe-computers.test.ts | 377 ---- tools/mcp-bridge/test/protocol.test.ts | 19 - tools/mcp-bridge/tsconfig.json | 17 - 43 files changed, 214 insertions(+), 4393 deletions(-) create mode 100644 .plans/remove-mcp-bridge-plan.md delete mode 100644 packages/trapos-ai/apis/libhttpws.lua delete mode 100644 tools/mcp-bridge/README.md delete mode 100644 tools/mcp-bridge/eslint.config.js delete mode 100644 tools/mcp-bridge/package-lock.json delete mode 100644 tools/mcp-bridge/package.json delete mode 100644 tools/mcp-bridge/src/index.ts delete mode 100644 tools/mcp-bridge/src/link-server.ts delete mode 100644 tools/mcp-bridge/src/mcp-server.ts delete mode 100644 tools/mcp-bridge/src/opencode-proxy.ts delete mode 100644 tools/mcp-bridge/src/protocol.ts delete mode 100644 tools/mcp-bridge/test-integration/ai-cli.test.ts delete mode 100644 tools/mcp-bridge/test-integration/harness.ts delete mode 100644 tools/mcp-bridge/test-integration/lua/ai-cli-check.lua delete mode 100644 tools/mcp-bridge/test-integration/lua/echo-client.lua delete mode 100644 tools/mcp-bridge/test-integration/lua/multi-echo-client.lua delete mode 100644 tools/mcp-bridge/test-integration/lua/opencode-proxy-check.lua delete mode 100644 tools/mcp-bridge/test-integration/lua/silent-client.lua delete mode 100644 tools/mcp-bridge/test-integration/opencode-proxy.test.ts delete mode 100644 tools/mcp-bridge/test-integration/probe-empty.test.ts delete mode 100644 tools/mcp-bridge/test-integration/probe-happy.test.ts delete mode 100644 tools/mcp-bridge/test-integration/probe-multi.test.ts delete mode 100644 tools/mcp-bridge/test-integration/probe-silent.test.ts delete mode 100644 tools/mcp-bridge/test/opencode-proxy.test.ts delete mode 100644 tools/mcp-bridge/test/probe-computers.test.ts delete mode 100644 tools/mcp-bridge/test/protocol.test.ts delete mode 100644 tools/mcp-bridge/tsconfig.json diff --git a/.opencode/agent/atm10-expert.md b/.opencode/agent/atm10-expert.md index 4c3fe97..4ba58d0 100644 --- a/.opencode/agent/atm10-expert.md +++ b/.opencode/agent/atm10-expert.md @@ -12,8 +12,8 @@ permission: bash: "*": ask "just check": allow - computercraft-mcp-bridge_probe-computers: allow - computercraft-mcp-bridge_exec-lua: allow + trapos-cloud-gateway-mcp_probe-computers: allow + trapos-cloud-gateway-mcp_exec-lua: allow --- You answer players using TrapOS `ai` from inside ATM10 / All The Mods 10. diff --git a/.plans/remove-mcp-bridge-plan.md b/.plans/remove-mcp-bridge-plan.md new file mode 100644 index 0000000..fa6a366 --- /dev/null +++ b/.plans/remove-mcp-bridge-plan.md @@ -0,0 +1,173 @@ +# Plan: Remove `tools/mcp-bridge` + +## Context + +`tools/mcp-bridge` used to provide two separate host-side surfaces: + +1. MCP tools and a dedicated ComputerCraft link transport. +2. An opencode HTTP-over-WebSocket proxy used by the in-game `ai` bridge mode. + +The MCP tools have already moved to `tools/trap-sandbox` and `.mcp.json` already points at +the sandbox MCP endpoint (`http://127.0.0.1:4445`). The remaining live dependency is the old +opencode proxy path (`opencc.bridge_url` / `packages/trapos-ai/apis/libhttpws.lua`). + +Post-pull path note: Lua source files now live under `packages//...`. The CraftOS-PC +test harness stages those files into flat runtime roots via `just/stage.just`, and package +descriptors still list installed flat paths such as `apis/libai.lua`. + +Decision: remove `tools/mcp-bridge` completely and intentionally drop the old opencode proxy +implementation for now. If bridge/proxy behavior is needed again, re-implement it from +scratch later; the old implementation remains available in git history. + +## Scope + +- Delete `tools/mcp-bridge/` entirely. +- Remove `ai` WebSocket bridge-mode support. +- Keep direct HTTP/HTTPS opencode support through `packages/trapos-ai/apis/libhttp.lua`. +- Keep current MCP tooling in `tools/trap-sandbox` unchanged, except for docs/tooling cleanup. + +## Runtime Changes + +### Drop `libhttpws` + +- Delete `packages/trapos-ai/apis/libhttpws.lua`. +- Remove `require('/apis/libhttpws')` from `packages/trapos-ai/apis/libai.lua`. +- Remove `opencc.bridge_url` handling from `packages/trapos-ai/apis/libai.lua`. +- Remove the fallback that treats `ws://` / `wss:// opencc.server_url` as bridge mode. +- Remove `opencc.request_timeout_seconds` usage if it only exists for the WebSocket transport. + +### Update `ai` CLI Help + +Update `packages/trapos-ai/programs/ai.lua` help text: + +- Remove `opencc.bridge_url` from required settings. +- Remove `opencc.request_timeout_seconds` from optional settings. +- Make `opencc.server_url` clearly HTTP/HTTPS-only. + +Expected behavior after removal: users configure `opencc.server_url` to an HTTP/HTTPS +`opencode serve` endpoint. There is no WebSocket bridge mode until a new implementation is +added later. + +## Tests + +### Lua Tests + +Update `packages/trapos-ai/tests/ai.lua`: + +- Remove bridge-specific tests under the `ask over the bridge ws transport` section. +- Remove fake WebSocket helper code if it becomes unused. +- Keep direct HTTP opencode behavior tests. + +### Node / Integration Tests + +- Delete all `tools/mcp-bridge/test/` and `tools/mcp-bridge/test-integration/` tests with the + directory. +- Do not port `opencode-proxy.test.ts` or `ai-cli.test.ts` to `tools/trap-sandbox`; the proxy + behavior is intentionally removed. +- Keep `tools/trap-sandbox/test/` and `tools/trap-sandbox/test-integration/` as the supported + MCP/gateway coverage. + +## Tooling Changes + +### `just/npm.just` + +- Remove `npm-install-mcp-bridge`. +- Remove `npm-build-mcp-bridge`. +- Remove `npm-check-mcp-bridge`. +- Remove `npm-test-mcp-bridge`. +- Remove `npm-test-integration-mcp-bridge`. +- Make aggregate Node recipes target only `tools/trap-sandbox`: + - `npm-install` + - `npm-build` + - `npm-check` + - `npm-test` + - `npm-test-integration` + +### `just/install.just` + +- Change `clean` to clean `tools/trap-sandbox` caches instead of `tools/mcp-bridge`, or make it + a broader Node-tool clean if useful. +- Update the comment above `clean`. + +### `just/test.just` + +- Update comments that describe end-to-end tests as spanning the MCP bridge; they now span the + sandbox gateway / MCP endpoint. + +### `just/stage.just` + +- No behavior change expected. It already stages package-owned sources into `.stage/`; deleting + `packages/trapos-ai/apis/libhttpws.lua` and removing it from `packages/trapos-ai/ccpm.json` + is sufficient. + +## Packaging + +Update `packages/trapos-ai/ccpm.json`: + +- Remove `apis/libhttpws.lua` from `files`. +- Bump the `trapos-ai` package version because runtime behavior/help changes. + +Update `packages/index.json`: + +- Mirror the new `trapos-ai` version. + +No `trapos-sandbox` package bump is required unless its behavior changes during cleanup. + +## Documentation Cleanup + +### Required Docs + +- `DEVELOPMENT.md`: update `just clean` description so it no longer mentions `mcp-bridge`. +- `docs/public-ports.md`: remove the old `4243` / `CC_LINK_PORT` ComputerCraft bridge entry, + unless a new service explicitly claims that port later. +- `docs/adrs/adr-0019-mcp-over-sandbox-gateway.md`: replace the statement that + `tools/mcp-bridge` is kept for the opencode proxy with a note that it has been removed and + the proxy was intentionally dropped pending a future rewrite. +- `docs/adrs/adr-0016-js-tool-verification.md`: replace `tools/mcp-bridge` as the JS tool + verification example with `tools/trap-sandbox`. + +### Historical / Optional Cleanup + +- `docs/adrs/adr-0017-mcp-remote-lua-execution.md`: this is historical; either leave it as-is + or add a short superseded note pointing to ADR-0019. +- `.opencode/agent/atm10-expert.md`: update old `computercraft-mcp-bridge_*` permission names + if they are still relevant. +- `.plans/archived/*`: leave old references as archived history unless doing a full text + cleanup pass. + +## Files Expected To Change + +- Delete: `tools/mcp-bridge/` +- Delete: `packages/trapos-ai/apis/libhttpws.lua` +- Modify: `packages/trapos-ai/apis/libai.lua` +- Modify: `packages/trapos-ai/programs/ai.lua` +- Modify: `packages/trapos-ai/tests/ai.lua` +- Modify: `just/npm.just` +- Modify: `just/install.just` +- Modify: `just/test.just` +- Modify: `packages/trapos-ai/ccpm.json` +- Modify: `packages/index.json` +- Modify: `DEVELOPMENT.md` +- Modify: `docs/public-ports.md` +- Modify: `docs/adrs/adr-0019-mcp-over-sandbox-gateway.md` +- Modify: `docs/adrs/adr-0016-js-tool-verification.md` +- Optional: `docs/adrs/adr-0017-mcp-remote-lua-execution.md` +- Optional: `.opencode/agent/atm10-expert.md` + +## Verification + +After implementation: + +1. Run `just check`. +2. Run `just test`. +3. Run `just npm-test-integration` if not already covered by the selected test target. +4. Optionally run full `just ci` before merging. + +Expected result: + +- No recipe references `tools/mcp-bridge`. +- No package descriptor references `apis/libhttpws.lua`. +- No source file references `require('/apis/libhttpws')`, `opencc.bridge_url`, or + `opencc.request_timeout_seconds` except archived history or generated/local CraftOS state. +- `ai` works only through direct HTTP/HTTPS `opencc.server_url`. +- MCP tools continue to work through `tools/trap-sandbox`. diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index ae3247d..3135a71 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -12,7 +12,7 @@ This installs local Git hooks, checks required tools, generates `.env`, and veri ## Cleaning -- `just clean` — clears the `mcp-bridge` build caches (`node_modules/.cache/tsc`, `node_modules/.cache/eslint`). Safe to run anytime; `just reinstall` chains it with `just install`. +- `just clean` — clears the `trap-sandbox` build caches (`node_modules/.cache/tsc`, `node_modules/.cache/eslint`). Safe to run anytime; `just reinstall` chains it with `just install`. - `just clean-env` — deletes `.env`. Run only when you actually want to drop locally generated tokens. See [`docs/README.md`](docs/README.md) for repository docs and [`docs/adrs/README.md`](docs/adrs/README.md) for architecture decisions. diff --git a/docs/adrs/adr-0016-js-tool-verification.md b/docs/adrs/adr-0016-js-tool-verification.md index c0380c3..7d6a236 100644 --- a/docs/adrs/adr-0016-js-tool-verification.md +++ b/docs/adrs/adr-0016-js-tool-verification.md @@ -10,9 +10,9 @@ Accepted ## Context -The repository now contains `tools/mcp-bridge`, a TypeScript Node.js tool that sits next to the ComputerCraft Lua code. It has a different build and test lifecycle than TrapOS packages, but it still participates in the same local developer workflow and Git hooks described by [ADR-0011](adr-0011-repo-conventions.md). +The repository contains `tools/trap-sandbox`, a TypeScript Node.js tool that sits next to the ComputerCraft Lua code. It has a different build and test lifecycle than TrapOS packages, but it still participates in the same local developer workflow and Git hooks described by [ADR-0011](adr-0011-repo-conventions.md). (This convention was first established for `tools/mcp-bridge`, which has since been removed; `trap-sandbox` is now the JS tool it governs.) -The bridge also needs future end-to-end coverage that spans both runtimes: a host Node process and a headless CraftOS-PC computer. That is broader and slower than normal unit tests, and it needs the same kind of timeout discipline as the Lua harness in [ADR-0007](adr-0007-test-framework.md). +The tool also needs end-to-end coverage that spans both runtimes: a host Node process and a headless CraftOS-PC computer. That is broader and slower than normal unit tests, and it needs the same kind of timeout discipline as the Lua harness in [ADR-0007](adr-0007-test-framework.md). ## Decision @@ -22,12 +22,12 @@ Keep the Node package scripts simple and one-purpose: - `npm run test` runs the Node unit tests directly from TypeScript with `tsx --test test/*.test.ts`. No prior build is required. - `npm run check` runs ESLint. TypeScript compilation is covered by `npm run build` and `npm run test:ci` to avoid duplicate compiler runs in repository CI. - `npm run test:ci` runs `npm run check && npm run build && npm run test`, so type errors and lint failures both surface even though `test` itself no longer compiles. -- `npm run test-integration` runs the bridge-to-CraftOS integration suite with `tsx --test --test-concurrency=1 test-integration/*.test.ts`. Each case boots the bridge in-process on fixed loopback ports (`127.0.0.1:2000` for MCP HTTP, `127.0.0.1:2001` for the CraftOS link), spawns a CraftOS-PC headless computer that connects back, exercises `tools/call probe-computers`, and tears everything down. `--test-concurrency=1` keeps the fixed ports collision-free. -- These fixed integration-test ports are loopback-only and unrelated to the public production ports documented in [`../public-ports.md`](../public-ports.md). +- `npm run test:integration` runs the host-to-CraftOS integration suite with `tsx --test --test-concurrency=1 test-integration/*.test.ts`. Each case boots the gateway in-process on an ephemeral loopback port, spawns a CraftOS-PC headless computer that connects back over the gateway WebSocket, drives MCP tool calls through the Fastify app (`inject`, no separate listener), and tears everything down. +- These integration-test ports are loopback-only and unrelated to the public production ports documented in [`../public-ports.md`](../public-ports.md). Expose matching repository recipes for the Node lifecycle: -- `just build` delegates to `npm run build` for the bridge. +- `just build` delegates to `npm run build` for the sandbox tool. - `just test` depends on `just build`, runs `npm run test`, then runs the existing CraftOS-PC test suite. - `just check` includes `npm run check` alongside Lua and Markdown checks. - `just ci` uses `npm run test:ci`, then runs CraftOS-PC tests and the broader integration/harness target. @@ -39,7 +39,7 @@ Expose matching repository recipes for the Node lifecycle: - TypeScript errors are no longer caught by `npm run test`; they are caught by `npm run build`, which stays wired into `npm run test:ci`, `just build`, `just test`, and `just ci`. - `just ci` avoids duplicating Node unit tests by calling `npm run test:ci` directly and then invoking only the CraftOS-side test body. - ESLint failures are part of `just check`, so they are covered by the same pre-commit and pre-push hooks as Lua and Markdown checks. -- Integration tests live in `tools/mcp-bridge/test-integration/`, with Lua client fixtures under `test-integration/lua/`. Slow end-to-end behavior stays out of the fast unit-test path. +- Integration tests live in `tools/trap-sandbox/test-integration/`, with Lua client fixtures under `test-integration/lua/`. Slow end-to-end behavior stays out of the fast unit-test path. ## Future Work diff --git a/docs/adrs/adr-0017-mcp-remote-lua-execution.md b/docs/adrs/adr-0017-mcp-remote-lua-execution.md index 03710d1..b992682 100644 --- a/docs/adrs/adr-0017-mcp-remote-lua-execution.md +++ b/docs/adrs/adr-0017-mcp-remote-lua-execution.md @@ -3,8 +3,9 @@ ## Status Accepted. Transport superseded by [ADR-0019](adr-0019-mcp-over-sandbox-gateway.md) — the -MCP tools now ride the sandbox gateway and address computers by `traposId`. The execution -semantics and danger model below still apply. +MCP tools now ride the sandbox gateway and address computers by `traposId`, and +`tools/mcp-bridge` (referenced below in the past tense) has since been removed. The +execution semantics and danger model below still apply. ## Date diff --git a/docs/adrs/adr-0019-mcp-over-sandbox-gateway.md b/docs/adrs/adr-0019-mcp-over-sandbox-gateway.md index 3e142a8..e01c423 100644 --- a/docs/adrs/adr-0019-mcp-over-sandbox-gateway.md +++ b/docs/adrs/adr-0019-mcp-over-sandbox-gateway.md @@ -51,9 +51,11 @@ Move the MCP features onto the sandbox gateway and retire the dedicated MCP tran `ok:false` result payload (preserving captured `output`), matching the old bridge's semantics; only a dropped connection or timeout is a transport-level failure. -`tools/mcp-bridge` is **not** deleted: its opencode HTTP-over-WS proxy is still used by the -in-game `ai` bridge mode (`apis/libhttpws`, `apis/libai`). Only the MCP server and CC-link -parts are superseded here; the proxy migration is deferred. +`tools/mcp-bridge` has since been **removed** entirely. Its MCP server and CC-link parts +were superseded here; its opencode HTTP-over-WS proxy (and the in-game `ai` WebSocket bridge +mode it backed, `apis/libhttpws`) was intentionally dropped rather than migrated. The `ai` +client now talks direct HTTP/HTTPS to `opencode serve` (`opencc.server_url`); if a proxy is +needed again it will be re-implemented from scratch. The old code remains in git history. ## Consequences @@ -68,5 +70,6 @@ parts are superseded here; the proxy migration is deferred. ## Future Work -- Migrate the opencode HTTP proxy off `mcp-bridge` so the tool can be fully retired. +- Re-implement an opencode proxy / WebSocket transport for the `ai` client if the + ComputerCraft ~60s HTTP cap becomes a problem again (the old `mcp-bridge` proxy was dropped). - Per-computer exec policy if the gateway is ever shared beyond trusted development. diff --git a/docs/public-ports.md b/docs/public-ports.md index e7ecf8f..3afa20f 100644 --- a/docs/public-ports.md +++ b/docs/public-ports.md @@ -5,7 +5,7 @@ TrapOS public production services use the `4242-4244` TCP range. Keep local deve | Port | Service | Notes | |---|---|---| | `4242` | opencode server HTTP API | Production/public port for `opencode serve`. Local/dev examples may still use opencode's default `4096`. | -| `4243` | ComputerCraft bridge WebSocket | Production/public equivalent of the local bridge link default `3001`. Use `CC_LINK_PORT=4243` for production bridge deployments. | +| `4243` | Reserved | Formerly the ComputerCraft bridge WebSocket (removed with `tools/mcp-bridge`). Free for a future service. | | `4244` | Reserved | Free public production port reserved for a future service. Do not assign it casually in local tooling. | | `4444` | trap-sandbox gateway | Fastify service (`tools/trap-sandbox`) exposing the `trapos-cloud-gateway` WebSocket at `/gateway`. Sits **outside** the `4242-4244` range on purpose (4244 stays reserved). Default `SANDBOX_PORT`; serve with `just sandbox-serve`. | @@ -13,7 +13,5 @@ TrapOS public production services use the `4242-4244` TCP range. Keep local deve - Local/dev opencode: `http://127.0.0.1:4096`. - Public/production opencode: `http://:4242`. -- Local/dev ComputerCraft bridge link: `ws://:3001`. -- Public/production ComputerCraft bridge link: `ws://:4243`. Production services exposed on public ports should use the normal deployment controls for the host: authentication where supported, firewall rules, and TLS or a reverse proxy when crossing untrusted networks. diff --git a/just/install.just b/just/install.just index b8931a9..9a7f2ae 100644 --- a/just/install.just +++ b/just/install.just @@ -1,9 +1,9 @@ # Install local development tooling. install: install-git-hooks check-install npm-install generate-env -# Remove build caches (tsc / eslint) for the mcp-bridge tool. +# Remove build caches (tsc / eslint) for the trap-sandbox tool. clean: - npm run clean --prefix tools/mcp-bridge + npm run clean --prefix tools/trap-sandbox # Remove generated local environment files (e.g. .env with tokens). clean-env: diff --git a/just/npm.just b/just/npm.just index 6f8b09c..5a159c7 100644 --- a/just/npm.just +++ b/just/npm.just @@ -1,46 +1,30 @@ # Install Node dependencies for repository tools. -npm-install: npm-install-mcp-bridge npm-install-trap-sandbox - -# Install Node dependencies for the MCP bridge. -npm-install-mcp-bridge: - npm install --prefix tools/mcp-bridge +npm-install: npm-install-trap-sandbox # Install Node dependencies for trap-sandbox. npm-install-trap-sandbox: npm install --prefix tools/trap-sandbox/ # Build Node-based repository tools. -npm-build: npm-build-mcp-bridge npm-build-trap-sandbox - -npm-build-mcp-bridge: - npm run build --prefix tools/mcp-bridge +npm-build: npm-build-trap-sandbox npm-build-trap-sandbox: npm run build --prefix tools/trap-sandbox/ # Check Node-based repository tools. -npm-check: npm-check-mcp-bridge npm-check-trap-sandbox - -npm-check-mcp-bridge: - npm run check --prefix tools/mcp-bridge +npm-check: npm-check-trap-sandbox npm-check-trap-sandbox: npm run check --prefix tools/trap-sandbox/ # Run Node-based tool tests. -npm-test: npm-test-mcp-bridge npm-test-trap-sandbox - -npm-test-mcp-bridge: - npm test --prefix tools/mcp-bridge +npm-test: npm-test-trap-sandbox npm-test-trap-sandbox: npm test --prefix tools/trap-sandbox/ # Run Node-based tool integration tests. -npm-test-integration: stage npm-test-integration-mcp-bridge npm-test-integration-trap-sandbox - -npm-test-integration-mcp-bridge: - npm run test:integration --prefix tools/mcp-bridge +npm-test-integration: stage npm-test-integration-trap-sandbox npm-test-integration-trap-sandbox: npm run test:integration --prefix tools/trap-sandbox/ diff --git a/just/test.just b/just/test.just index 6428fd3..c0ac35a 100644 --- a/just/test.just +++ b/just/test.just @@ -8,7 +8,7 @@ ci *args: check-craftos check check-packages npm-build npm-test test *args: build npm-test @just _craftos-test {{args}} -# Run end-to-end tests that span the MCP bridge and headless CraftOS-PC. +# Run end-to-end tests that span the trap-sandbox gateway / MCP endpoint and headless CraftOS-PC. e2e: npm-test-integration # Run integration and harness tests that are too broad for unit test targets. diff --git a/manifest.json b/manifest.json index be60da4..37878e5 100644 --- a/manifest.json +++ b/manifest.json @@ -1,6 +1,6 @@ { "name": "TrapOS", - "version": "0.11.5", + "version": "0.11.6", "branch": "master", "packages": [ "trapos" diff --git a/packages/index.json b/packages/index.json index ef31646..4c77d72 100644 --- a/packages/index.json +++ b/packages/index.json @@ -5,9 +5,9 @@ "trapos-boot": "0.4.2", "trapos-net": "0.3.2", "trapos-ui": "0.2.4", - "trapos-ai": "0.7.2", + "trapos-ai": "0.8.0", "trapos-sandbox": "0.3.5", "trapos-sandbox-legacy": "0.3.2", - "trapos": "0.11.5" + "trapos": "0.11.6" } } diff --git a/packages/trapos-ai/apis/libai.lua b/packages/trapos-ai/apis/libai.lua index 6c1570f..2db04c2 100644 --- a/packages/trapos-ai/apis/libai.lua +++ b/packages/trapos-ai/apis/libai.lua @@ -2,21 +2,13 @@ local PING_PROMPT = 'reply with exactly: pong'; local DEFAULT_TIMEOUT_SECONDS = 60; local MAX_TIMEOUT_SECONDS = 60; -local DEFAULT_REQUEST_TIMEOUT_SECONDS = 600; -local MAX_REQUEST_TIMEOUT_SECONDS = 600; local DEFAULT_LUA_EXEC_MAX_RETRIES = 2; local DEFAULT_LUA_EXEC_TIMEOUT_SECONDS = 5; local DEFAULT_SESSION_SETTING_KEY = 'opencc.session_id'; local DEFAULT_AGENT_SETTING_KEY = 'opencc.agent'; local DEFAULT_VARIANT_SETTING_KEY = 'opencc.variant'; -local DEFAULT_BRIDGE_SETTING_KEY = 'opencc.bridge_url'; local createHttp = require('/apis/libhttp'); -local createHttpWs = require('/apis/libhttpws'); - -local function isWsUrl(url) - return type(url) == 'string' and string.match(url, '^wss?://') ~= nil; -end local function isBlank(s) return type(s) ~= 'string' or string.match(s, '^%s*$') ~= nil; @@ -155,34 +147,7 @@ local function createAi(opts) local settingsLib = opts.settings or settings; local osLib = opts.os or os; - local function resolveRequestTimeout() - local raw = settingsLib.get('opencc.request_timeout_seconds'); - local n = tonumber(raw); - if not n or n <= 0 then n = DEFAULT_REQUEST_TIMEOUT_SECONDS; end - if n > MAX_REQUEST_TIMEOUT_SECONDS then n = MAX_REQUEST_TIMEOUT_SECONDS; end - return n; - end - - -- A ws:// bridge routes opencode calls through the mcp-bridge proxy, escaping - -- ComputerCraft's hard ~60s http timeout. Falls back to direct http otherwise. - local bridgeUrl = opts.bridgeUrl or settingsLib.get(DEFAULT_BRIDGE_SETTING_KEY); - if isBlank(bridgeUrl) and isWsUrl(settingsLib.get('opencc.server_url')) then - bridgeUrl = settingsLib.get('opencc.server_url'); - end - local bridgeActive = not isBlank(bridgeUrl); - - local httpClient = opts.httpClient; - if not httpClient then - if bridgeActive then - httpClient = createHttpWs({ - http = httpLib, - bridgeUrl = bridgeUrl, - receiveTimeout = resolveRequestTimeout(), - }); - else - httpClient = createHttp({ http = httpLib }); - end - end + local httpClient = opts.httpClient or createHttp({ http = httpLib }); local api = {}; @@ -233,12 +198,8 @@ local function createAi(opts) local function resolveConfig(options) local url = options.serverUrl or settingsLib.get('opencc.server_url'); - -- In bridge mode the proxy holds the real opencode URL, so server_url is optional. if isBlank(url) then - if not bridgeActive then - return nil, 'missing opencc.server_url; run: set opencc.server_url '; - end - url = ''; + return nil, 'missing opencc.server_url; run: set opencc.server_url '; end local username = options.username or settingsLib.get('opencc.username') or 'opencode'; local password = options.password or settingsLib.get('opencc.password') or ''; @@ -439,8 +400,7 @@ local function createAi(opts) end -- Synchronous /message only: opencode's prompt_async loops the model on some - -- builds and strands sessions as busy. The bridge ws transport removes the - -- ~60s http cap that previously made blocking impractical. + -- builds and strands sessions as busy. return askBlocking(cfg, sessionId, promptWithContext, persist, sessionSettingKey, log); end diff --git a/packages/trapos-ai/apis/libhttpws.lua b/packages/trapos-ai/apis/libhttpws.lua deleted file mode 100644 index aaf6395..0000000 --- a/packages/trapos-ai/apis/libhttpws.lua +++ /dev/null @@ -1,140 +0,0 @@ --- WebSocket transport for the opencode bridge proxy. --- --- Implements the same client surface as libhttp (getJson/postJson plus the --- trimTrailingSlash/queryString helpers libai calls), but performs each call as --- a blocking websocket round-trip to the mcp-bridge opencode proxy instead of a --- direct http.get/post. ComputerCraft's http API caps requests at ~60s; a --- websocket round-trip has no such cap, so the bridge can run a synchronous --- opencode request server-side for as long as it needs. - -local DEFAULT_RECEIVE_TIMEOUT_SECONDS = 600; - -local function isBlank(s) - return type(s) ~= 'string' or string.match(s, '^%s*$') ~= nil; -end - -local function trimTrailingSlash(s) - return (s:gsub('/+$', '')); -end - -local function urlEncode(s) - return (tostring(s):gsub('[^%w%-_%.~]', function(c) - return string.format('%%%02X', string.byte(c)); - end)); -end - -local function queryString(params) - local parts = {}; - for _, item in ipairs(params) do - if not isBlank(item[2]) then - parts[#parts + 1] = urlEncode(item[1]) .. '=' .. urlEncode(item[2]); - end - end - if #parts == 0 then return ''; end - return '?' .. table.concat(parts, '&'); -end - -local function createHttpWs(opts) - opts = opts or {}; - local httpLib = opts.http or http; - local textutilsLib = opts.textutils or textutils; - local bridgeUrl = opts.bridgeUrl; - local receiveTimeout = tonumber(opts.receiveTimeout) or DEFAULT_RECEIVE_TIMEOUT_SECONDS; - - local api = { - trimTrailingSlash = trimTrailingSlash, - urlEncode = urlEncode, - queryString = queryString, - }; - - local activeWs = nil; - - local function ensureSocket() - if activeWs then return activeWs, nil; end - if isBlank(bridgeUrl) then - return nil, 'missing opencc.bridge_url'; - end - local ws, err = httpLib.websocket(bridgeUrl); - if not ws then - return nil, 'bridge unreachable: ' .. tostring(err); - end - activeWs = ws; - return ws, nil; - end - - local function closeSocket() - if activeWs then - pcall(function() activeWs.close(); end); - activeWs = nil; - end - end - - local idCounter = 0; - local function nextId() - idCounter = idCounter + 1; - local stamp = os.epoch and os.epoch('utc') or os.clock(); - return 'req_' .. tostring(stamp) .. '_' .. tostring(idCounter) .. '_' .. tostring(math.random(100000, 999999)); - end - - local function trySend(ws, text) - return pcall(function() ws.send(text); end); - end - - local function roundtrip(method, path, payload) - local ws, err = ensureSocket(); - if not ws then return nil, err; end - - local id = nextId(); - local frame = { type = 'http', id = id, method = method, path = path }; - if payload ~= nil then - frame.body = textutilsLib.serializeJSON(payload); - end - local text = textutilsLib.serializeJSON(frame); - - if not trySend(ws, text) then - -- Socket went away; drop it and try one fresh reconnect. - closeSocket(); - ws, err = ensureSocket(); - if not ws then return nil, err; end - if not trySend(ws, text) then - closeSocket(); - return nil, 'bridge send failed'; - end - end - - while true do - local ok, message = pcall(function() return ws.receive(receiveTimeout); end); - if not ok then - closeSocket(); - return nil, 'bridge connection closed'; - end - if message == nil then - return nil, 'timeout waiting for bridge response'; - end - local decoded = textutilsLib.unserializeJSON(message); - if type(decoded) == 'table' and decoded.type == 'http-response' and decoded.id == id then - if decoded.status == nil or decoded.status == 0 then - return nil, decoded.error or 'bridge error'; - end - return decoded.body, decoded.status; - end - -- Ignore frames that don't correlate (stale/other ids) and keep waiting. - end - end - - function api.getJson(_cfg, path) - return roundtrip('GET', path, nil); - end - - function api.postJson(_cfg, path, payload) - return roundtrip('POST', path, payload); - end - - function api.close() - closeSocket(); - end - - return api; -end - -return createHttpWs; diff --git a/packages/trapos-ai/ccpm.json b/packages/trapos-ai/ccpm.json index 844d485..34a32cd 100644 --- a/packages/trapos-ai/ccpm.json +++ b/packages/trapos-ai/ccpm.json @@ -1,12 +1,11 @@ { "name": "trapos-ai", - "version": "0.7.2", + "version": "0.8.0", "description": "TrapOS AI client for opencode serve", "dependencies": ["trapos-core"], "files": [ "apis/libai.lua", "apis/libhttp.lua", - "apis/libhttpws.lua", "apis/libtrapgpt.lua", "programs/ai.lua", "programs/trapgpt.lua" diff --git a/packages/trapos-ai/programs/ai.lua b/packages/trapos-ai/programs/ai.lua index d7eb561..9e1e3d0 100644 --- a/packages/trapos-ai/programs/ai.lua +++ b/packages/trapos-ai/programs/ai.lua @@ -41,21 +41,19 @@ local function printUsage() print(' ai --version'); print(' ai --help'); print(); - print('settings required (one of):'); - print(' opencc.server_url (direct http opencode URL)'); - print(' opencc.bridge_url (ws:// mcp-bridge proxy; bypasses CC 60s http cap)'); + print('settings required:'); + print(' opencc.server_url (http/https opencode serve URL)'); print(); print('settings optional:'); - print(' opencc.username (default: opencode; direct mode only)'); - print(' opencc.password (Basic Auth password; direct mode only)'); + print(' opencc.username (default: opencode)'); + print(' opencc.password (Basic Auth password)'); print(' opencc.session_id (auto-managed)'); print(' opencc.directory (optional session list scope)'); print(' opencc.agent (e.g. atm10-expert)'); print(' opencc.variant (e.g. low)'); print(' opencc.provider_id (e.g. anthropic)'); print(' opencc.model_id (e.g. claude-opus-4-7)'); - print(' opencc.timeout_seconds (per HTTP call, max 60; direct mode)'); - print(' opencc.request_timeout_seconds (ws reply wait, default/max: 600)'); + print(' opencc.timeout_seconds (per HTTP call, max 60)'); end local function printAiLog(message) diff --git a/packages/trapos-ai/tests/ai.lua b/packages/trapos-ai/tests/ai.lua index e75576e..cbce6ee 100644 --- a/packages/trapos-ai/tests/ai.lua +++ b/packages/trapos-ai/tests/ai.lua @@ -67,59 +67,6 @@ local function fakeHttp(postResults, getResults) }; end --- Fake CC http library exposing websocket(), for the bridge ws transport. --- wsResults: list of { status=, body=, error= } returned per round-trip in order. --- A nil entry simulates ws.receive timing out. opts.connectFail makes --- http.websocket return (nil, err). -local function fakeWsHttp(wsResults, opts) - wsResults = wsResults or {}; - opts = opts or {}; - local sent = {}; - local receiveTimeouts = {}; - local idx = 0; - local lastId = nil; - local closed = false; - local connectUrl = nil; - - local ws = { - send = function(text) - local frame = textutils.unserializeJSON(text); - sent[#sent + 1] = frame; - lastId = frame and frame.id; - end, - receive = function(timeout) - receiveTimeouts[#receiveTimeouts + 1] = timeout; - idx = idx + 1; - local r = wsResults[idx]; - if r == nil then return nil; end - return textutils.serializeJSON({ - type = 'http-response', - id = lastId, - status = r.status, - body = r.body, - error = r.error, - }); - end, - close = function() closed = true; end, - }; - - local httpLib = { - websocket = function(url) - connectUrl = url; - if opts.connectFail then return nil, 'refused'; end - return ws; - end, - }; - - return { - http = httpLib, - sent = sent, - isClosed = function() return closed; end, - connectUrl = function() return connectUrl; end, - lastReceiveTimeout = function() return receiveTimeouts[#receiveTimeouts]; end, - }; -end - local function httpError(code, body) return function() return nil, 'HTTP response code ' .. tostring(code), response(code, body); @@ -137,21 +84,6 @@ local function messageResp(reply) })); end -local function wsResult(status, body) - return { status = status, body = body }; -end - -local function wsSessionResult(id) - return wsResult(200, textutils.serializeJSON({ id = id, title = 'cc-ai' })); -end - -local function wsMessageResult(reply) - return wsResult(200, textutils.serializeJSON({ - info = { time = { completed = 1 } }, - parts = { { type = 'text', text = reply } }, - })); -end - local function postedText(call) local body = textutils.unserializeJSON(call.body); return body.parts[1].text; @@ -937,152 +869,6 @@ testlib.test('ask omits Authorization header when no password', function() testlib.assertEquals(httpStub.postCalls[1].headers['Authorization'], nil); end); --- ask over the bridge ws transport -- - -testlib.test('ask over bridge uses synchronous message endpoint', function() - local ws = fakeWsHttp({ - wsSessionResult('ses_ws'), - wsMessageResult('reply'), - }); - local settingsStub = fakeSettings({ ['opencc.bridge_url'] = 'ws://bridge' }); - local ai = createAi({ http = ws.http, settings = settingsStub }); - - local ok, result = ai.ask('hello'); - - testlib.assertTrue(ok, tostring(result)); - testlib.assertEquals(result.reply, 'reply'); - testlib.assertEquals(result.sessionId, 'ses_ws'); - testlib.assertEquals(ws.connectUrl(), 'ws://bridge'); - testlib.assertEquals(#ws.sent, 2); - testlib.assertEquals(ws.sent[1].method, 'POST'); - testlib.assertEquals(ws.sent[1].path, '/session'); - testlib.assertEquals(ws.sent[2].method, 'POST'); - testlib.assertEquals(ws.sent[2].path, '/session/ses_ws/message'); - testlib.assertTrue(string.find(ws.sent[2].path, 'prompt_async', 1, true) == nil); -end); - -testlib.test('ask uses bridge when server_url has ws scheme', function() - local ws = fakeWsHttp({ - wsSessionResult('ses_ws'), - wsMessageResult('reply'), - }); - local settingsStub = fakeSettings({ ['opencc.server_url'] = 'ws://bridgehost' }); - local ai = createAi({ http = ws.http, settings = settingsStub }); - - local ok, result = ai.ask('hello'); - - testlib.assertTrue(ok, tostring(result)); - testlib.assertEquals(result.reply, 'reply'); - testlib.assertEquals(ws.connectUrl(), 'ws://bridgehost'); - testlib.assertTrue(#ws.sent >= 1); -end); - -testlib.test('ask over bridge sends body as serialized json', function() - local ws = fakeWsHttp({ wsMessageResult('reply') }); - local settingsStub = fakeSettings({ - ['opencc.bridge_url'] = 'ws://bridge', - ['opencc.session_id'] = 'ses_1', - ['opencc.agent'] = 'atm10-expert', - }); - local ai = createAi({ http = ws.http, settings = settingsStub, os = fakeOs(5, 'turtle') }); - - local ok = ai.ask('hello'); - - testlib.assertTrue(ok); - testlib.assertEquals(#ws.sent, 1); - testlib.assertEquals(ws.sent[1].path, '/session/ses_1/message'); - local body = textutils.unserializeJSON(ws.sent[1].body); - testlib.assertEquals(body.agent, 'atm10-expert'); - testlib.assertTrue(string.find(body.parts[1].text, 'computer id: 5', 1, true) ~= nil); -end); - -testlib.test('ask over bridge surfaces a transport error frame', function() - local ws = fakeWsHttp({ { status = 0, error = 'fetch failed: ECONNREFUSED' } }); - local settingsStub = fakeSettings({ - ['opencc.bridge_url'] = 'ws://bridge', - ['opencc.session_id'] = 'ses_1', - }); - local ai = createAi({ http = ws.http, settings = settingsStub }); - - local ok, err = ai.ask('hello'); - - testlib.assertTrue(not ok); - testlib.assertTrue(string.find(err, 'fetch failed', 1, true) ~= nil); -end); - -testlib.test('ask over bridge times out when no reply', function() - local ws = fakeWsHttp({}); - local settingsStub = fakeSettings({ - ['opencc.bridge_url'] = 'ws://bridge', - ['opencc.session_id'] = 'ses_1', - }); - local ai = createAi({ http = ws.http, settings = settingsStub }); - - local ok, err = ai.ask('hello'); - - testlib.assertTrue(not ok); - testlib.assertTrue(string.find(err, 'timeout', 1, true) ~= nil); -end); - -testlib.test('ask over bridge maps a 404 frame to missing session', function() - local ws = fakeWsHttp({ wsResult(404, '{}') }); - local settingsStub = fakeSettings({ - ['opencc.bridge_url'] = 'ws://bridge', - ['opencc.session_id'] = 'ses_stale', - }); - local ai = createAi({ http = ws.http, settings = settingsStub }); - - local ok, err = ai.ask('hello'); - - testlib.assertTrue(not ok); - testlib.assertTrue(string.find(err, 'session introuvable', 1, true) ~= nil); - testlib.assertEquals(settingsStub.values['opencc.session_id'], nil); -end); - -testlib.test('ask over bridge fails when websocket connect fails', function() - local ws = fakeWsHttp({}, { connectFail = true }); - local settingsStub = fakeSettings({ - ['opencc.bridge_url'] = 'ws://bridge', - ['opencc.session_id'] = 'ses_1', - }); - local ai = createAi({ http = ws.http, settings = settingsStub }); - - local ok, err = ai.ask('hello'); - - testlib.assertTrue(not ok); - testlib.assertTrue(string.find(err, 'bridge unreachable', 1, true) ~= nil); -end); - -testlib.test('ask over bridge uses request_timeout_seconds for ws receive', function() - local ws = fakeWsHttp({ wsMessageResult('reply') }); - local settingsStub = fakeSettings({ - ['opencc.bridge_url'] = 'ws://bridge', - ['opencc.session_id'] = 'ses_1', - ['opencc.request_timeout_seconds'] = 120, - }); - local ai = createAi({ http = ws.http, settings = settingsStub }); - - local ok = ai.ask('hello'); - - testlib.assertTrue(ok); - testlib.assertEquals(ws.lastReceiveTimeout(), 120); -end); - -testlib.test('ask over bridge caps request_timeout_seconds at ten minutes', function() - local ws = fakeWsHttp({ wsMessageResult('reply') }); - local settingsStub = fakeSettings({ - ['opencc.bridge_url'] = 'ws://bridge', - ['opencc.session_id'] = 'ses_1', - ['opencc.request_timeout_seconds'] = 5000, - }); - local ai = createAi({ http = ws.http, settings = settingsStub }); - - local ok = ai.ask('hello'); - - testlib.assertTrue(ok); - testlib.assertEquals(ws.lastReceiveTimeout(), 600); -end); - -- lua executor -- testlib.test('lua executor captures print write and returned values', function() diff --git a/packages/trapos/ccpm.json b/packages/trapos/ccpm.json index e9f9342..8fc3663 100644 --- a/packages/trapos/ccpm.json +++ b/packages/trapos/ccpm.json @@ -1,6 +1,6 @@ { "name": "trapos", - "version": "0.11.5", + "version": "0.11.6", "description": "TrapOS full install meta-package", "dependencies": [ "trapos-boot", diff --git a/tools/mcp-bridge/README.md b/tools/mcp-bridge/README.md deleted file mode 100644 index 150c8af..0000000 --- a/tools/mcp-bridge/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# mcp-bridge (deprecated, except the opencode proxy) - -> **Deprecated.** The MCP server (`probe-computers` / `exec-lua` / `write-file`) and the -> CC-link WebSocket server have been migrated to `tools/trap-sandbox`, which serves the -> same tools over the sandbox gateway and addresses computers by `traposId`. See -> [ADR-0019](../../docs/adrs/adr-0019-mcp-over-sandbox-gateway.md). - -This tool is kept alive only for its **opencode HTTP-over-WS proxy**, still used by the -in-game `ai` bridge mode (`apis/libhttpws.lua`, `apis/libai.lua`, `programs/ai.lua`) to -bypass ComputerCraft's ~60s HTTP cap. Once that proxy is migrated onto the gateway, this -tool can be removed entirely. - -Do not add new MCP features here — add them to `tools/trap-sandbox/src/modules/mcp`. diff --git a/tools/mcp-bridge/eslint.config.js b/tools/mcp-bridge/eslint.config.js deleted file mode 100644 index fae6796..0000000 --- a/tools/mcp-bridge/eslint.config.js +++ /dev/null @@ -1,26 +0,0 @@ -import js from "@eslint/js"; -import globals from "globals"; -import tseslint from "typescript-eslint"; - -export default tseslint.config( - { ignores: ["dist/**"] }, - js.configs.recommended, - ...tseslint.configs.recommendedTypeChecked, - { - languageOptions: { - globals: globals.node, - parserOptions: { - projectService: { - allowDefaultProject: ["eslint.config.js"], - }, - tsconfigRootDir: import.meta.dirname, - }, - }, - }, - { - files: ["test/**/*.ts", "test-integration/**/*.ts"], - rules: { - "@typescript-eslint/no-floating-promises": "off", - }, - }, -); diff --git a/tools/mcp-bridge/package-lock.json b/tools/mcp-bridge/package-lock.json deleted file mode 100644 index 1349cc0..0000000 --- a/tools/mcp-bridge/package-lock.json +++ /dev/null @@ -1,1833 +0,0 @@ -{ - "name": "mcp-bridge", - "version": "0.3.1", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "mcp-bridge", - "version": "0.3.1", - "dependencies": { - "ws": "^8.17.1" - }, - "devDependencies": { - "@eslint/js": "^10.0.1", - "@types/node": "^20.14.10", - "@types/ws": "^8.5.10", - "eslint": "^10.4.1", - "globals": "^17.6.0", - "tsx": "^4.16.2", - "typescript": "^5.5.3", - "typescript-eslint": "^8.61.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", - "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", - "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", - "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", - "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", - "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", - "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", - "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", - "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", - "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", - "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", - "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", - "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", - "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", - "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", - "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", - "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", - "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", - "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", - "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", - "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", - "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", - "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", - "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", - "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", - "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", - "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.23.5", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", - "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^3.0.5", - "debug": "^4.3.1", - "minimatch": "^10.2.4" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", - "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^1.2.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/core": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", - "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/js": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", - "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "eslint": "^10.0.0" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, - "node_modules/@eslint/object-schema": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", - "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", - "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^1.2.1", - "levn": "^0.4.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", - "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/types": "^0.15.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", - "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.2", - "@humanfs/types": "^0.15.0", - "@humanwhocodes/retry": "^0.4.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/types": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", - "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@types/esrecurse": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", - "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "20.19.42", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.42.tgz", - "integrity": "sha512-5L7SUaFC1RyDraj2yRhyBzHTobyXHmohD100CChNtyPyleoq37Mqab5Gn8XEKI04dfN/oqPdpHk38MgcQWHbZg==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.61.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.0.tgz", - "integrity": "sha512-bFNvl9ZczlVb+wR2Akszf3gHfKVj/8WanXaGJ3UstTA7brNKg0cNdk6X1Psu5V7MZ2oQtzZKOEzIUehaoxbDGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.61.0", - "@typescript-eslint/type-utils": "8.61.0", - "@typescript-eslint/utils": "8.61.0", - "@typescript-eslint/visitor-keys": "8.61.0", - "ignore": "^7.0.5", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.61.0", - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.61.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.0.tgz", - "integrity": "sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.61.0", - "@typescript-eslint/types": "8.61.0", - "@typescript-eslint/typescript-estree": "8.61.0", - "@typescript-eslint/visitor-keys": "8.61.0", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.61.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.61.0.tgz", - "integrity": "sha512-DV42F7MLJO6Rax7SK1yg43tcnEfGUrurSpSxKuVX+a3RCTzBlH3fuxprrOJXKCJGAaw82xXocikJ0uQaqwXgGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.61.0", - "@typescript-eslint/types": "^8.61.0", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.61.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.61.0.tgz", - "integrity": "sha512-IWdXFHFSb6mlC3HPc7QsLDm5zYEbUla6trDEHf32D3/dnuUyXd87plScSNXSbm0/RxMvObpI17sv/EDTGrGZkA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.61.0", - "@typescript-eslint/visitor-keys": "8.61.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.61.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.0.tgz", - "integrity": "sha512-O5Amvdv9ztMpxpf+vmFULGG78IE6Qwdr3bCGvqwG4nwc9H2qXkOYJJnRbRHyMkQTjv1d03olqwwwzHLMqpFePQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.61.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.61.0.tgz", - "integrity": "sha512-TuBiQYIkd97yBfInHCTKVYMbX4kvEmpOEuixIuzCU9p8BGT1SfyyO0d0IfDMbPIHcjn/hWnusUX5e8v5Xg+X8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.61.0", - "@typescript-eslint/typescript-estree": "8.61.0", - "@typescript-eslint/utils": "8.61.0", - "debug": "^4.4.3", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.61.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.0.tgz", - "integrity": "sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.61.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.0.tgz", - "integrity": "sha512-42zatd5qSvvcV1JdDBCLxYRznvP4eIHpPoZXdkPFnAmanA4FuZ5dibSnCBggY8hQnqajPpoGjXFdZ7fIJKQnlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.61.0", - "@typescript-eslint/tsconfig-utils": "8.61.0", - "@typescript-eslint/types": "8.61.0", - "@typescript-eslint/visitor-keys": "8.61.0", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.61.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.61.0.tgz", - "integrity": "sha512-3bzFt7ImFMW/jVYwJamDoe/dMOdFLSC6pom6rRjdh4SZJEYupyMzem8e7vKZLclLfpHjlwSAXOUxtKxGXUiLqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.61.0", - "@typescript-eslint/types": "8.61.0", - "@typescript-eslint/typescript-estree": "8.61.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.61.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.0.tgz", - "integrity": "sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.61.0", - "eslint-visitor-keys": "^5.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/esbuild": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", - "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.0", - "@esbuild/android-arm": "0.28.0", - "@esbuild/android-arm64": "0.28.0", - "@esbuild/android-x64": "0.28.0", - "@esbuild/darwin-arm64": "0.28.0", - "@esbuild/darwin-x64": "0.28.0", - "@esbuild/freebsd-arm64": "0.28.0", - "@esbuild/freebsd-x64": "0.28.0", - "@esbuild/linux-arm": "0.28.0", - "@esbuild/linux-arm64": "0.28.0", - "@esbuild/linux-ia32": "0.28.0", - "@esbuild/linux-loong64": "0.28.0", - "@esbuild/linux-mips64el": "0.28.0", - "@esbuild/linux-ppc64": "0.28.0", - "@esbuild/linux-riscv64": "0.28.0", - "@esbuild/linux-s390x": "0.28.0", - "@esbuild/linux-x64": "0.28.0", - "@esbuild/netbsd-arm64": "0.28.0", - "@esbuild/netbsd-x64": "0.28.0", - "@esbuild/openbsd-arm64": "0.28.0", - "@esbuild/openbsd-x64": "0.28.0", - "@esbuild/openharmony-arm64": "0.28.0", - "@esbuild/sunos-x64": "0.28.0", - "@esbuild/win32-arm64": "0.28.0", - "@esbuild/win32-ia32": "0.28.0", - "@esbuild/win32-x64": "0.28.0" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.4.1.tgz", - "integrity": "sha512-AyIKhnOBuOAdueD7RB3xB+YeAWScb9jHsJBgH2Hcde8InP5JYhqrRR6iTMHyTEwgENK54Cp44e4v8BwNhsuHuw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.2", - "@eslint/config-array": "^0.23.5", - "@eslint/config-helpers": "^0.6.0", - "@eslint/core": "^1.2.1", - "@eslint/plugin-kit": "^0.7.2", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.14.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^9.1.2", - "eslint-visitor-keys": "^5.0.1", - "espree": "^11.2.0", - "esquery": "^1.7.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "minimatch": "^10.2.4", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-scope": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", - "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@types/esrecurse": "^4.3.1", - "@types/estree": "^1.0.8", - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", - "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.16.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^5.0.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", - "dev": true, - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "17.6.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", - "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.5" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/semver": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", - "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", - "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/ts-api-utils": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", - "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/tsx": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", - "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "~0.28.0" - }, - "bin": { - "tsx": "dist/cli.mjs" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - } - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typescript-eslint": { - "version": "8.61.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.61.0.tgz", - "integrity": "sha512-8y31Rd0eGTrDKqhy6vT0HtzhN+YLjQizwX3aA3hPXP/ynSfnrBXcQY5IzsP9/DM7+klX4IUncZZjkchP0z+rUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.61.0", - "@typescript-eslint/parser": "8.61.0", - "@typescript-eslint/typescript-estree": "8.61.0", - "@typescript-eslint/utils": "8.61.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ws": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", - "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/tools/mcp-bridge/package.json b/tools/mcp-bridge/package.json deleted file mode 100644 index 1c03ffe..0000000 --- a/tools/mcp-bridge/package.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "name": "mcp-bridge", - "version": "0.3.1", - "private": true, - "type": "module", - "engines": { - "node": ">=18" - }, - "scripts": { - "build": "tsc", - "eslint": "eslint . --cache --cache-location node_modules/.cache/eslint/", - "clean": "rm -rf node_modules/.cache/tsc node_modules/.cache/eslint", - "check": "npm run eslint", - "test:all": "npm run check && npm run build && npm run test", - "test:ci": "npm run test:all && npm run test:integration", - "dev": "tsx watch src/index.ts", - "start": "npm run build && node dist/src/index.js", - "test": "tsx --test test/*.test.ts", - "test:integration": "tsx --test --test-concurrency=1 test-integration/*.test.ts" - }, - "dependencies": { - "ws": "^8.17.1" - }, - "devDependencies": { - "@eslint/js": "^10.0.1", - "@types/node": "^20.14.10", - "@types/ws": "^8.5.10", - "eslint": "^10.4.1", - "globals": "^17.6.0", - "tsx": "^4.16.2", - "typescript": "^5.5.3", - "typescript-eslint": "^8.61.0" - }, - "allowScripts": { - "esbuild@0.28.0": true, - "fsevents@2.3.3": true - } -} diff --git a/tools/mcp-bridge/src/index.ts b/tools/mcp-bridge/src/index.ts deleted file mode 100644 index e30026c..0000000 --- a/tools/mcp-bridge/src/index.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { LinkRegistry, startLinkServer } from "./link-server.js"; -import { startMcpServer } from "./mcp-server.js"; -import { startOpencodeProxy } from "./opencode-proxy.js"; - -const config = { - mcpHost: process.env.MCP_HOST ?? "127.0.0.1", - mcpPort: readPort(process.env.MCP_PORT, 3000), - ccLinkHost: process.env.CC_LINK_HOST ?? "0.0.0.0", - ccLinkPort: readPort(process.env.CC_LINK_PORT, 3001), - probeTimeoutMs: readPort(process.env.CC_PROBE_TIMEOUT_MS, 2000), - opencodeProxyHost: process.env.OPENCODE_PROXY_HOST ?? "0.0.0.0", - opencodeProxyPort: readPort(process.env.OPENCODE_PROXY_PORT, 3002), - opencodeUrl: process.env.OPENCODE_URL, - opencodeUsername: process.env.OPENCODE_USERNAME, - opencodePassword: process.env.OPENCODE_PASSWORD, -}; - -const registry = new LinkRegistry(); -startMcpServer({ host: config.mcpHost, port: config.mcpPort, probeTimeoutMs: config.probeTimeoutMs, registry }); -startLinkServer({ host: config.ccLinkHost, port: config.ccLinkPort, registry }); - -console.log(`MCP bridge listening on http://${config.mcpHost}:${config.mcpPort}`); -console.log(`ComputerCraft link listening on ws://${config.ccLinkHost}:${config.ccLinkPort}`); - -if (config.opencodeUrl) { - startOpencodeProxy({ - host: config.opencodeProxyHost, - port: config.opencodeProxyPort, - opencodeUrl: config.opencodeUrl, - username: config.opencodeUsername, - password: config.opencodePassword, - }); - console.log(`opencode proxy listening on ws://${config.opencodeProxyHost}:${config.opencodeProxyPort} -> ${config.opencodeUrl}`); -} else { - console.log("opencode proxy disabled (set OPENCODE_URL to enable)"); -} - -function readPort(value: string | undefined, fallback: number): number { - if (!value) { - return fallback; - } - - const parsed = Number(value); - return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback; -} diff --git a/tools/mcp-bridge/src/link-server.ts b/tools/mcp-bridge/src/link-server.ts deleted file mode 100644 index 2ad372a..0000000 --- a/tools/mcp-bridge/src/link-server.ts +++ /dev/null @@ -1,212 +0,0 @@ -import { randomUUID } from "node:crypto"; -import { WebSocketServer, type WebSocket } from "ws"; -import { formatComputer, parseJsonFrame, parseLinkMessage, type ResponseMessage } from "./protocol.js"; - -export type ComputerConnection = { - computerId: number; - label: string | null; - ws: WebSocket; - connectedAt: number; - lastSeenAt: number; -}; - -type PendingRequest = { - computerId: number; - resolve: (message: ResponseMessage) => void; -}; - -export class LinkRegistry { - private readonly computers = new Map(); - private readonly pending = new Map(); - - register(connection: ComputerConnection): void { - const existing = this.computers.get(connection.computerId); - if (existing && existing.ws !== connection.ws) { - existing.ws.close(); - } - this.computers.set(connection.computerId, connection); - } - - unregister(ws: WebSocket): void { - for (const [computerId, connection] of this.computers) { - if (connection.ws === ws) { - this.computers.delete(computerId); - } - } - } - - count(): number { - return this.computers.size; - } - - snapshot(): ComputerConnection[] { - return [...this.computers.values()]; - } - - handleFrame(ws: WebSocket, data: unknown): void { - const message = parseLinkMessage(parseJsonFrame(data)); - if (!message) { - return; - } - - if (message.type === "hello") { - const now = Date.now(); - this.register({ - computerId: message.computerId, - label: message.computerLabel, - ws, - connectedAt: now, - lastSeenAt: now, - }); - ws.send(JSON.stringify({ type: "hello-ok" })); - return; - } - - const pending = this.pending.get(message.id); - if (!pending) { - return; - } - - const connection = this.computers.get(pending.computerId); - if (connection) { - connection.lastSeenAt = Date.now(); - } - this.pending.delete(message.id); - pending.resolve(message); - } - - async probeComputers(timeoutMs: number): Promise { - const computers = this.snapshot(); - if (computers.length === 0) { - return "No computers connected."; - } - - const lines = await Promise.all(computers.map((computer) => this.probeComputer(computer, timeoutMs))); - return lines.join("\n"); - } - - async execLua(computerId: number, code: string, timeoutMs: number): Promise { - const computer = this.computers.get(computerId); - if (!computer) { - return `No computer with id ${computerId} connected.`; - } - - const result = await this.requestComputer(computer, "exec-lua", { code }, timeoutMs); - return formatExecutionResult(computer, result); - } - - async runFile(computerId: number, path: string, args: string[], timeoutMs: number): Promise { - const computer = this.computers.get(computerId); - if (!computer) { - return `No computer with id ${computerId} connected.`; - } - - const result = await this.requestComputer(computer, "run-file", { path, args }, timeoutMs); - return formatExecutionResult(computer, result); - } - - async writeFile(computerId: number, path: string, content: string, timeoutMs: number): Promise { - const computer = this.computers.get(computerId); - if (!computer) { - return `No computer with id ${computerId} connected.`; - } - - const result = await this.requestComputer(computer, "write-file", { path, content }, timeoutMs); - return formatWriteFileResult(computer, result); - } - - private async probeComputer(computer: ComputerConnection, timeoutMs: number): Promise { - const result = await this.requestComputer(computer, "ping", undefined, timeoutMs); - if (!result) { - return `timeout from ${formatComputer(computer.computerId, computer.label)}`; - } - - if (result.ok && typeof result.result === "string") { - return result.result; - } - - return `error from ${formatComputer(computer.computerId, computer.label)}: ${result.error ?? "unknown error"}`; - } - - private async requestComputer( - computer: ComputerConnection, - method: string, - params: Record | undefined, - timeoutMs: number, - ): Promise { - const id = randomUUID(); - const response = new Promise((resolve) => { - this.pending.set(id, { computerId: computer.computerId, resolve }); - computer.ws.send(JSON.stringify(params ? { type: "request", id, method, params } : { type: "request", id, method })); - }); - - const timeout = new Promise((resolve) => { - setTimeout(() => { - this.pending.delete(id); - resolve(null); - }, timeoutMs); - }); - - return Promise.race([response, timeout]); - } -} - -function formatExecutionResult(computer: ComputerConnection, response: ResponseMessage | null): string { - const computerText = formatComputer(computer.computerId, computer.label); - if (!response) { - return `computer: ${computerText}\nok: false\nerror: timeout`; - } - - const payload = isRecord(response.result) ? response.result : {}; - const output = typeof payload.output === "string" ? payload.output : ""; - const ok = response.ok && payload.ok !== false; - const lines = [`computer: ${computerText}`, `ok: ${ok ? "true" : "false"}`]; - - if (ok) { - lines.push(`returns: ${JSON.stringify(Array.isArray(payload.returns) ? payload.returns : [])}`); - } else { - lines.push(`error: ${typeof payload.error === "string" ? payload.error : response.error ?? "unknown error"}`); - } - - lines.push("output:"); - if (output !== "") { - lines.push(output.endsWith("\n") ? output.slice(0, -1) : output); - } - return lines.join("\n"); -} - -function formatWriteFileResult(computer: ComputerConnection, response: ResponseMessage | null): string { - const computerText = formatComputer(computer.computerId, computer.label); - if (!response) { - return `computer: ${computerText}\nok: false\nerror: timeout`; - } - - const payload = isRecord(response.result) ? response.result : {}; - const ok = response.ok && payload.ok !== false; - const lines = [`computer: ${computerText}`, `ok: ${ok ? "true" : "false"}`]; - - if (ok) { - lines.push(`path: ${typeof payload.path === "string" ? payload.path : ""}`); - lines.push(`bytes: ${typeof payload.bytes === "number" ? payload.bytes : 0}`); - } else { - lines.push(`error: ${typeof payload.error === "string" ? payload.error : response.error ?? "unknown error"}`); - } - - return lines.join("\n"); -} - -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value); -} - -export function startLinkServer(options: { host: string; port: number; registry: LinkRegistry }): WebSocketServer { - const server = new WebSocketServer({ host: options.host, port: options.port }); - - server.on("connection", (ws) => { - ws.on("message", (data) => options.registry.handleFrame(ws, data)); - ws.on("close", () => options.registry.unregister(ws)); - ws.on("error", () => options.registry.unregister(ws)); - }); - - return server; -} diff --git a/tools/mcp-bridge/src/mcp-server.ts b/tools/mcp-bridge/src/mcp-server.ts deleted file mode 100644 index aebb69e..0000000 --- a/tools/mcp-bridge/src/mcp-server.ts +++ /dev/null @@ -1,276 +0,0 @@ -import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http"; -import type { LinkRegistry } from "./link-server.js"; - -type JsonRpcRequest = { - jsonrpc?: unknown; - id?: unknown; - method?: unknown; - params?: unknown; -}; - -export function startMcpServer(options: { - host: string; - port: number; - probeTimeoutMs: number; - registry: LinkRegistry; -}): Server { - const server = createServer((req, res) => { - void handleRequest(req, res, options.registry, options.probeTimeoutMs); - }); - - server.listen(options.port, options.host); - return server; -} - -export async function handleMcpRequest(body: unknown, registry: LinkRegistry, probeTimeoutMs: number): Promise { - if (Array.isArray(body)) { - return Promise.all(body.map((item) => handleSingleRequest(item as JsonRpcRequest, registry, probeTimeoutMs))); - } - - return handleSingleRequest(body as JsonRpcRequest, registry, probeTimeoutMs); -} - -async function handleRequest( - req: IncomingMessage, - res: ServerResponse, - registry: LinkRegistry, - probeTimeoutMs: number, -): Promise { - if (req.method === "GET" && req.url === "/health") { - writeJson(res, 200, { ok: true, computers: registry.count() }); - return; - } - - if (req.method !== "POST") { - writeJson(res, 404, { error: "not found" }); - return; - } - - const raw = await readBody(req); - let body: unknown; - try { - body = JSON.parse(raw) as unknown; - } catch { - writeJson(res, 400, jsonRpcError(null, -32700, "Parse error")); - return; - } - - writeJson(res, 200, await handleMcpRequest(body, registry, probeTimeoutMs)); -} - -async function handleSingleRequest(request: JsonRpcRequest, registry: LinkRegistry, probeTimeoutMs: number): Promise { - const id = request && "id" in request ? request.id : null; - if (!request || request.jsonrpc !== "2.0" || typeof request.method !== "string") { - return jsonRpcError(id, -32600, "Invalid Request"); - } - - if (request.method === "initialize") { - return jsonRpcResult(id, { - protocolVersion: "2024-11-05", - capabilities: { tools: {} }, - serverInfo: { name: "mcp-bridge", version: "0.3.1" }, - }); - } - - if (request.method === "notifications/initialized") { - return null; - } - - if (request.method === "tools/list") { - return jsonRpcResult(id, { - tools: [ - { - name: "probe-computers", - description: "Probe all linked ComputerCraft computers.", - inputSchema: { type: "object", properties: {}, additionalProperties: false }, - }, - { - name: "exec-lua", - description: "Execute Lua code on a linked ComputerCraft computer.", - inputSchema: { - type: "object", - properties: { - computerId: { type: "number", description: "ComputerCraft computer id to execute on." }, - code: { type: "string", description: "Lua source code to execute." }, - timeoutMs: { type: "number", description: "Optional host-side timeout in milliseconds, max 30000." }, - }, - required: ["computerId", "code"], - additionalProperties: false, - }, - }, - { - name: "write-file", - description: "Write file content on a linked ComputerCraft computer, overwriting any existing file.", - inputSchema: { - type: "object", - properties: { - computerId: { type: "number", description: "ComputerCraft computer id to write on." }, - path: { type: "string", description: "Target path on the ComputerCraft computer." }, - content: { type: "string", description: "File content to write. Empty strings are allowed." }, - timeoutMs: { type: "number", description: "Optional host-side timeout in milliseconds, max 30000." }, - }, - required: ["computerId", "path", "content"], - additionalProperties: false, - }, - }, - { - name: "run-file", - description: "Run a Lua file on a linked ComputerCraft computer with captured print/write output.", - inputSchema: { - type: "object", - properties: { - computerId: { type: "number", description: "ComputerCraft computer id to run on." }, - path: { type: "string", description: "Lua file path on the ComputerCraft computer." }, - args: { type: "array", items: { type: "string" }, description: "Optional program arguments passed as Lua varargs." }, - timeoutMs: { type: "number", description: "Optional host-side timeout in milliseconds, max 30000." }, - }, - required: ["computerId", "path"], - additionalProperties: false, - }, - }, - ], - }); - } - - if (request.method === "tools/call") { - const params = isRecord(request.params) ? request.params : {}; - if (params.name === "probe-computers") { - const text = await registry.probeComputers(probeTimeoutMs); - return jsonRpcResult(id, { content: [{ type: "text", text }] }); - } - - if (params.name === "exec-lua") { - const args = isRecord(params.arguments) ? params.arguments : {}; - const parsed = parseExecLuaArgs(args, probeTimeoutMs); - if (!parsed.ok) { - return jsonRpcError(id, -32602, parsed.error); - } - - const text = await registry.execLua(parsed.computerId, parsed.code, parsed.timeoutMs); - return jsonRpcResult(id, { content: [{ type: "text", text }] }); - } - - if (params.name === "write-file") { - const args = isRecord(params.arguments) ? params.arguments : {}; - const parsed = parseWriteFileArgs(args, probeTimeoutMs); - if (!parsed.ok) { - return jsonRpcError(id, -32602, parsed.error); - } - - const text = await registry.writeFile(parsed.computerId, parsed.path, parsed.content, parsed.timeoutMs); - return jsonRpcResult(id, { content: [{ type: "text", text }] }); - } - - if (params.name === "run-file") { - const args = isRecord(params.arguments) ? params.arguments : {}; - const parsed = parseRunFileArgs(args, probeTimeoutMs); - if (!parsed.ok) { - return jsonRpcError(id, -32602, parsed.error); - } - - const text = await registry.runFile(parsed.computerId, parsed.path, parsed.args, parsed.timeoutMs); - return jsonRpcResult(id, { content: [{ type: "text", text }] }); - } - - return jsonRpcError(id, -32602, "Unknown tool"); - } - - return jsonRpcError(id, -32601, "Method not found"); -} - -function parseExecLuaArgs( - args: Record, - defaultTimeoutMs: number, -): { ok: true; computerId: number; code: string; timeoutMs: number } | { ok: false; error: string } { - if (typeof args.computerId !== "number" || !Number.isFinite(args.computerId)) { - return { ok: false, error: "computerId must be a finite number" }; - } - - if (typeof args.code !== "string" || args.code.trim() === "") { - return { ok: false, error: "code must be a non-empty string" }; - } - - if (args.timeoutMs !== undefined && (typeof args.timeoutMs !== "number" || !Number.isFinite(args.timeoutMs) || args.timeoutMs <= 0)) { - return { ok: false, error: "timeoutMs must be a positive finite number" }; - } - - const timeoutMs = Math.min(args.timeoutMs ?? defaultTimeoutMs, 30_000); - return { ok: true, computerId: args.computerId, code: args.code, timeoutMs }; -} - -function parseWriteFileArgs( - args: Record, - defaultTimeoutMs: number, -): { ok: true; computerId: number; path: string; content: string; timeoutMs: number } | { ok: false; error: string } { - if (typeof args.computerId !== "number" || !Number.isFinite(args.computerId)) { - return { ok: false, error: "computerId must be a finite number" }; - } - - if (typeof args.path !== "string" || args.path.trim() === "") { - return { ok: false, error: "path must be a non-empty string" }; - } - - if (typeof args.content !== "string") { - return { ok: false, error: "content must be a string" }; - } - - if (args.timeoutMs !== undefined && (typeof args.timeoutMs !== "number" || !Number.isFinite(args.timeoutMs) || args.timeoutMs <= 0)) { - return { ok: false, error: "timeoutMs must be a positive finite number" }; - } - - const timeoutMs = Math.min(args.timeoutMs ?? defaultTimeoutMs, 30_000); - return { ok: true, computerId: args.computerId, path: args.path, content: args.content, timeoutMs }; -} - -function parseRunFileArgs( - args: Record, - defaultTimeoutMs: number, -): { ok: true; computerId: number; path: string; args: string[]; timeoutMs: number } | { ok: false; error: string } { - if (typeof args.computerId !== "number" || !Number.isFinite(args.computerId)) { - return { ok: false, error: "computerId must be a finite number" }; - } - - if (typeof args.path !== "string" || args.path.trim() === "") { - return { ok: false, error: "path must be a non-empty string" }; - } - - if (args.args !== undefined && !isStringArray(args.args)) { - return { ok: false, error: "args must be an array of strings" }; - } - - if (args.timeoutMs !== undefined && (typeof args.timeoutMs !== "number" || !Number.isFinite(args.timeoutMs) || args.timeoutMs <= 0)) { - return { ok: false, error: "timeoutMs must be a positive finite number" }; - } - - const timeoutMs = Math.min(args.timeoutMs ?? defaultTimeoutMs, 30_000); - return { ok: true, computerId: args.computerId, path: args.path, args: args.args ?? [], timeoutMs }; -} - -function isStringArray(value: unknown): value is string[] { - return Array.isArray(value) && value.every((item) => typeof item === "string"); -} - -function jsonRpcResult(id: unknown, result: unknown): unknown { - return { jsonrpc: "2.0", id, result }; -} - -function jsonRpcError(id: unknown, code: number, message: string): unknown { - return { jsonrpc: "2.0", id, error: { code, message } }; -} - -function writeJson(res: ServerResponse, statusCode: number, body: unknown): void { - res.writeHead(statusCode, { "content-type": "application/json" }); - res.end(JSON.stringify(body)); -} - -async function readBody(req: IncomingMessage): Promise { - const chunks: Buffer[] = []; - for await (const chunk of req) { - chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))); - } - return Buffer.concat(chunks).toString("utf8"); -} - -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value); -} diff --git a/tools/mcp-bridge/src/opencode-proxy.ts b/tools/mcp-bridge/src/opencode-proxy.ts deleted file mode 100644 index cbbf8a0..0000000 --- a/tools/mcp-bridge/src/opencode-proxy.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { WebSocketServer, type WebSocket } from "ws"; -import { parseHttpRequestFrame, parseJsonFrame, type HttpResponseFrame } from "./protocol.js"; - -export type OpencodeProxyOptions = { - host: string; - port: number; - opencodeUrl: string; - username?: string; - password?: string; - maxRequestMs?: number; -}; - -const DEFAULT_MAX_REQUEST_MS = 600_000; - -// WebSocket endpoint where the ComputerCraft client is the requester and the -// bridge is the responder: it performs the opencode http call server-side (no -// short timeout) and returns the raw response. This lets the in-game client use -// opencode's synchronous /message endpoint without hitting CC's ~60s http cap. -export function startOpencodeProxy(options: OpencodeProxyOptions): WebSocketServer { - const baseUrl = options.opencodeUrl.replace(/\/+$/, ""); - const maxRequestMs = options.maxRequestMs ?? DEFAULT_MAX_REQUEST_MS; - const authHeader = options.password - ? "Basic " + Buffer.from(`${options.username ?? "opencode"}:${options.password}`).toString("base64") - : undefined; - - const server = new WebSocketServer({ host: options.host, port: options.port }); - - server.on("connection", (ws) => { - ws.on("message", (data) => { - void handleFrame(ws, data); - }); - ws.on("error", () => { - /* connection-level errors just drop the socket; nothing to clean up */ - }); - }); - - async function handleFrame(ws: WebSocket, data: unknown): Promise { - const frame = parseHttpRequestFrame(parseJsonFrame(data)); - if (!frame) { - return; - } - - const headers: Record = { - "Content-Type": "application/json", - Accept: "application/json", - }; - if (authHeader) { - headers.Authorization = authHeader; - } - - try { - const res = await fetch(baseUrl + frame.path, { - method: frame.method, - headers, - body: frame.method === "POST" ? frame.body : undefined, - signal: AbortSignal.timeout(maxRequestMs), - }); - const body = await res.text(); - send(ws, { type: "http-response", id: frame.id, status: res.status, body }); - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - send(ws, { type: "http-response", id: frame.id, status: 0, error: message }); - } - } - - return server; -} - -function send(ws: WebSocket, frame: HttpResponseFrame): void { - if (ws.readyState === ws.OPEN) { - ws.send(JSON.stringify(frame)); - } -} diff --git a/tools/mcp-bridge/src/protocol.ts b/tools/mcp-bridge/src/protocol.ts deleted file mode 100644 index 1435996..0000000 --- a/tools/mcp-bridge/src/protocol.ts +++ /dev/null @@ -1,135 +0,0 @@ -export type HelloMessage = { - type: "hello"; - computerId: number; - computerLabel: string | null; -}; - -export type ResponseMessage = { - type: "response"; - id: string; - ok: boolean; - result?: unknown; - error?: string; -}; - -export type LinkMessage = HelloMessage | ResponseMessage; - -export type HttpRequestFrame = { - type: "http"; - id: string; - method: "GET" | "POST"; - path: string; - body?: string; -}; - -export type HttpResponseFrame = { - type: "http-response"; - id: string; - status: number; - body?: string; - error?: string; -}; - -export function parseHttpRequestFrame(value: unknown): HttpRequestFrame | null { - if (!isRecord(value) || value.type !== "http") { - return null; - } - - if (typeof value.id !== "string" || value.id === "") { - return null; - } - - if (value.method !== "GET" && value.method !== "POST") { - return null; - } - - if (typeof value.path !== "string" || !value.path.startsWith("/")) { - return null; - } - - if (value.body !== undefined && typeof value.body !== "string") { - return null; - } - - return { - type: "http", - id: value.id, - method: value.method, - path: value.path, - body: typeof value.body === "string" ? value.body : undefined, - }; -} - -export function parseJsonFrame(data: unknown): unknown { - if (typeof data === "string") { - return parseJson(data); - } - - if (Buffer.isBuffer(data)) { - return parseJson(data.toString("utf8")); - } - - if (data instanceof ArrayBuffer) { - return parseJson(Buffer.from(data).toString("utf8")); - } - - if (Array.isArray(data)) { - return parseJson(Buffer.concat(data).toString("utf8")); - } - - return null; -} - -export function parseLinkMessage(value: unknown): LinkMessage | null { - if (!isRecord(value) || typeof value.type !== "string") { - return null; - } - - if (value.type === "hello") { - if (typeof value.computerId !== "number" || !Number.isFinite(value.computerId)) { - return null; - } - - const label = typeof value.computerLabel === "string" && value.computerLabel.trim() !== "" - ? value.computerLabel - : null; - - return { - type: "hello", - computerId: value.computerId, - computerLabel: label, - }; - } - - if (value.type === "response") { - if (typeof value.id !== "string" || typeof value.ok !== "boolean") { - return null; - } - - return { - type: "response", - id: value.id, - ok: value.ok, - result: value.result, - error: typeof value.error === "string" ? value.error : undefined, - }; - } - - return null; -} - -export function formatComputer(computerId: number, label: string | null): string { - return `${computerId} (Label: ${label ?? "null"})`; -} - -function parseJson(text: string): unknown { - try { - return JSON.parse(text) as unknown; - } catch { - return null; - } -} - -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value); -} diff --git a/tools/mcp-bridge/test-integration/ai-cli.test.ts b/tools/mcp-bridge/test-integration/ai-cli.test.ts deleted file mode 100644 index 72cc400..0000000 --- a/tools/mcp-bridge/test-integration/ai-cli.test.ts +++ /dev/null @@ -1,181 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; -import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http"; -import type { WebSocketServer } from "ws"; -import { formatFailure, startCraftos } from "./harness.js"; -import { startOpencodeProxy } from "../src/opencode-proxy.js"; - -type RequestLog = { - method: string; - path: string; - body?: unknown; -}; - -test("ai CLI commands run through the opencode bridge proxy", async () => { - const requests: RequestLog[] = []; - let nextSession = 1; - - const opencode = createServer((req, res) => { - void handleOpencodeRequest(req, res, requests, () => { - const id = `ses_${nextSession}`; - nextSession += 1; - return id; - }); - }); - - await new Promise((resolve) => opencode.listen(0, "127.0.0.1", resolve)); - const opencodeUrl = `http://127.0.0.1:${serverPort(opencode)}`; - - const proxy = startOpencodeProxy({ host: "127.0.0.1", port: 0, opencodeUrl }); - await waitForListening(proxy); - const proxyUrl = `ws://127.0.0.1:${wssPort(proxy)}`; - - const craftos = startCraftos("ai-cli-check.lua", { - mountRepo: true, - shellArgs: [proxyUrl], - timeoutMs: 20_000, - }); - - try { - const result = await craftos.done; - const output = result.output; - assert.equal(result.status, 0, formatFailure("expected craftos to exit cleanly", output)); - assert.match(output, /ses_existing {2}Existing title/, formatFailure("expected sessions output", output)); - assert.match(output, /\bpong\b/, formatFailure("expected ping reply", output)); - assert.match(output, /new reply/, formatFailure("expected ai new reply", output)); - assert.match(output, /plain reply/, formatFailure("expected plain ai reply", output)); - assert.match(output, /SESSION_AFTER_PING=ses_1/, formatFailure("expected ping session to persist", output)); - assert.match(output, /SESSION_AFTER_NEW=ses_2/, formatFailure("expected ai new to replace session", output)); - assert.match(output, /SESSION_AFTER_ASK=ses_2/, formatFailure("expected plain ai to reuse new session", output)); - - assert.deepEqual(requests.map((r) => `${r.method} ${r.path}`), [ - "GET /session", - "POST /session", - "POST /session/ses_1/message", - "POST /session", - "POST /session/ses_2/message", - "POST /session/ses_2/message", - ]); - assert.match(promptText(requests[2].body), /reply with exactly: pong/); - assert.match(promptText(requests[4].body), /User prompt:\nfresh start/); - assert.match(promptText(requests[5].body), /User prompt:\ncontinue please/); - } finally { - craftos.abort(); - await craftos.done.catch(() => undefined); - await closeWss(proxy); - await new Promise((resolve) => opencode.close(() => resolve())); - } -}); - -async function handleOpencodeRequest( - req: IncomingMessage, - res: ServerResponse, - requests: RequestLog[], - createSessionId: () => string, -): Promise { - const bodyText = await readBody(req); - const body: unknown = bodyText === "" ? undefined : JSON.parse(bodyText) as unknown; - const url = new URL(req.url ?? "/", "http://127.0.0.1"); - requests.push({ method: req.method ?? "GET", path: url.pathname + url.search, body }); - - if (req.method === "GET" && url.pathname === "/session") { - respondJson(res, [ - { id: "ses_existing", title: "Existing title", time: { updated: 10 } }, - ]); - return; - } - - if (req.method === "POST" && url.pathname === "/session") { - const id = createSessionId(); - respondJson(res, { id, title: "cc-ai" }); - return; - } - - const messageMatch = url.pathname.match(/^\/session\/([^/]+)\/message$/); - if (req.method === "POST" && messageMatch) { - respondJson(res, { - info: { id: `msg_${requests.length}`, time: { completed: 1 } }, - parts: [{ type: "text", text: replyForPrompt(promptText(body)) }], - }); - return; - } - - respondJson(res, { error: "not found" }, 404); -} - -function replyForPrompt(prompt: string): string { - if (prompt.includes("reply with exactly: pong")) { - return "pong"; - } - if (prompt.includes("fresh start")) { - return "new reply"; - } - if (prompt.includes("continue please")) { - return "plain reply"; - } - return `unhandled prompt: ${prompt}`; -} - -function promptText(body: unknown): string { - if (!isObject(body) || !Array.isArray(body.parts)) { - return ""; - } - const parts = body.parts as unknown[]; - const first = parts[0]; - if (!isObject(first) || typeof first.text !== "string") { - return ""; - } - return first.text; -} - -function isObject(value: unknown): value is Record { - return typeof value === "object" && value !== null; -} - -function readBody(req: IncomingMessage): Promise { - const chunks: Buffer[] = []; - req.on("data", (chunk: Buffer) => chunks.push(chunk)); - return new Promise((resolve, reject) => { - req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8"))); - req.on("error", reject); - }); -} - -function respondJson(res: ServerResponse, body: unknown, status = 200): void { - res.writeHead(status, { "content-type": "application/json" }); - res.end(JSON.stringify(body)); -} - -function serverPort(server: Server): number { - const address = server.address(); - if (!address || typeof address === "string") { - throw new Error("server has no TCP address"); - } - return address.port; -} - -function wssPort(proxy: WebSocketServer): number { - const address = proxy.address(); - if (!address || typeof address === "string") { - throw new Error("proxy has no TCP address"); - } - return address.port; -} - -function waitForListening(proxy: WebSocketServer): Promise { - return new Promise((resolve, reject) => { - if (proxy.address()) { - resolve(); - return; - } - proxy.once("listening", () => resolve()); - proxy.once("error", reject); - }); -} - -function closeWss(proxy: WebSocketServer): Promise { - for (const client of proxy.clients) { - client.terminate(); - } - return new Promise((resolve) => proxy.close(() => resolve())); -} diff --git a/tools/mcp-bridge/test-integration/harness.ts b/tools/mcp-bridge/test-integration/harness.ts deleted file mode 100644 index e3e906d..0000000 --- a/tools/mcp-bridge/test-integration/harness.ts +++ /dev/null @@ -1,228 +0,0 @@ -import { spawn } from "node:child_process"; -import { existsSync } from "node:fs"; -import { mkdtemp, rm } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; -import type { Server } from "node:http"; -import type { WebSocketServer } from "ws"; -import { LinkRegistry, startLinkServer } from "../src/link-server.js"; -import { startMcpServer } from "../src/mcp-server.js"; - -const HERE = dirname(fileURLToPath(import.meta.url)); -const LUA_DIR = join(HERE, "lua"); -const REPO_ROOT = join(HERE, "../../.."); - -export type Bridge = { - registry: LinkRegistry; - mcpUrl: string; - linkUrl: string; - close: () => Promise; -}; - -export async function startBridge(probeTimeoutMs = 500): Promise { - const registry = new LinkRegistry(); - const mcpServer = startMcpServer({ host: "127.0.0.1", port: 0, probeTimeoutMs, registry }); - const linkServer = startLinkServer({ host: "127.0.0.1", port: 0, registry }); - await Promise.all([waitForListening(mcpServer), waitForListening(linkServer)]); - return { - registry, - mcpUrl: `http://127.0.0.1:${serverPort(mcpServer)}`, - linkUrl: `ws://127.0.0.1:${serverPort(linkServer)}`, - close: () => closeBridge(mcpServer, linkServer), - }; -} - -export async function waitForComputers(registry: LinkRegistry, count: number, timeoutMs: number): Promise { - const deadline = Date.now() + timeoutMs; - while (Date.now() < deadline) { - if (registry.count() >= count) { - return; - } - await sleep(50); - } - throw new Error(`waitForComputers timed out (expected ${count}, got ${registry.count()})`); -} - -export async function callProbeComputers(mcpUrl: string): Promise { - const body = { - jsonrpc: "2.0", - id: 1, - method: "tools/call", - params: { name: "probe-computers", arguments: {} }, - }; - const response = await fetch(mcpUrl, { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify(body), - }); - const payload = (await response.json()) as { - result?: { content?: { type: string; text: string }[] }; - }; - const text = payload.result?.content?.[0]?.text; - if (typeof text !== "string") { - throw new Error(`Unexpected MCP response: ${JSON.stringify(payload)}`); - } - return text; -} - -export async function callExecLua(mcpUrl: string, computerId: number, code: string, timeoutMs?: number): Promise { - const body = { - jsonrpc: "2.0", - id: 1, - method: "tools/call", - params: { name: "exec-lua", arguments: { computerId, code, timeoutMs } }, - }; - const response = await fetch(mcpUrl, { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify(body), - }); - const payload = (await response.json()) as { - result?: { content?: { type: string; text: string }[] }; - }; - const text = payload.result?.content?.[0]?.text; - if (typeof text !== "string") { - throw new Error(`Unexpected MCP response: ${JSON.stringify(payload)}`); - } - return text; -} - -export async function callWriteFile( - mcpUrl: string, - computerId: number, - path: string, - content: string, - timeoutMs?: number, -): Promise { - const body = { - jsonrpc: "2.0", - id: 1, - method: "tools/call", - params: { name: "write-file", arguments: { computerId, path, content, timeoutMs } }, - }; - const response = await fetch(mcpUrl, { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify(body), - }); - const payload = (await response.json()) as { - result?: { content?: { type: string; text: string }[] }; - }; - const text = payload.result?.content?.[0]?.text; - if (typeof text !== "string") { - throw new Error(`Unexpected MCP response: ${JSON.stringify(payload)}`); - } - return text; -} - -export type CraftosResult = { - status: number | null; - signal: NodeJS.Signals | null; - output: string; -}; - -export type CraftosHandle = { - done: Promise; - abort: () => void; -}; - -export function startCraftos( - luaName: string, - opts: { computerId?: number; computerLabel?: string; mountRepo?: boolean; shellArgs?: string[]; timeoutMs?: number } = {}, -): CraftosHandle { - const timeoutMs = opts.timeoutMs ?? 15_000; - const controller = new AbortController(); - - const done = (async (): Promise => { - const dataDir = await mkdtemp(join(tmpdir(), "mcp-bridge-it-")); - const watchdog = setTimeout(() => controller.abort(), timeoutMs); - try { - const args: string[] = ["--directory", dataDir, "--headless", "--mount-ro", `/staging=${LUA_DIR}`]; - if (opts.computerId !== undefined) { - args.push("--id", String(opts.computerId)); - } - if (opts.mountRepo) { - if (!existsSync(join(REPO_ROOT, ".stage/apis"))) { - throw new Error( - ".stage/ is missing — run `just stage` (or use `just test-integration`) before the integration tests.", - ); - } - args.push( - "--mount-ro", `/trapos=${REPO_ROOT}`, - "--mount-ro", `/apis=${join(REPO_ROOT, ".stage/apis")}`, - "--mount-ro", `/programs=${join(REPO_ROOT, ".stage/programs")}`, - ); - } - if (process.platform === "darwin") { - args.push("--rom", "/Applications/CraftOS-PC.app/Contents/Resources"); - } - args.push("--exec", buildExecCode(luaName, opts.shellArgs ?? [], opts.computerLabel)); - - const chunks: Buffer[] = []; - const child = spawn("craftos", args, { signal: controller.signal }); - child.stdout.on("data", (d: Buffer) => chunks.push(d)); - child.stderr.on("data", (d: Buffer) => chunks.push(d)); - - const result = await new Promise<{ status: number | null; signal: NodeJS.Signals | null }>((resolve) => { - child.once("close", (code, signal) => resolve({ status: code, signal })); - child.once("error", () => resolve({ status: null, signal: null })); - }); - - return { status: result.status, signal: result.signal, output: Buffer.concat(chunks).toString("utf8") }; - } finally { - clearTimeout(watchdog); - await rm(dataDir, { recursive: true, force: true }); - } - })(); - - return { done, abort: () => controller.abort() }; -} - -export function formatFailure(message: string, craftosOutput: string): string { - const lines = ["\x1b[31mFAIL\x1b[0m " + message, "--- craftos output ---", craftosOutput.trimEnd(), "----------------------"]; - return lines.join("\n"); -} - -function buildExecCode(luaName: string, shellArgs: string[], computerLabel?: string): string { - const programPath = luaName.startsWith("/") ? luaName : `/staging/${luaName}`; - const parts = [luaQuote(programPath), ...shellArgs.map(luaQuote)]; - const setup = computerLabel === undefined ? "" : `os.setComputerLabel(${luaQuote(computerLabel)}); `; - return `${setup}shell.run(${parts.join(", ")})`; -} - -function luaQuote(value: string): string { - return `'${value.replaceAll("\\", "\\\\").replaceAll("'", "\\'")}'`; -} - -function waitForListening(server: Server | WebSocketServer): Promise { - return new Promise((resolve, reject) => { - const addr = "address" in server ? server.address() : null; - if (addr) { - resolve(); - return; - } - server.once("listening", () => resolve()); - server.once("error", reject); - }); -} - -function serverPort(server: Server | WebSocketServer): number { - const address = server.address(); - if (!address || typeof address === "string") { - throw new Error("server does not have a TCP address"); - } - return address.port; -} - -async function closeBridge(mcpServer: Server, linkServer: WebSocketServer): Promise { - for (const client of linkServer.clients) { - client.terminate(); - } - await new Promise((resolve) => linkServer.close(() => resolve())); - await new Promise((resolve) => mcpServer.close(() => resolve())); -} - -function sleep(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); -} diff --git a/tools/mcp-bridge/test-integration/lua/ai-cli-check.lua b/tools/mcp-bridge/test-integration/lua/ai-cli-check.lua deleted file mode 100644 index 835c6e8..0000000 --- a/tools/mcp-bridge/test-integration/lua/ai-cli-check.lua +++ /dev/null @@ -1,27 +0,0 @@ --- Runs the real /programs/ai.lua CLI against a bridge proxy URL. --- Usage: ai-cli-check -local args = { ... }; -local url = args[1]; - -settings.unset('opencc.server_url'); -settings.unset('opencc.session_id'); -settings.set('opencc.bridge_url', url); -settings.set('opencc.request_timeout_seconds', 10); -settings.save(); - -print('--- sessions ---'); -shell.run('/programs/ai.lua', 'sessions'); - -print('--- ping ---'); -shell.run('/programs/ai.lua', 'ping'); -print('SESSION_AFTER_PING=' .. tostring(settings.get('opencc.session_id'))); - -print('--- new ---'); -shell.run('/programs/ai.lua', 'new', 'fresh', 'start'); -print('SESSION_AFTER_NEW=' .. tostring(settings.get('opencc.session_id'))); - -print('--- ask ---'); -shell.run('/programs/ai.lua', 'continue', 'please'); -print('SESSION_AFTER_ASK=' .. tostring(settings.get('opencc.session_id'))); - -os.shutdown(); diff --git a/tools/mcp-bridge/test-integration/lua/echo-client.lua b/tools/mcp-bridge/test-integration/lua/echo-client.lua deleted file mode 100644 index b02c108..0000000 --- a/tools/mcp-bridge/test-integration/lua/echo-client.lua +++ /dev/null @@ -1,42 +0,0 @@ --- Integration-test helper: connect to the mcp-bridge link server, send a hello --- frame, then reply to every `request` with a `pong` response until a deadline --- expires. Args (via shell.run): id, label, urlBase, durationSeconds. -local args = {...}; - -local id = tonumber(args[1]) or 1; -local label = args[2] or "echo"; -local urlBase = args[3] or "ws://127.0.0.1:2001"; -local duration = tonumber(args[4]) or 5; - -local url = urlBase .. "/?id=" .. id; -local ws, err = http.websocket(url); -if not ws then - print("websocket failed: " .. tostring(err)); - os.shutdown(); - return; -end - -ws.send(textutils.serializeJSON({ - type = "hello", - computerId = id, - computerLabel = label, -})); - -local deadline = os.epoch("utc") + duration * 1000; -while os.epoch("utc") < deadline do - local msg = ws.receive(0.5); - if msg then - local frame = textutils.unserializeJSON(msg); - if frame and frame.type == "request" then - ws.send(textutils.serializeJSON({ - type = "response", - id = frame.id, - ok = true, - result = "pong from " .. id .. " (Label: " .. label .. ")", - })); - end - end -end - -pcall(function() ws.close(); end); -os.shutdown(); diff --git a/tools/mcp-bridge/test-integration/lua/multi-echo-client.lua b/tools/mcp-bridge/test-integration/lua/multi-echo-client.lua deleted file mode 100644 index 8dc3124..0000000 --- a/tools/mcp-bridge/test-integration/lua/multi-echo-client.lua +++ /dev/null @@ -1,44 +0,0 @@ --- Integration-test helper: open two websocket connections in parallel, each --- presenting itself as a distinct logical computer. Used by probe-multi. -local args = {...}; -local urlBase = args[1] or "ws://127.0.0.1:2001"; - -local function connect(id, label, duration) - local url = urlBase .. "/?id=" .. id; - local ws, err = http.websocket(url); - if not ws then - print("websocket failed (" .. id .. "): " .. tostring(err)); - return; - end - - ws.send(textutils.serializeJSON({ - type = "hello", - computerId = id, - computerLabel = label, - })); - - local deadline = os.epoch("utc") + duration * 1000; - while os.epoch("utc") < deadline do - local msg = ws.receive(0.3); - if msg then - local frame = textutils.unserializeJSON(msg); - if frame and frame.type == "request" then - ws.send(textutils.serializeJSON({ - type = "response", - id = frame.id, - ok = true, - result = "pong from " .. id .. " (Label: " .. label .. ")", - })); - end - end - end - - pcall(function() ws.close(); end); -end - -parallel.waitForAll( - function() connect(1001, "echo-A", 5); end, - function() connect(1002, "echo-B", 5); end -); - -os.shutdown(); diff --git a/tools/mcp-bridge/test-integration/lua/opencode-proxy-check.lua b/tools/mcp-bridge/test-integration/lua/opencode-proxy-check.lua deleted file mode 100644 index 63ebfa3..0000000 --- a/tools/mcp-bridge/test-integration/lua/opencode-proxy-check.lua +++ /dev/null @@ -1,15 +0,0 @@ --- Drives the real libhttpws transport against the bridge opencode proxy. --- Usage: opencode-proxy-check -local createHttpWs = require('/apis/libhttpws'); - -local args = { ... }; -local url = args[1]; - -local client = createHttpWs({ bridgeUrl = url, receiveTimeout = 10 }); -local body, code = client.postJson({ url = '' }, '/session/ses_x/message', { - parts = { { type = 'text', text = 'ping' } }, -}); -print('STATUS=' .. tostring(code)); -print('BODY=' .. tostring(body)); -client.close(); -os.shutdown(); diff --git a/tools/mcp-bridge/test-integration/lua/silent-client.lua b/tools/mcp-bridge/test-integration/lua/silent-client.lua deleted file mode 100644 index 9f4f52f..0000000 --- a/tools/mcp-bridge/test-integration/lua/silent-client.lua +++ /dev/null @@ -1,28 +0,0 @@ --- Integration-test helper: connect, send a hello frame, then ignore every --- request until the deadline expires. Used to prove the bridge's per-computer --- probe timeout path. -local args = {...}; - -local id = tonumber(args[1]) or 99; -local label = args[2] or "silent"; -local urlBase = args[3] or "ws://127.0.0.1:2001"; -local duration = tonumber(args[4]) or 5; - -local url = urlBase .. "/?id=" .. id; -local ws, err = http.websocket(url); -if not ws then - print("websocket failed: " .. tostring(err)); - os.shutdown(); - return; -end - -ws.send(textutils.serializeJSON({ - type = "hello", - computerId = id, - computerLabel = label, -})); - -sleep(duration); - -pcall(function() ws.close(); end); -os.shutdown(); diff --git a/tools/mcp-bridge/test-integration/opencode-proxy.test.ts b/tools/mcp-bridge/test-integration/opencode-proxy.test.ts deleted file mode 100644 index a1afaef..0000000 --- a/tools/mcp-bridge/test-integration/opencode-proxy.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; -import { createServer, type Server } from "node:http"; -import type { WebSocketServer } from "ws"; -import { formatFailure, startCraftos } from "./harness.js"; -import { startOpencodeProxy } from "../src/opencode-proxy.js"; - -test("libhttpws drives a synchronous opencode call through the bridge proxy", async () => { - const opencode = createServer((_req, res) => { - res.writeHead(200, { "content-type": "application/json" }); - res.end(JSON.stringify({ info: { finish: "stop" }, parts: [{ type: "text", text: "pong" }] })); - }); - await new Promise((resolve) => opencode.listen(0, "127.0.0.1", resolve)); - const opencodeUrl = `http://127.0.0.1:${serverPort(opencode)}`; - - const proxy = startOpencodeProxy({ host: "127.0.0.1", port: 0, opencodeUrl }); - await waitForListening(proxy); - const proxyUrl = `ws://127.0.0.1:${wssPort(proxy)}`; - - const craftos = startCraftos("opencode-proxy-check.lua", { - mountRepo: true, - shellArgs: [proxyUrl], - timeoutMs: 20_000, - }); - - try { - const result = await craftos.done; - assert.match(result.output, /STATUS=200/, formatFailure("expected STATUS=200", result.output)); - assert.match(result.output, /pong/, formatFailure("expected reply body to contain pong", result.output)); - } finally { - craftos.abort(); - await craftos.done.catch(() => undefined); - await closeWss(proxy); - await new Promise((resolve) => opencode.close(() => resolve())); - } -}); - -function serverPort(server: Server): number { - const address = server.address(); - if (!address || typeof address === "string") { - throw new Error("server has no TCP address"); - } - return address.port; -} - -function wssPort(proxy: WebSocketServer): number { - const address = proxy.address(); - if (!address || typeof address === "string") { - throw new Error("proxy has no TCP address"); - } - return address.port; -} - -function waitForListening(proxy: WebSocketServer): Promise { - return new Promise((resolve, reject) => { - if (proxy.address()) { - resolve(); - return; - } - proxy.once("listening", () => resolve()); - proxy.once("error", reject); - }); -} - -function closeWss(proxy: WebSocketServer): Promise { - for (const client of proxy.clients) { - client.terminate(); - } - return new Promise((resolve) => proxy.close(() => resolve())); -} diff --git a/tools/mcp-bridge/test-integration/probe-empty.test.ts b/tools/mcp-bridge/test-integration/probe-empty.test.ts deleted file mode 100644 index 196cae2..0000000 --- a/tools/mcp-bridge/test-integration/probe-empty.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; -import { callProbeComputers, startBridge } from "./harness.js"; - -test("probe-computers returns the no-computers message when nothing is connected", async () => { - const bridge = await startBridge(); - try { - assert.equal(await callProbeComputers(bridge.mcpUrl), "No computers connected."); - } finally { - await bridge.close(); - } -}); diff --git a/tools/mcp-bridge/test-integration/probe-happy.test.ts b/tools/mcp-bridge/test-integration/probe-happy.test.ts deleted file mode 100644 index 759ae88..0000000 --- a/tools/mcp-bridge/test-integration/probe-happy.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; -import { callProbeComputers, formatFailure, startBridge, startCraftos, waitForComputers } from "./harness.js"; - -test("probe-computers aggregates a single CraftOS echo computer", async () => { - const bridge = await startBridge(); - const craftos = startCraftos("echo-client.lua", { shellArgs: ["1", "echo-1", bridge.linkUrl, "8"] }); - try { - await waitForComputers(bridge.registry, 1, 12_000); - const text = await callProbeComputers(bridge.mcpUrl); - assert.equal(text, "pong from 1 (Label: echo-1)"); - } catch (error) { - craftos.abort(); - const result = await craftos.done; - throw new Error(formatFailure(error instanceof Error ? error.message : String(error), result.output), { cause: error }); - } finally { - craftos.abort(); - await craftos.done; - await bridge.close(); - } -}); diff --git a/tools/mcp-bridge/test-integration/probe-multi.test.ts b/tools/mcp-bridge/test-integration/probe-multi.test.ts deleted file mode 100644 index e6d7c65..0000000 --- a/tools/mcp-bridge/test-integration/probe-multi.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; -import { callProbeComputers, formatFailure, startBridge, startCraftos, waitForComputers } from "./harness.js"; - -test("probe-computers aggregates two CraftOS computers from the same headless process", async () => { - const bridge = await startBridge(); - const craftos = startCraftos("multi-echo-client.lua", { shellArgs: [bridge.linkUrl], timeoutMs: 15_000 }); - try { - await waitForComputers(bridge.registry, 2, 12_000); - const text = await callProbeComputers(bridge.mcpUrl); - const lines = text.split("\n").sort(); - assert.deepEqual(lines, [ - "pong from 1001 (Label: echo-A)", - "pong from 1002 (Label: echo-B)", - ]); - } catch (error) { - craftos.abort(); - const result = await craftos.done; - throw new Error(formatFailure(error instanceof Error ? error.message : String(error), result.output), { cause: error }); - } finally { - craftos.abort(); - await craftos.done; - await bridge.close(); - } -}); diff --git a/tools/mcp-bridge/test-integration/probe-silent.test.ts b/tools/mcp-bridge/test-integration/probe-silent.test.ts deleted file mode 100644 index 76f643d..0000000 --- a/tools/mcp-bridge/test-integration/probe-silent.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; -import { callProbeComputers, formatFailure, startBridge, startCraftos, waitForComputers } from "./harness.js"; - -test("probe-computers reports timeout for a connected computer that never replies", async () => { - const bridge = await startBridge(300); - const craftos = startCraftos("silent-client.lua", { shellArgs: ["7", "silent-7", bridge.linkUrl, "8"] }); - try { - await waitForComputers(bridge.registry, 1, 12_000); - const text = await callProbeComputers(bridge.mcpUrl); - assert.equal(text, "timeout from 7 (Label: silent-7)"); - } catch (error) { - craftos.abort(); - const result = await craftos.done; - throw new Error(formatFailure(error instanceof Error ? error.message : String(error), result.output), { cause: error }); - } finally { - craftos.abort(); - await craftos.done; - await bridge.close(); - } -}); diff --git a/tools/mcp-bridge/test/opencode-proxy.test.ts b/tools/mcp-bridge/test/opencode-proxy.test.ts deleted file mode 100644 index 005bbb2..0000000 --- a/tools/mcp-bridge/test/opencode-proxy.test.ts +++ /dev/null @@ -1,163 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; -import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http"; -import type { AddressInfo } from "node:net"; -import { WebSocket, type RawData, type WebSocketServer } from "ws"; -import { parseHttpRequestFrame, parseJsonFrame } from "../src/protocol.js"; -import { startOpencodeProxy } from "../src/opencode-proxy.js"; - -test("parseHttpRequestFrame accepts a valid frame", () => { - const frame = parseJsonFrame( - JSON.stringify({ type: "http", id: "req_1", method: "POST", path: "/session/x/message", body: "{}" }), - ); - assert.deepEqual(parseHttpRequestFrame(frame), { - type: "http", - id: "req_1", - method: "POST", - path: "/session/x/message", - body: "{}", - }); -}); - -test("parseHttpRequestFrame omits absent body", () => { - const frame = parseHttpRequestFrame({ type: "http", id: "req_1", method: "GET", path: "/session" }); - assert.deepEqual(frame, { type: "http", id: "req_1", method: "GET", path: "/session", body: undefined }); -}); - -test("parseHttpRequestFrame rejects invalid frames", () => { - assert.equal(parseHttpRequestFrame({ type: "http", id: "", method: "GET", path: "/x" }), null); - assert.equal(parseHttpRequestFrame({ type: "http", id: "a", method: "DELETE", path: "/x" }), null); - assert.equal(parseHttpRequestFrame({ type: "http", id: "a", method: "GET", path: "no-slash" }), null); - assert.equal(parseHttpRequestFrame({ type: "http", id: "a", method: "GET", path: "/x", body: 5 }), null); - assert.equal(parseHttpRequestFrame({ type: "other", id: "a", method: "GET", path: "/x" }), null); -}); - -test("proxy forwards a request to opencode and returns the raw response", async () => { - const seen: { method?: string; url?: string; auth?: string; body?: string } = {}; - const opencode = await startFakeOpencode((req, res, body) => { - seen.method = req.method; - seen.url = req.url; - seen.auth = req.headers.authorization; - seen.body = body; - res.writeHead(200, { "content-type": "application/json" }); - res.end(JSON.stringify({ info: { finish: "stop" }, parts: [{ type: "text", text: "pong" }] })); - }); - const proxy = startOpencodeProxy({ - host: "127.0.0.1", - port: 0, - opencodeUrl: opencode.url, - username: "opencode", - password: "secret", - }); - await waitForListening(proxy); - - try { - const response = await roundtrip(proxyUrl(proxy), { - type: "http", - id: "req_42", - method: "POST", - path: "/session/ses_1/message", - body: JSON.stringify({ parts: [{ type: "text", text: "ping" }] }), - }); - - assert.equal(response.type, "http-response"); - assert.equal(response.id, "req_42"); - assert.equal(response.status, 200); - const parsed = JSON.parse(response.body as string) as { parts: { text: string }[] }; - assert.equal(parsed.parts[0].text, "pong"); - - assert.equal(seen.method, "POST"); - assert.equal(seen.url, "/session/ses_1/message"); - assert.equal(seen.auth, "Basic " + Buffer.from("opencode:secret").toString("base64")); - assert.equal(seen.body, JSON.stringify({ parts: [{ type: "text", text: "ping" }] })); - } finally { - await closeWss(proxy); - await opencode.close(); - } -}); - -test("proxy maps a fetch failure to status 0", async () => { - // Point at a port with nothing listening so fetch rejects. - const proxy = startOpencodeProxy({ host: "127.0.0.1", port: 0, opencodeUrl: "http://127.0.0.1:1" }); - await waitForListening(proxy); - - try { - const response = await roundtrip(proxyUrl(proxy), { - type: "http", - id: "req_err", - method: "GET", - path: "/session", - }); - assert.equal(response.id, "req_err"); - assert.equal(response.status, 0); - assert.equal(typeof response.error, "string"); - } finally { - await closeWss(proxy); - } -}); - -type HttpResponse = { type: string; id: string; status: number; body?: string; error?: string }; - -function rawToString(data: RawData): string { - if (Array.isArray(data)) { - return Buffer.concat(data).toString("utf8"); - } - if (Buffer.isBuffer(data)) { - return data.toString("utf8"); - } - return Buffer.from(data).toString("utf8"); -} - -function roundtrip(url: string, frame: unknown): Promise { - return new Promise((resolve, reject) => { - const ws = new WebSocket(url); - ws.on("open", () => ws.send(JSON.stringify(frame))); - ws.on("message", (data: RawData) => { - ws.close(); - resolve(JSON.parse(rawToString(data)) as HttpResponse); - }); - ws.on("error", reject); - }); -} - -async function startFakeOpencode( - handler: (req: IncomingMessage, res: ServerResponse, body: string) => void, -): Promise<{ url: string; close: () => Promise }> { - const server = createServer((req, res) => { - const chunks: Buffer[] = []; - req.on("data", (c: Buffer) => chunks.push(c)); - req.on("end", () => handler(req, res, Buffer.concat(chunks).toString("utf8"))); - }); - await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); - const port = (server.address() as AddressInfo).port; - return { - url: `http://127.0.0.1:${port}`, - close: () => new Promise((resolve) => server.close(() => resolve())), - }; -} - -function proxyUrl(proxy: WebSocketServer): string { - const address = proxy.address(); - if (!address || typeof address === "string") { - throw new Error("proxy has no TCP address"); - } - return `ws://127.0.0.1:${address.port}`; -} - -function waitForListening(server: WebSocketServer | Server): Promise { - return new Promise((resolve, reject) => { - if (server.address()) { - resolve(); - return; - } - server.once("listening", () => resolve()); - server.once("error", reject); - }); -} - -function closeWss(proxy: WebSocketServer): Promise { - for (const client of proxy.clients) { - client.terminate(); - } - return new Promise((resolve) => proxy.close(() => resolve())); -} diff --git a/tools/mcp-bridge/test/probe-computers.test.ts b/tools/mcp-bridge/test/probe-computers.test.ts deleted file mode 100644 index fad5fb7..0000000 --- a/tools/mcp-bridge/test/probe-computers.test.ts +++ /dev/null @@ -1,377 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; -import { WebSocket } from "ws"; -import { LinkRegistry } from "../src/link-server.js"; -import { handleMcpRequest } from "../src/mcp-server.js"; - -test("probe-computers returns no computers message when registry is empty", async () => { - const registry = new LinkRegistry(); - assert.equal(await registry.probeComputers(10), "No computers connected."); -}); - -test("probe-computers aggregates multiple successful responses", async () => { - const registry = new LinkRegistry(); - const computer1 = new FakeSocket(); - const computer2 = new FakeSocket(); - - registry.register({ computerId: 12, label: "base-turtle", ws: computer1 as unknown as WebSocket, connectedAt: 1, lastSeenAt: 1 }); - registry.register({ computerId: 13, label: "miner-1", ws: computer2 as unknown as WebSocket, connectedAt: 1, lastSeenAt: 1 }); - - const promise = registry.probeComputers(50); - computer1.respond(registry, 12, "pong from 12 (Label: base-turtle)"); - computer2.respond(registry, 13, "pong from 13 (Label: miner-1)"); - - assert.equal(await promise, "pong from 12 (Label: base-turtle)\npong from 13 (Label: miner-1)"); -}); - -test("probe-computers reports timeout for a connected computer that does not answer", async () => { - const registry = new LinkRegistry(); - registry.register({ computerId: 14, label: "farm-turtle", ws: new FakeSocket() as unknown as WebSocket, connectedAt: 1, lastSeenAt: 1 }); - - assert.equal(await registry.probeComputers(5), "timeout from 14 (Label: farm-turtle)"); -}); - -test("MCP tool call returns text content", async () => { - const registry = new LinkRegistry(); - const response = await handleMcpRequest( - { jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "probe-computers", arguments: {} } }, - registry, - 10, - ); - - assert.deepEqual(response, { - jsonrpc: "2.0", - id: 1, - result: { content: [{ type: "text", text: "No computers connected." }] }, - }); -}); - -test("MCP tools/list includes exec-lua schema", async () => { - const registry = new LinkRegistry(); - const response = await handleMcpRequest({ jsonrpc: "2.0", id: 1, method: "tools/list" }, registry, 10); - - assert.deepEqual(response, { - jsonrpc: "2.0", - id: 1, - result: { - tools: [ - { - name: "probe-computers", - description: "Probe all linked ComputerCraft computers.", - inputSchema: { type: "object", properties: {}, additionalProperties: false }, - }, - { - name: "exec-lua", - description: "Execute Lua code on a linked ComputerCraft computer.", - inputSchema: { - type: "object", - properties: { - computerId: { type: "number", description: "ComputerCraft computer id to execute on." }, - code: { type: "string", description: "Lua source code to execute." }, - timeoutMs: { type: "number", description: "Optional host-side timeout in milliseconds, max 30000." }, - }, - required: ["computerId", "code"], - additionalProperties: false, - }, - }, - { - name: "write-file", - description: "Write file content on a linked ComputerCraft computer, overwriting any existing file.", - inputSchema: { - type: "object", - properties: { - computerId: { type: "number", description: "ComputerCraft computer id to write on." }, - path: { type: "string", description: "Target path on the ComputerCraft computer." }, - content: { type: "string", description: "File content to write. Empty strings are allowed." }, - timeoutMs: { type: "number", description: "Optional host-side timeout in milliseconds, max 30000." }, - }, - required: ["computerId", "path", "content"], - additionalProperties: false, - }, - }, - { - name: "run-file", - description: "Run a Lua file on a linked ComputerCraft computer with captured print/write output.", - inputSchema: { - type: "object", - properties: { - computerId: { type: "number", description: "ComputerCraft computer id to run on." }, - path: { type: "string", description: "Lua file path on the ComputerCraft computer." }, - args: { type: "array", items: { type: "string" }, description: "Optional program arguments passed as Lua varargs." }, - timeoutMs: { type: "number", description: "Optional host-side timeout in milliseconds, max 30000." }, - }, - required: ["computerId", "path"], - additionalProperties: false, - }, - }, - ], - }, - }); -}); - -test("exec-lua sends code to the selected computer", async () => { - const registry = new LinkRegistry(); - const computer = new FakeSocket(); - registry.register({ computerId: 12, label: "base-turtle", ws: computer as unknown as WebSocket, connectedAt: 1, lastSeenAt: 1 }); - - const promise = registry.execLua(12, "print('hi') return 2 + 3", 50); - assert.deepEqual(computer.lastRequest(), { - type: "request", - id: computer.lastRequest().id, - method: "exec-lua", - params: { code: "print('hi') return 2 + 3" }, - }); - - computer.respond(registry, 12, { returns: [{ type: "number", value: 5 }], output: "hi\n" }); - assert.equal( - await promise, - 'computer: 12 (Label: base-turtle)\nok: true\nreturns: [{"type":"number","value":5}]\noutput:\nhi', - ); -}); - -test("exec-lua reports unknown computer", async () => { - const registry = new LinkRegistry(); - - assert.equal(await registry.execLua(99, "return 1", 50), "No computer with id 99 connected."); -}); - -test("exec-lua formats computer error responses", async () => { - const registry = new LinkRegistry(); - const computer = new FakeSocket(); - registry.register({ computerId: 12, label: "base-turtle", ws: computer as unknown as WebSocket, connectedAt: 1, lastSeenAt: 1 }); - - const promise = registry.execLua(12, "print('before'); error('boom')", 50); - computer.respondError(registry, 12, "boom", { output: "before\n" }); - - assert.equal(await promise, "computer: 12 (Label: base-turtle)\nok: false\nerror: boom\noutput:\nbefore"); -}); - -test("exec-lua formats ok false execution results", async () => { - const registry = new LinkRegistry(); - const computer = new FakeSocket(); - registry.register({ computerId: 12, label: "base-turtle", ws: computer as unknown as WebSocket, connectedAt: 1, lastSeenAt: 1 }); - - const promise = registry.execLua(12, "print('before'); error('boom')", 50); - computer.respond(registry, 12, { ok: false, error: "boom", output: "before\n" }); - - assert.equal(await promise, "computer: 12 (Label: base-turtle)\nok: false\nerror: boom\noutput:\nbefore"); -}); - -test("exec-lua reports timeouts", async () => { - const registry = new LinkRegistry(); - registry.register({ computerId: 12, label: "base-turtle", ws: new FakeSocket() as unknown as WebSocket, connectedAt: 1, lastSeenAt: 1 }); - - assert.equal(await registry.execLua(12, "while true do end", 5), "computer: 12 (Label: base-turtle)\nok: false\nerror: timeout"); -}); - -test("run-file sends path to the selected computer", async () => { - const registry = new LinkRegistry(); - const computer = new FakeSocket(); - registry.register({ computerId: 12, label: "base-turtle", ws: computer as unknown as WebSocket, connectedAt: 1, lastSeenAt: 1 }); - - const promise = registry.runFile(12, "/helloworld.lua", ["install", "pkg"], 50); - assert.deepEqual(computer.lastRequest(), { - type: "request", - id: computer.lastRequest().id, - method: "run-file", - params: { path: "/helloworld.lua", args: ["install", "pkg"] }, - }); - - computer.respond(registry, 12, { returns: [], output: "Blah!\n" }); - assert.equal(await promise, "computer: 12 (Label: base-turtle)\nok: true\nreturns: []\noutput:\nBlah!"); -}); - -test("run-file reports unknown computer", async () => { - const registry = new LinkRegistry(); - - assert.equal(await registry.runFile(99, "/helloworld.lua", [], 50), "No computer with id 99 connected."); -}); - -test("MCP run-file validates required arguments", async () => { - const registry = new LinkRegistry(); - const response = await handleMcpRequest( - { jsonrpc: "2.0", id: 2, method: "tools/call", params: { name: "run-file", arguments: { computerId: 1 } } }, - registry, - 10, - ); - - assert.deepEqual(response, { - jsonrpc: "2.0", - id: 2, - error: { code: -32602, message: "path must be a non-empty string" }, - }); -}); - -test("MCP run-file returns text content", async () => { - const registry = new LinkRegistry(); - const computer = new FakeSocket(); - registry.register({ computerId: 12, label: "base-turtle", ws: computer as unknown as WebSocket, connectedAt: 1, lastSeenAt: 1 }); - - const responsePromise = handleMcpRequest( - { - jsonrpc: "2.0", - id: 2, - method: "tools/call", - params: { name: "run-file", arguments: { computerId: 12, path: "/helloworld.lua", args: ["a", "b"] } }, - }, - registry, - 10, - ); - computer.respond(registry, 12, { returns: [], output: "Blah!\n" }); - - assert.deepEqual(computer.lastRequest().params, { path: "/helloworld.lua", args: ["a", "b"] }); - - assert.deepEqual(await responsePromise, { - jsonrpc: "2.0", - id: 2, - result: { content: [{ type: "text", text: "computer: 12 (Label: base-turtle)\nok: true\nreturns: []\noutput:\nBlah!" }] }, - }); -}); - -test("MCP exec-lua validates required arguments", async () => { - const registry = new LinkRegistry(); - const response = await handleMcpRequest( - { jsonrpc: "2.0", id: 2, method: "tools/call", params: { name: "exec-lua", arguments: { computerId: 1 } } }, - registry, - 10, - ); - - assert.deepEqual(response, { - jsonrpc: "2.0", - id: 2, - error: { code: -32602, message: "code must be a non-empty string" }, - }); -}); - -test("MCP exec-lua validates timeoutMs", async () => { - const registry = new LinkRegistry(); - const response = await handleMcpRequest( - { jsonrpc: "2.0", id: 2, method: "tools/call", params: { name: "exec-lua", arguments: { computerId: 1, code: "return 1", timeoutMs: 0 } } }, - registry, - 10, - ); - - assert.deepEqual(response, { - jsonrpc: "2.0", - id: 2, - error: { code: -32602, message: "timeoutMs must be a positive finite number" }, - }); -}); - -test("write-file sends path and content to the selected computer", async () => { - const registry = new LinkRegistry(); - const computer = new FakeSocket(); - registry.register({ computerId: 12, label: "base-turtle", ws: computer as unknown as WebSocket, connectedAt: 1, lastSeenAt: 1 }); - - const promise = registry.writeFile(12, "notes/todo.txt", "hello\nworld", 50); - assert.deepEqual(computer.lastRequest(), { - type: "request", - id: computer.lastRequest().id, - method: "write-file", - params: { path: "notes/todo.txt", content: "hello\nworld" }, - }); - - computer.respond(registry, 12, { path: "notes/todo.txt", bytes: 11 }); - assert.equal(await promise, "computer: 12 (Label: base-turtle)\nok: true\npath: notes/todo.txt\nbytes: 11"); -}); - -test("write-file reports unknown computer", async () => { - const registry = new LinkRegistry(); - - assert.equal(await registry.writeFile(99, "note.txt", "hello", 50), "No computer with id 99 connected."); -}); - -test("write-file formats computer error responses", async () => { - const registry = new LinkRegistry(); - const computer = new FakeSocket(); - registry.register({ computerId: 12, label: "base-turtle", ws: computer as unknown as WebSocket, connectedAt: 1, lastSeenAt: 1 }); - - const promise = registry.writeFile(12, "missing/note.txt", "hello", 50); - computer.respondError(registry, 12, "No such file", {}); - - assert.equal(await promise, "computer: 12 (Label: base-turtle)\nok: false\nerror: No such file"); -}); - -test("write-file formats ok false operation results", async () => { - const registry = new LinkRegistry(); - const computer = new FakeSocket(); - registry.register({ computerId: 12, label: "base-turtle", ws: computer as unknown as WebSocket, connectedAt: 1, lastSeenAt: 1 }); - - const promise = registry.writeFile(12, "missing/note.txt", "hello", 50); - computer.respond(registry, 12, { ok: false, error: "No such file" }); - - assert.equal(await promise, "computer: 12 (Label: base-turtle)\nok: false\nerror: No such file"); -}); - -test("write-file reports timeouts", async () => { - const registry = new LinkRegistry(); - registry.register({ computerId: 12, label: "base-turtle", ws: new FakeSocket() as unknown as WebSocket, connectedAt: 1, lastSeenAt: 1 }); - - assert.equal(await registry.writeFile(12, "note.txt", "hello", 5), "computer: 12 (Label: base-turtle)\nok: false\nerror: timeout"); -}); - -test("MCP write-file validates required arguments", async () => { - const registry = new LinkRegistry(); - const response = await handleMcpRequest( - { jsonrpc: "2.0", id: 2, method: "tools/call", params: { name: "write-file", arguments: { computerId: 1, content: "hello" } } }, - registry, - 10, - ); - - assert.deepEqual(response, { - jsonrpc: "2.0", - id: 2, - error: { code: -32602, message: "path must be a non-empty string" }, - }); -}); - -test("MCP write-file allows empty content", async () => { - const registry = new LinkRegistry(); - const computer = new FakeSocket(); - registry.register({ computerId: 12, label: "base-turtle", ws: computer as unknown as WebSocket, connectedAt: 1, lastSeenAt: 1 }); - - const responsePromise = handleMcpRequest( - { jsonrpc: "2.0", id: 2, method: "tools/call", params: { name: "write-file", arguments: { computerId: 12, path: "empty.txt", content: "" } } }, - registry, - 10, - ); - computer.respond(registry, 12, { path: "empty.txt", bytes: 0 }); - - assert.deepEqual(await responsePromise, { - jsonrpc: "2.0", - id: 2, - result: { content: [{ type: "text", text: "computer: 12 (Label: base-turtle)\nok: true\npath: empty.txt\nbytes: 0" }] }, - }); -}); - -class FakeSocket { - sent: unknown[] = []; - - send(data: unknown): void { - this.sent.push(data); - } - - close(): void { - return; - } - - lastRequest(): { id: string; type: string; method: string; params?: unknown } { - const last = this.sent.at(-1); - assert.equal(typeof last, "string"); - return JSON.parse(last as string) as { id: string; type: string; method: string; params?: unknown }; - } - - respond(registry: LinkRegistry, computerId: number, result: unknown): void { - const request = this.lastRequest(); - registry.handleFrame(this as unknown as WebSocket, JSON.stringify({ type: "response", id: request.id, ok: true, result })); - - assert.equal(computerId > 0, true); - } - - respondError(registry: LinkRegistry, computerId: number, error: string, result: unknown): void { - const request = this.lastRequest(); - registry.handleFrame(this as unknown as WebSocket, JSON.stringify({ type: "response", id: request.id, ok: false, error, result })); - - assert.equal(computerId > 0, true); - } -} diff --git a/tools/mcp-bridge/test/protocol.test.ts b/tools/mcp-bridge/test/protocol.test.ts deleted file mode 100644 index e102ec2..0000000 --- a/tools/mcp-bridge/test/protocol.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; -import { parseJsonFrame, parseLinkMessage } from "../src/protocol.js"; - -test("protocol validation accepts valid hello", () => { - const frame = parseJsonFrame(JSON.stringify({ type: "hello", computerId: 12, computerLabel: "base-turtle" })); - assert.deepEqual(parseLinkMessage(frame), { type: "hello", computerId: 12, computerLabel: "base-turtle" }); -}); - -test("protocol validation normalizes empty label", () => { - const frame = parseJsonFrame(JSON.stringify({ type: "hello", computerId: 12, computerLabel: "" })); - assert.deepEqual(parseLinkMessage(frame), { type: "hello", computerId: 12, computerLabel: null }); -}); - -test("protocol validation rejects invalid frames", () => { - assert.equal(parseJsonFrame("not json"), null); - assert.equal(parseLinkMessage({ type: "hello", computerId: "12" }), null); - assert.equal(parseLinkMessage({ type: "response", id: 1, ok: true }), null); -}); diff --git a/tools/mcp-bridge/tsconfig.json b/tools/mcp-bridge/tsconfig.json deleted file mode 100644 index 41d9509..0000000 --- a/tools/mcp-bridge/tsconfig.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "strict": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "skipLibCheck": true, - "outDir": "dist", - "rootDir": ".", - "declaration": true, - "incremental": true, - "tsBuildInfoFile": "node_modules/.cache/tsc/tsconfig.tsbuildinfo" - }, - "include": ["src/**/*.ts", "test/**/*.ts", "test-integration/**/*.ts"] -}