From 6a22a5155b2857643e7961032a1dcabacc056ef9 Mon Sep 17 00:00:00 2001 From: SysAdmin Date: Mon, 16 Mar 2026 22:19:52 +0000 Subject: [PATCH] feat: add GrabCraft blueprints, building helpers, and world state awareness - Phase 1: Player position (querytarget @s) and testforblock tools - Phase 2: GrabCraft scraper with LRU cache, 372-block Java-to-Bedrock mapping, search and auto-build blueprint tools with dryRun support - Phase 3: Raise build limit to 5000 (MAX_BUILD_COMMANDS env), add progress notifications and build cancellation - Phase 4: Geometric shape builders (sphere, cylinder, dome, pyramid, wall, box) using fill commands for efficiency - Phase 5: Event buffer 100->1000 (EVENT_BUFFER_SIZE env), add getByTypes and getSince query methods - Phase 6: MCP resources for block ID reference and GrabCraft categories Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitea/workflows/deploy.yml | 84 +- .gitignore | 8 +- Dockerfile | 28 +- docker-compose.yml | 24 +- package-lock.json | 3104 +++++++++++++++++------------------ package.json | 42 +- src/bedrock-ws.js | 606 ++++--- src/block-map.js | 692 ++++++++ src/building-helpers.js | 272 +++ src/command-queue.js | 422 +++-- src/encryption.js | 107 ++ src/event-store.js | 148 +- src/grabcraft.js | 396 +++++ src/index.js | 72 +- src/mcp-server.js | 1286 ++++++++++----- src/utils.js | 188 ++- 16 files changed, 4843 insertions(+), 2636 deletions(-) create mode 100644 src/block-map.js create mode 100644 src/building-helpers.js create mode 100644 src/encryption.js create mode 100644 src/grabcraft.js diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 4a95844..f76f4f9 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -1,42 +1,42 @@ -name: Deploy to Docker - -on: - push: - branches: [main] - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - name: Deploy via SSH - uses: appleboy/ssh-action@v1 - with: - host: ${{ secrets.DEPLOY_HOST }} - username: ${{ secrets.DEPLOY_USER }} - password: ${{ secrets.DEPLOY_PASSWORD }} - port: 22 - script: | - set -e - APP_DIR="$HOME/mc-ai-bridge" - - # First run: clone. Subsequent: pull. - if [ ! -d "$APP_DIR/.git" ]; then - git clone https://git.silverlabs.uk/SilverLABS/mc-ai-bridge.git "$APP_DIR" - else - cd "$APP_DIR" - git fetch origin main - git reset --hard origin/main - fi - - cd "$APP_DIR" - - # Build and deploy - docker compose down --remove-orphans || true - docker compose build --no-cache - docker compose up -d - - # Verify container is running - sleep 5 - docker compose ps - echo "--- Health check ---" - curl -sf http://localhost:3002/health || echo "Health check not yet available (container may still be starting)" +name: Deploy to Docker + +on: + push: + branches: [main] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Deploy via SSH + uses: appleboy/ssh-action@v1 + with: + host: ${{ secrets.DEPLOY_HOST }} + username: ${{ secrets.DEPLOY_USER }} + password: ${{ secrets.DEPLOY_PASSWORD }} + port: 22 + script: | + set -e + APP_DIR="$HOME/mc-ai-bridge" + + # First run: clone. Subsequent: pull. + if [ ! -d "$APP_DIR/.git" ]; then + git clone https://git.silverlabs.uk/SilverLABS/mc-ai-bridge.git "$APP_DIR" + else + cd "$APP_DIR" + git fetch origin main + git reset --hard origin/main + fi + + cd "$APP_DIR" + + # Build and deploy + docker compose down --remove-orphans || true + docker compose build --no-cache + docker compose up -d + + # Verify container is running + sleep 5 + docker compose ps + echo "--- Health check ---" + curl -sf http://localhost:3002/health || echo "Health check not yet available (container may still be starting)" diff --git a/.gitignore b/.gitignore index daf1dd0..ca6d62c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -node_modules/ -.env -*.log -.DS_Store +node_modules/ +.env +*.log +.DS_Store diff --git a/Dockerfile b/Dockerfile index 97ff1ce..f40572b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,14 @@ -FROM node:22-alpine - -WORKDIR /app - -COPY package.json package-lock.json* ./ -RUN npm ci --production 2>/dev/null || npm install --production - -COPY src/ ./src/ - -EXPOSE 3001 3002 - -USER node - -CMD ["node", "src/index.js"] +FROM node:22-alpine + +WORKDIR /app + +COPY package.json package-lock.json* ./ +RUN npm ci --production 2>/dev/null || npm install --production + +COPY src/ ./src/ + +EXPOSE 3001 3002 + +USER node + +CMD ["node", "src/index.js"] diff --git a/docker-compose.yml b/docker-compose.yml index a9f3672..9293c9c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,12 +1,12 @@ -services: - mc-ai-bridge: - build: . - container_name: mc-ai-bridge - ports: - - "3001:3001" # Minecraft WebSocket - - "3002:3002" # MCP SSE transport - environment: - - WS_PORT=3001 - - MCP_PORT=3002 - - NODE_ENV=production - restart: unless-stopped +services: + mc-ai-bridge: + build: . + container_name: mc-ai-bridge + ports: + - "3001:3001" # Minecraft WebSocket + - "3002:3002" # MCP SSE transport + environment: + - WS_PORT=3001 + - MCP_PORT=3002 + - NODE_ENV=production + restart: unless-stopped diff --git a/package-lock.json b/package-lock.json index c39337d..5a878a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,1552 +1,1552 @@ -{ - "name": "mc-ai-bridge", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "mc-ai-bridge", - "version": "1.0.0", - "dependencies": { - "@modelcontextprotocol/sdk": "^1.27.1", - "express": "^4.21.2", - "ws": "^8.18.0", - "zod": "^3.25.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@hono/node-server": { - "version": "1.19.11", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", - "integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==", - "license": "MIT", - "engines": { - "node": ">=18.14.1" - }, - "peerDependencies": { - "hono": "^4" - } - }, - "node_modules/@modelcontextprotocol/sdk": { - "version": "1.27.1", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.27.1.tgz", - "integrity": "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==", - "license": "MIT", - "dependencies": { - "@hono/node-server": "^1.19.9", - "ajv": "^8.17.1", - "ajv-formats": "^3.0.1", - "content-type": "^1.0.5", - "cors": "^2.8.5", - "cross-spawn": "^7.0.5", - "eventsource": "^3.0.2", - "eventsource-parser": "^3.0.0", - "express": "^5.2.1", - "express-rate-limit": "^8.2.1", - "hono": "^4.11.4", - "jose": "^6.1.3", - "json-schema-typed": "^8.0.2", - "pkce-challenge": "^5.0.0", - "raw-body": "^3.0.0", - "zod": "^3.25 || ^4.0", - "zod-to-json-schema": "^3.25.1" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@cfworker/json-schema": "^4.1.1", - "zod": "^3.25 || ^4.0" - }, - "peerDependenciesMeta": { - "@cfworker/json-schema": { - "optional": true - }, - "zod": { - "optional": false - } - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/body-parser": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", - "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.3", - "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", - "on-finished": "^2.4.1", - "qs": "^6.14.1", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", - "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/express": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.1", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "depd": "^2.0.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/finalhandler": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.1", - "mime-types": "^3.0.2", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/serve-static": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", - "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT" - }, - "node_modules/body-parser": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", - "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "~1.2.0", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "on-finished": "~2.4.1", - "qs": "~6.14.0", - "raw-body": "~2.5.3", - "type-is": "~1.6.18", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/body-parser/node_modules/raw-body": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", - "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", - "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", - "license": "MIT" - }, - "node_modules/cors": { - "version": "2.8.6", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", - "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/eventsource": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", - "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", - "license": "MIT", - "dependencies": { - "eventsource-parser": "^3.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/eventsource-parser": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", - "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/express": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", - "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "~1.20.3", - "content-disposition": "~0.5.4", - "content-type": "~1.0.4", - "cookie": "~0.7.1", - "cookie-signature": "~1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.3.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "~0.1.12", - "proxy-addr": "~2.0.7", - "qs": "~6.14.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "~0.19.0", - "serve-static": "~1.16.2", - "setprototypeof": "1.2.0", - "statuses": "~2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express-rate-limit": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.0.tgz", - "integrity": "sha512-KJzBawY6fB9FiZGdE/0aftepZ91YlaGIrV8vgblRM3J8X+dHx/aiowJWwkx6LIGyuqGiANsjSwwrbb8mifOJ4Q==", - "license": "MIT", - "dependencies": { - "ip-address": "10.1.0" - }, - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": ">= 4.11" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" - }, - "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/finalhandler": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", - "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "statuses": "~2.0.2", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hono": { - "version": "4.12.5", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.5.tgz", - "integrity": "sha512-3qq+FUBtlTHhtYxbxheZgY8NIFnkkC/MR8u5TTsr7YZ3wixryQ3cCwn3iZbg8p8B88iDBBAYSfZDS75t8MN7Vg==", - "license": "MIT", - "engines": { - "node": ">=16.9.0" - } - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/jose": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.0.tgz", - "integrity": "sha512-xsfE1TcSCbUdo6U07tR0mvhg0flGxU8tPLbF03mirl2ukGQENhUg4ubGYQnhVH0b5stLlPM+WOqDkEl1R1y5sQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, - "node_modules/json-schema-typed": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", - "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", - "license": "BSD-2-Clause" - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", - "license": "MIT" - }, - "node_modules/pkce-challenge": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", - "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", - "license": "MIT", - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/qs": { - "version": "6.14.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", - "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/raw-body/node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/router/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/router/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/router/node_modules/path-to-regexp": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/send": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", - "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.1", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "~2.4.1", - "range-parser": "~1.2.1", - "statuses": "~2.0.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/serve-static": { - "version": "1.16.3", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", - "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", - "license": "MIT", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "~0.19.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, - "node_modules/ws": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", - "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", - "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/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-to-json-schema": { - "version": "3.25.1", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", - "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", - "license": "ISC", - "peerDependencies": { - "zod": "^3.25 || ^4" - } - } - } -} +{ + "name": "mc-ai-bridge", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mc-ai-bridge", + "version": "1.0.0", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.27.1", + "express": "^4.21.2", + "ws": "^8.18.0", + "zod": "^3.25.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", + "integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.27.1.tgz", + "integrity": "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.0.tgz", + "integrity": "sha512-KJzBawY6fB9FiZGdE/0aftepZ91YlaGIrV8vgblRM3J8X+dHx/aiowJWwkx6LIGyuqGiANsjSwwrbb8mifOJ4Q==", + "license": "MIT", + "dependencies": { + "ip-address": "10.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.5", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.5.tgz", + "integrity": "sha512-3qq+FUBtlTHhtYxbxheZgY8NIFnkkC/MR8u5TTsr7YZ3wixryQ3cCwn3iZbg8p8B88iDBBAYSfZDS75t8MN7Vg==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.0.tgz", + "integrity": "sha512-xsfE1TcSCbUdo6U07tR0mvhg0flGxU8tPLbF03mirl2ukGQENhUg4ubGYQnhVH0b5stLlPM+WOqDkEl1R1y5sQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/router/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/router/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/router/node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "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/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", + "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" + } + } + } +} diff --git a/package.json b/package.json index dd92bdc..8fd032d 100644 --- a/package.json +++ b/package.json @@ -1,21 +1,21 @@ -{ - "name": "mc-ai-bridge", - "version": "1.0.0", - "description": "MCP server bridging Minecraft Bedrock Edition to Claude Code via WebSocket", - "type": "module", - "main": "src/index.js", - "scripts": { - "start": "node src/index.js", - "dev": "node --watch src/index.js" - }, - "dependencies": { - "@modelcontextprotocol/sdk": "^1.27.1", - "express": "^4.21.2", - "ws": "^8.18.0", - "zod": "^3.25.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "private": true -} +{ + "name": "mc-ai-bridge", + "version": "1.0.0", + "description": "MCP server bridging Minecraft Bedrock Edition to Claude Code via WebSocket", + "type": "module", + "main": "src/index.js", + "scripts": { + "start": "node src/index.js", + "dev": "node --watch src/index.js" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.27.1", + "express": "^4.21.2", + "ws": "^8.18.0", + "zod": "^3.25.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "private": true +} diff --git a/src/bedrock-ws.js b/src/bedrock-ws.js index 433e762..592a09e 100644 --- a/src/bedrock-ws.js +++ b/src/bedrock-ws.js @@ -1,224 +1,382 @@ -import { WebSocketServer } from 'ws'; -import { createSubscribeMessage, createCommandMessage, sanitize, log, logError } from './utils.js'; -import { EventStore } from './event-store.js'; -import { CommandQueue } from './command-queue.js'; - -const TAG = 'BedrockWS'; - -/** - * WebSocket server that accepts a connection from Minecraft Bedrock Edition. - * Only one Minecraft client is supported at a time. - */ -export class BedrockWebSocket { - /** - * @param {object} opts - * @param {number} opts.port - WebSocket listen port (default 3001) - */ - constructor(opts = {}) { - this.port = opts.port ?? 3001; - this.events = new EventStore(100); - this.commandQueue = new CommandQueue(); - - /** @type {import('ws').WebSocket | null} */ - this._ws = null; - this._wss = null; - this._connectedAt = null; - this._playerName = null; - this._subscriptions = new Set(); - } - - /** Start the WebSocket server */ - start() { - this._wss = new WebSocketServer({ port: this.port }); - - this._wss.on('listening', () => { - log(TAG, `WebSocket server listening on port ${this.port}`); - log(TAG, `In Minecraft, type: /connect ws://:${this.port}`); - }); - - this._wss.on('connection', (ws) => { - // Only allow one connection at a time - if (this._ws) { - log(TAG, 'Rejecting new connection - already have an active client'); - ws.close(1000, 'Only one Minecraft client supported'); - return; - } - - this._ws = ws; - this._connectedAt = new Date(); - log(TAG, 'Minecraft client connected!'); - - // Wire up command queue to send over this socket - this.commandQueue.setSendFunction((id, message) => { - if (this._ws && this._ws.readyState === 1) { - this._ws.send(message); - } else { - throw new Error('WebSocket not connected'); - } - }); - - // Auto-subscribe to key events - this._autoSubscribe(); - - ws.on('message', (raw) => { - try { - const data = JSON.parse(raw.toString()); - this._handleMessage(data); - } catch (err) { - logError(TAG, 'Failed to parse message:', err.message); - } - }); - - ws.on('close', (code, reason) => { - log(TAG, `Minecraft disconnected (code: ${code}, reason: ${reason || 'none'})`); - this._ws = null; - this._connectedAt = null; - this._playerName = null; - this._subscriptions.clear(); - this.commandQueue.setSendFunction(null); - }); - - ws.on('error', (err) => { - logError(TAG, 'WebSocket error:', err.message); - }); - }); - - this._wss.on('error', (err) => { - logError(TAG, 'Server error:', err.message); - }); - } - - /** Subscribe to default event types */ - _autoSubscribe() { - const defaultEvents = ['PlayerMessage']; - for (const eventName of defaultEvents) { - this.subscribe(eventName); - } - } - - /** - * Subscribe to a Bedrock event type. - * @param {string} eventName - */ - subscribe(eventName) { - if (!this._ws || this._ws.readyState !== 1) { - log(TAG, `Cannot subscribe to ${eventName} - not connected`); - return false; - } - - if (this._subscriptions.has(eventName)) { - log(TAG, `Already subscribed to ${eventName}`); - return true; - } - - this._ws.send(createSubscribeMessage(eventName)); - this._subscriptions.add(eventName); - log(TAG, `Subscribed to ${eventName}`); - return true; - } - - /** - * Handle an incoming WebSocket message from Bedrock. - * @param {object} data - Parsed JSON message - */ - _handleMessage(data) { - const purpose = data?.header?.messagePurpose; - - if (purpose === 'commandResponse') { - // Response to a command we sent - const requestId = data.header.requestId; - this.commandQueue.handleResponse(requestId, data.body); - return; - } - - if (purpose === 'event') { - const eventName = data.header.eventName; - const body = data.body || {}; - - // Filter bot's own messages to prevent echo loops - if (eventName === 'PlayerMessage') { - const sender = body.sender || ''; - const message = sanitize(body.message || ''); - const type = body.type || 'chat'; - - // Skip messages from external sources (commands, say, tell from server) - if (type !== 'chat' || sender === 'External' || sender === '') { - return; - } - - // Track player name from first chat message - if (!this._playerName && sender) { - this._playerName = sender; - log(TAG, `Player identified: ${this._playerName}`); - } - - this.events.push(eventName, { - sender, - message, - type, - }); - - log(TAG, `[Chat] <${sender}> ${message}`); - return; - } - - // Store all other events - this.events.push(eventName, body); - } - } - - /** - * Send a command to Minecraft. - * @param {string} commandLine - e.g. "give @p diamond 64" - * @returns {Promise} Command response - */ - async sendCommand(commandLine) { - if (!this.isConnected()) { - throw new Error('Minecraft is not connected'); - } - const { id, message } = createCommandMessage(commandLine); - return this.commandQueue.enqueue(id, message); - } - - /** - * Send a batch of commands (for building). - * @param {string[]} commandLines - * @returns {Promise} Batch result - */ - async sendBatch(commandLines) { - if (!this.isConnected()) { - throw new Error('Minecraft is not connected'); - } - const commands = commandLines.map((line) => createCommandMessage(line)); - return this.commandQueue.enqueueBatch(commands); - } - - /** @returns {boolean} Whether a Minecraft client is connected */ - isConnected() { - return this._ws !== null && this._ws.readyState === 1; - } - - /** @returns {object} Status information */ - getStatus() { - return { - connected: this.isConnected(), - playerName: this._playerName, - connectedAt: this._connectedAt?.toISOString() || null, - subscriptions: [...this._subscriptions], - eventCount: this.events.size, - ...this.commandQueue.getStatus(), - }; - } - - /** Shut down the server */ - stop() { - this.commandQueue.destroy(); - if (this._ws) { - this._ws.close(); - } - if (this._wss) { - this._wss.close(); - } - log(TAG, 'Server stopped'); - } -} +import { WebSocketServer } from 'ws'; +import { createSubscribeMessage, createCommandMessage, createEnableEncryptionMessage, sanitize, log, logError } from './utils.js'; +import { EventStore } from './event-store.js'; +import { CommandQueue } from './command-queue.js'; +import { ServerEncryption } from './encryption.js'; + +const TAG = 'BedrockWS'; + +/** + * WebSocket server that accepts a connection from Minecraft Bedrock Edition. + * Only one Minecraft client is supported at a time. + * Supports Bedrock's application-level encryption handshake. + */ +export class BedrockWebSocket { + /** + * @param {object} opts + * @param {number} opts.port - WebSocket listen port (default 3001) + */ + constructor(opts = {}) { + this.port = opts.port ?? 3001; + this.events = new EventStore(); + this.commandQueue = new CommandQueue(); + + /** @type {import('ws').WebSocket | null} */ + this._ws = null; + this._wss = null; + this._connectedAt = null; + this._playerName = null; + this._subscriptions = new Set(); + + // Encryption state + /** @type {ServerEncryption | null} */ + this._encryption = null; + this._pendingEncryption = false; + this._encryptionRequestId = null; + } + + /** Start the WebSocket server */ + start() { + this._wss = new WebSocketServer({ + port: this.port, + handleProtocols: (protocols) => { + // Accept Bedrock's encryption subprotocol if offered + if (protocols.has('com.microsoft.minecraft.wsencrypt')) { + return 'com.microsoft.minecraft.wsencrypt'; + } + return false; + }, + }); + + this._wss.on('listening', () => { + log(TAG, `WebSocket server listening on port ${this.port}`); + log(TAG, `In Minecraft, type: /connect ws://:${this.port}`); + }); + + this._wss.on('connection', (ws) => { + // Only allow one connection at a time + if (this._ws) { + log(TAG, 'Rejecting new connection - already have an active client'); + ws.close(1000, 'Only one Minecraft client supported'); + return; + } + + this._ws = ws; + this._connectedAt = new Date(); + log(TAG, 'Minecraft client connected!'); + + // Start encryption handshake BEFORE wiring up command queue + this._beginEncryptionHandshake(); + + ws.on('message', (raw) => { + try { + let data; + + if (this._encryption && this._encryption.enabled) { + // All messages after handshake are encrypted + const buf = typeof raw === 'string' ? Buffer.from(raw) : raw; + const plaintext = this._encryption.decrypt(buf); + data = JSON.parse(plaintext); + } else { + // Pre-encryption: plaintext JSON + data = JSON.parse(raw.toString()); + } + + this._handleMessage(data); + } catch (err) { + logError(TAG, 'Failed to parse message:', err.message); + } + }); + + ws.on('close', (code, reason) => { + log(TAG, `Minecraft disconnected (code: ${code}, reason: ${reason || 'none'})`); + this._ws = null; + this._connectedAt = null; + this._playerName = null; + this._subscriptions.clear(); + this._encryption = null; + this._pendingEncryption = false; + this._encryptionRequestId = null; + this.commandQueue.setSendFunction(null); + }); + + ws.on('error', (err) => { + logError(TAG, 'WebSocket error:', err.message); + }); + }); + + this._wss.on('error', (err) => { + logError(TAG, 'Server error:', err.message); + }); + } + + /** Initiate the Bedrock encryption handshake */ + _beginEncryptionHandshake() { + this._encryption = new ServerEncryption(); + this._pendingEncryption = true; + + const { publicKey, salt } = this._encryption.getKeyExchangeParams(); + const { id, message } = createEnableEncryptionMessage(publicKey, salt); + this._encryptionRequestId = id; + + log(TAG, 'Sending enableencryption handshake...'); + + // Send plaintext — this is the last unencrypted message from server + if (this._ws && this._ws.readyState === 1) { + this._ws.send(message); + } + } + + /** Called after encryption handshake completes to wire up normal operation */ + _onEncryptionReady() { + log(TAG, 'Encryption active — wiring command queue and auto-subscribing'); + + // Now wire up command queue to send through encryption + this.commandQueue.setSendFunction((id, message) => { + if (this._ws && this._ws.readyState === 1) { + if (this._encryption && this._encryption.enabled) { + this._ws.send(this._encryption.encrypt(message)); + } else { + this._ws.send(message); + } + } else { + throw new Error('WebSocket not connected'); + } + }); + + // Auto-subscribe to key events + this._autoSubscribe(); + } + + /** Subscribe to default event types */ + _autoSubscribe() { + const defaultEvents = ['PlayerMessage']; + for (const eventName of defaultEvents) { + this.subscribe(eventName); + } + } + + /** + * Subscribe to a Bedrock event type. + * @param {string} eventName + */ + subscribe(eventName) { + if (!this._ws || this._ws.readyState !== 1) { + log(TAG, `Cannot subscribe to ${eventName} - not connected`); + return false; + } + + if (this._subscriptions.has(eventName)) { + log(TAG, `Already subscribed to ${eventName}`); + return true; + } + + const msg = createSubscribeMessage(eventName); + if (this._encryption && this._encryption.enabled) { + this._ws.send(this._encryption.encrypt(msg)); + } else { + this._ws.send(msg); + } + this._subscriptions.add(eventName); + log(TAG, `Subscribed to ${eventName}`); + return true; + } + + /** + * Handle an incoming WebSocket message from Bedrock. + * @param {object} data - Parsed JSON message + */ + _handleMessage(data) { + const purpose = data?.header?.messagePurpose; + + // Intercept encryption handshake response + if (this._pendingEncryption && purpose === 'commandResponse') { + const requestId = data.header.requestId; + if (requestId === this._encryptionRequestId) { + this._pendingEncryption = false; + this._encryptionRequestId = null; + + const publicKey = data.body?.publicKey; + if (publicKey) { + try { + this._encryption.completeKeyExchange(publicKey); + this._onEncryptionReady(); + } catch (err) { + logError(TAG, 'Encryption key exchange failed:', err.message); + // Fall back to unencrypted mode + this._encryption = null; + this._onEncryptionReady(); + } + } else { + logError(TAG, 'No public key in encryption response — falling back to plaintext'); + this._encryption = null; + this._onEncryptionReady(); + } + return; + } + } + + if (purpose === 'commandResponse') { + // Response to a command we sent + const requestId = data.header.requestId; + this.commandQueue.handleResponse(requestId, data.body); + return; + } + + if (purpose === 'event') { + const eventName = data.header.eventName; + const body = data.body || {}; + + // Filter bot's own messages to prevent echo loops + if (eventName === 'PlayerMessage') { + const sender = body.sender || ''; + const message = sanitize(body.message || ''); + const type = body.type || 'chat'; + + // Skip messages from external sources (commands, say, tell from server) + if (type !== 'chat' || sender === 'External' || sender === '') { + return; + } + + // Track player name from first chat message + if (!this._playerName && sender) { + this._playerName = sender; + log(TAG, `Player identified: ${this._playerName}`); + } + + this.events.push(eventName, { + sender, + message, + type, + }); + + log(TAG, `[Chat] <${sender}> ${message}`); + return; + } + + // Store all other events + this.events.push(eventName, body); + } + } + + /** + * Send a command to Minecraft. + * @param {string} commandLine - e.g. "give @p diamond 64" + * @returns {Promise} Command response + */ + async sendCommand(commandLine) { + if (!this.isConnected()) { + throw new Error('Minecraft is not connected'); + } + const { id, message } = createCommandMessage(commandLine); + return this.commandQueue.enqueue(id, message); + } + + /** + * Send a batch of commands (for building). + * @param {string[]} commandLines + * @returns {Promise} Batch result + */ + async sendBatch(commandLines) { + if (!this.isConnected()) { + throw new Error('Minecraft is not connected'); + } + const commands = commandLines.map((line) => createCommandMessage(line)); + return this.commandQueue.enqueueBatch(commands); + } + + /** @returns {boolean} Whether a Minecraft client is connected */ + isConnected() { + return this._ws !== null && this._ws.readyState === 1; + } + + /** + * Get the player's current position using /querytarget @s. + * @returns {Promise<{ x: number, y: number, z: number, rx: number, ry: number, dimension: number }>} + */ + async getPlayerPosition() { + if (!this.isConnected()) { + throw new Error('Minecraft is not connected'); + } + + const response = await this.sendCommand('querytarget @s'); + const details = response?.details; + + if (!details) { + throw new Error('No response from querytarget — is a player connected?'); + } + + // querytarget returns a JSON string in details field + try { + let parsed; + if (typeof details === 'string') { + // Response is a JSON array string like: [{"uniqueId":...,"position":{...},...}] + parsed = JSON.parse(details); + } else { + parsed = details; + } + + const target = Array.isArray(parsed) ? parsed[0] : parsed; + if (!target || !target.position) { + throw new Error('Invalid querytarget response format'); + } + + return { + x: Math.floor(target.position.x), + y: Math.floor(target.position.y), + z: Math.floor(target.position.z), + rx: target.yRot ?? 0, + ry: target.xRot ?? 0, + dimension: target.dimension ?? 0, + }; + } catch (err) { + if (err.message.includes('Invalid querytarget')) throw err; + throw new Error(`Failed to parse position data: ${err.message}`); + } + } + + /** + * Test for a specific block at coordinates. + * @param {number} x + * @param {number} y + * @param {number} z + * @param {string} [blockId] - Optional block ID to test for + * @returns {Promise} Test result + */ + async testForBlock(x, y, z, blockId) { + if (!this.isConnected()) { + throw new Error('Minecraft is not connected'); + } + + const cmd = blockId + ? `testforblock ${x} ${y} ${z} ${blockId}` + : `testforblock ${x} ${y} ${z}`; + + return this.sendCommand(cmd); + } + + /** @returns {object} Status information */ + getStatus() { + return { + connected: this.isConnected(), + encrypted: this._encryption?.enabled ?? false, + playerName: this._playerName, + connectedAt: this._connectedAt?.toISOString() || null, + subscriptions: [...this._subscriptions], + eventCount: this.events.size, + ...this.commandQueue.getStatus(), + }; + } + + /** Shut down the server */ + stop() { + this.commandQueue.destroy(); + if (this._ws) { + this._ws.close(); + } + if (this._wss) { + this._wss.close(); + } + log(TAG, 'Server stopped'); + } +} diff --git a/src/block-map.js b/src/block-map.js new file mode 100644 index 0000000..96fb43b --- /dev/null +++ b/src/block-map.js @@ -0,0 +1,692 @@ +import { log } from './utils.js'; + +const TAG = 'BlockMap'; + +/** + * Mapping from GrabCraft Java Edition block IDs to Bedrock Edition block IDs. + * GrabCraft uses format like "5:5" (blockId:dataValue) and English names. + * Bedrock uses string IDs like "planks" with data values. + * + * Format: { javaId: "numericId:data", bedrockId: "block_name", bedrockData: number } + */ +const BLOCK_MAP = new Map([ + // ── Air ── + ['0', { bedrock: 'air', data: 0, name: 'Air' }], + + // ── Stone variants ── + ['1', { bedrock: 'stone', data: 0, name: 'Stone' }], + ['1:1', { bedrock: 'stone', data: 1, name: 'Granite' }], + ['1:2', { bedrock: 'stone', data: 2, name: 'Polished Granite' }], + ['1:3', { bedrock: 'stone', data: 3, name: 'Diorite' }], + ['1:4', { bedrock: 'stone', data: 4, name: 'Polished Diorite' }], + ['1:5', { bedrock: 'stone', data: 5, name: 'Andesite' }], + ['1:6', { bedrock: 'stone', data: 6, name: 'Polished Andesite' }], + + // ── Grass & Dirt ── + ['2', { bedrock: 'grass_block', data: 0, name: 'Grass Block' }], + ['3', { bedrock: 'dirt', data: 0, name: 'Dirt' }], + ['3:1', { bedrock: 'dirt', data: 1, name: 'Coarse Dirt' }], + ['3:2', { bedrock: 'podzol', data: 0, name: 'Podzol' }], + + // ── Cobblestone ── + ['4', { bedrock: 'cobblestone', data: 0, name: 'Cobblestone' }], + + // ── Planks ── + ['5', { bedrock: 'planks', data: 0, name: 'Oak Planks' }], + ['5:1', { bedrock: 'planks', data: 1, name: 'Spruce Planks' }], + ['5:2', { bedrock: 'planks', data: 2, name: 'Birch Planks' }], + ['5:3', { bedrock: 'planks', data: 3, name: 'Jungle Planks' }], + ['5:4', { bedrock: 'planks', data: 4, name: 'Acacia Planks' }], + ['5:5', { bedrock: 'planks', data: 5, name: 'Dark Oak Planks' }], + + // ── Saplings ── + ['6', { bedrock: 'sapling', data: 0, name: 'Oak Sapling' }], + ['6:1', { bedrock: 'sapling', data: 1, name: 'Spruce Sapling' }], + ['6:2', { bedrock: 'sapling', data: 2, name: 'Birch Sapling' }], + ['6:3', { bedrock: 'sapling', data: 3, name: 'Jungle Sapling' }], + ['6:4', { bedrock: 'sapling', data: 4, name: 'Acacia Sapling' }], + ['6:5', { bedrock: 'sapling', data: 5, name: 'Dark Oak Sapling' }], + + // ── Bedrock ── + ['7', { bedrock: 'bedrock', data: 0, name: 'Bedrock' }], + + // ── Water & Lava ── + ['8', { bedrock: 'water', data: 0, name: 'Water' }], + ['9', { bedrock: 'water', data: 0, name: 'Water (stationary)' }], + ['10', { bedrock: 'lava', data: 0, name: 'Lava' }], + ['11', { bedrock: 'lava', data: 0, name: 'Lava (stationary)' }], + + // ── Sand & Gravel ── + ['12', { bedrock: 'sand', data: 0, name: 'Sand' }], + ['12:1', { bedrock: 'sand', data: 1, name: 'Red Sand' }], + ['13', { bedrock: 'gravel', data: 0, name: 'Gravel' }], + + // ── Ores ── + ['14', { bedrock: 'gold_ore', data: 0, name: 'Gold Ore' }], + ['15', { bedrock: 'iron_ore', data: 0, name: 'Iron Ore' }], + ['16', { bedrock: 'coal_ore', data: 0, name: 'Coal Ore' }], + + // ── Logs ── + ['17', { bedrock: 'log', data: 0, name: 'Oak Log' }], + ['17:1', { bedrock: 'log', data: 1, name: 'Spruce Log' }], + ['17:2', { bedrock: 'log', data: 2, name: 'Birch Log' }], + ['17:3', { bedrock: 'log', data: 3, name: 'Jungle Log' }], + ['162', { bedrock: 'log2', data: 0, name: 'Acacia Log' }], + ['162:1', { bedrock: 'log2', data: 1, name: 'Dark Oak Log' }], + + // ── Leaves ── + ['18', { bedrock: 'leaves', data: 0, name: 'Oak Leaves' }], + ['18:1', { bedrock: 'leaves', data: 1, name: 'Spruce Leaves' }], + ['18:2', { bedrock: 'leaves', data: 2, name: 'Birch Leaves' }], + ['18:3', { bedrock: 'leaves', data: 3, name: 'Jungle Leaves' }], + ['161', { bedrock: 'leaves2', data: 0, name: 'Acacia Leaves' }], + ['161:1', { bedrock: 'leaves2', data: 1, name: 'Dark Oak Leaves' }], + + // ── Sponge ── + ['19', { bedrock: 'sponge', data: 0, name: 'Sponge' }], + ['19:1', { bedrock: 'sponge', data: 1, name: 'Wet Sponge' }], + + // ── Glass ── + ['20', { bedrock: 'glass', data: 0, name: 'Glass' }], + ['102', { bedrock: 'glass_pane', data: 0, name: 'Glass Pane' }], + + // ── Lapis ── + ['21', { bedrock: 'lapis_ore', data: 0, name: 'Lapis Lazuli Ore' }], + ['22', { bedrock: 'lapis_block', data: 0, name: 'Lapis Lazuli Block' }], + + // ── Dispenser & Noteblock ── + ['23', { bedrock: 'dispenser', data: 0, name: 'Dispenser' }], + ['25', { bedrock: 'noteblock', data: 0, name: 'Note Block' }], + + // ── Sandstone ── + ['24', { bedrock: 'sandstone', data: 0, name: 'Sandstone' }], + ['24:1', { bedrock: 'sandstone', data: 1, name: 'Chiseled Sandstone' }], + ['24:2', { bedrock: 'sandstone', data: 2, name: 'Smooth Sandstone' }], + + // ── Wool ── + ['35', { bedrock: 'wool', data: 0, name: 'White Wool' }], + ['35:1', { bedrock: 'wool', data: 1, name: 'Orange Wool' }], + ['35:2', { bedrock: 'wool', data: 2, name: 'Magenta Wool' }], + ['35:3', { bedrock: 'wool', data: 3, name: 'Light Blue Wool' }], + ['35:4', { bedrock: 'wool', data: 4, name: 'Yellow Wool' }], + ['35:5', { bedrock: 'wool', data: 5, name: 'Lime Wool' }], + ['35:6', { bedrock: 'wool', data: 6, name: 'Pink Wool' }], + ['35:7', { bedrock: 'wool', data: 7, name: 'Gray Wool' }], + ['35:8', { bedrock: 'wool', data: 8, name: 'Light Gray Wool' }], + ['35:9', { bedrock: 'wool', data: 9, name: 'Cyan Wool' }], + ['35:10', { bedrock: 'wool', data: 10, name: 'Purple Wool' }], + ['35:11', { bedrock: 'wool', data: 11, name: 'Blue Wool' }], + ['35:12', { bedrock: 'wool', data: 12, name: 'Brown Wool' }], + ['35:13', { bedrock: 'wool', data: 13, name: 'Green Wool' }], + ['35:14', { bedrock: 'wool', data: 14, name: 'Red Wool' }], + ['35:15', { bedrock: 'wool', data: 15, name: 'Black Wool' }], + + // ── Gold & Iron blocks ── + ['41', { bedrock: 'gold_block', data: 0, name: 'Block of Gold' }], + ['42', { bedrock: 'iron_block', data: 0, name: 'Block of Iron' }], + + // ── Slabs ── + ['44', { bedrock: 'stone_block_slab', data: 0, name: 'Stone Slab' }], + ['44:1', { bedrock: 'stone_block_slab', data: 1, name: 'Sandstone Slab' }], + ['44:3', { bedrock: 'stone_block_slab', data: 3, name: 'Cobblestone Slab' }], + ['44:4', { bedrock: 'stone_block_slab', data: 4, name: 'Brick Slab' }], + ['44:5', { bedrock: 'stone_block_slab', data: 5, name: 'Stone Brick Slab' }], + ['126', { bedrock: 'wooden_slab', data: 0, name: 'Oak Slab' }], + ['126:1', { bedrock: 'wooden_slab', data: 1, name: 'Spruce Slab' }], + ['126:2', { bedrock: 'wooden_slab', data: 2, name: 'Birch Slab' }], + ['126:3', { bedrock: 'wooden_slab', data: 3, name: 'Jungle Slab' }], + ['126:4', { bedrock: 'wooden_slab', data: 4, name: 'Acacia Slab' }], + ['126:5', { bedrock: 'wooden_slab', data: 5, name: 'Dark Oak Slab' }], + + // ── Bricks ── + ['45', { bedrock: 'brick_block', data: 0, name: 'Bricks' }], + ['98', { bedrock: 'stonebrick', data: 0, name: 'Stone Bricks' }], + ['98:1', { bedrock: 'stonebrick', data: 1, name: 'Mossy Stone Bricks' }], + ['98:2', { bedrock: 'stonebrick', data: 2, name: 'Cracked Stone Bricks' }], + ['98:3', { bedrock: 'stonebrick', data: 3, name: 'Chiseled Stone Bricks' }], + ['112', { bedrock: 'nether_brick', data: 0, name: 'Nether Bricks' }], + + // ── TNT ── + ['46', { bedrock: 'tnt', data: 0, name: 'TNT' }], + + // ── Bookshelf ── + ['47', { bedrock: 'bookshelf', data: 0, name: 'Bookshelf' }], + + // ── Mossy Cobblestone ── + ['48', { bedrock: 'mossy_cobblestone', data: 0, name: 'Mossy Cobblestone' }], + + // ── Obsidian ── + ['49', { bedrock: 'obsidian', data: 0, name: 'Obsidian' }], + + // ── Torches ── + ['50', { bedrock: 'torch', data: 0, name: 'Torch' }], + + // ── Stairs ── + ['53', { bedrock: 'oak_stairs', data: 0, name: 'Oak Stairs' }], + ['67', { bedrock: 'stone_stairs', data: 0, name: 'Cobblestone Stairs' }], + ['108', { bedrock: 'brick_stairs', data: 0, name: 'Brick Stairs' }], + ['109', { bedrock: 'stone_brick_stairs', data: 0, name: 'Stone Brick Stairs' }], + ['114', { bedrock: 'nether_brick_stairs', data: 0, name: 'Nether Brick Stairs' }], + ['128', { bedrock: 'sandstone_stairs', data: 0, name: 'Sandstone Stairs' }], + ['134', { bedrock: 'spruce_stairs', data: 0, name: 'Spruce Stairs' }], + ['135', { bedrock: 'birch_stairs', data: 0, name: 'Birch Stairs' }], + ['136', { bedrock: 'jungle_stairs', data: 0, name: 'Jungle Stairs' }], + ['163', { bedrock: 'acacia_stairs', data: 0, name: 'Acacia Stairs' }], + ['164', { bedrock: 'dark_oak_stairs', data: 0, name: 'Dark Oak Stairs' }], + ['156', { bedrock: 'quartz_stairs', data: 0, name: 'Quartz Stairs' }], + + // ── Chest ── + ['54', { bedrock: 'chest', data: 0, name: 'Chest' }], + + // ── Diamond ── + ['56', { bedrock: 'diamond_ore', data: 0, name: 'Diamond Ore' }], + ['57', { bedrock: 'diamond_block', data: 0, name: 'Block of Diamond' }], + + // ── Crafting Table ── + ['58', { bedrock: 'crafting_table', data: 0, name: 'Crafting Table' }], + + // ── Furnace ── + ['61', { bedrock: 'furnace', data: 0, name: 'Furnace' }], + ['62', { bedrock: 'lit_furnace', data: 0, name: 'Burning Furnace' }], + + // ── Doors ── + ['64', { bedrock: 'wooden_door', data: 0, name: 'Oak Door' }], + ['71', { bedrock: 'iron_door', data: 0, name: 'Iron Door' }], + ['193', { bedrock: 'spruce_door', data: 0, name: 'Spruce Door' }], + ['194', { bedrock: 'birch_door', data: 0, name: 'Birch Door' }], + ['195', { bedrock: 'jungle_door', data: 0, name: 'Jungle Door' }], + ['196', { bedrock: 'acacia_door', data: 0, name: 'Acacia Door' }], + ['197', { bedrock: 'dark_oak_door', data: 0, name: 'Dark Oak Door' }], + + // ── Ladders ── + ['65', { bedrock: 'ladder', data: 0, name: 'Ladder' }], + + // ── Rails ── + ['66', { bedrock: 'rail', data: 0, name: 'Rail' }], + ['27', { bedrock: 'golden_rail', data: 0, name: 'Powered Rail' }], + ['28', { bedrock: 'detector_rail', data: 0, name: 'Detector Rail' }], + ['157', { bedrock: 'activator_rail', data: 0, name: 'Activator Rail' }], + + // ── Snow & Ice ── + ['78', { bedrock: 'snow_layer', data: 0, name: 'Snow Layer' }], + ['79', { bedrock: 'ice', data: 0, name: 'Ice' }], + ['80', { bedrock: 'snow', data: 0, name: 'Snow Block' }], + ['174', { bedrock: 'packed_ice', data: 0, name: 'Packed Ice' }], + + // ── Cactus ── + ['81', { bedrock: 'cactus', data: 0, name: 'Cactus' }], + + // ── Clay ── + ['82', { bedrock: 'clay', data: 0, name: 'Clay' }], + + // ── Jukebox ── + ['84', { bedrock: 'jukebox', data: 0, name: 'Jukebox' }], + + // ── Fences ── + ['85', { bedrock: 'fence', data: 0, name: 'Oak Fence' }], + ['113', { bedrock: 'nether_brick_fence', data: 0, name: 'Nether Brick Fence' }], + ['188', { bedrock: 'fence', data: 1, name: 'Spruce Fence' }], + ['189', { bedrock: 'fence', data: 2, name: 'Birch Fence' }], + ['190', { bedrock: 'fence', data: 3, name: 'Jungle Fence' }], + ['191', { bedrock: 'fence', data: 4, name: 'Acacia Fence' }], + ['192', { bedrock: 'fence', data: 5, name: 'Dark Oak Fence' }], + + // ── Fence Gates ── + ['107', { bedrock: 'fence_gate', data: 0, name: 'Oak Fence Gate' }], + ['183', { bedrock: 'spruce_fence_gate', data: 0, name: 'Spruce Fence Gate' }], + ['184', { bedrock: 'birch_fence_gate', data: 0, name: 'Birch Fence Gate' }], + ['185', { bedrock: 'jungle_fence_gate', data: 0, name: 'Jungle Fence Gate' }], + ['186', { bedrock: 'acacia_fence_gate', data: 0, name: 'Acacia Fence Gate' }], + ['187', { bedrock: 'dark_oak_fence_gate', data: 0, name: 'Dark Oak Fence Gate' }], + + // ── Pumpkin & Melon ── + ['86', { bedrock: 'pumpkin', data: 0, name: 'Pumpkin' }], + ['91', { bedrock: 'lit_pumpkin', data: 0, name: 'Jack o\'Lantern' }], + ['103', { bedrock: 'melon_block', data: 0, name: 'Melon Block' }], + + // ── Netherrack & Soul Sand ── + ['87', { bedrock: 'netherrack', data: 0, name: 'Netherrack' }], + ['88', { bedrock: 'soul_sand', data: 0, name: 'Soul Sand' }], + + // ── Glowstone ── + ['89', { bedrock: 'glowstone', data: 0, name: 'Glowstone' }], + + // ── Stained Glass ── + ['95', { bedrock: 'stained_glass', data: 0, name: 'White Stained Glass' }], + ['95:1', { bedrock: 'stained_glass', data: 1, name: 'Orange Stained Glass' }], + ['95:2', { bedrock: 'stained_glass', data: 2, name: 'Magenta Stained Glass' }], + ['95:3', { bedrock: 'stained_glass', data: 3, name: 'Light Blue Stained Glass' }], + ['95:4', { bedrock: 'stained_glass', data: 4, name: 'Yellow Stained Glass' }], + ['95:5', { bedrock: 'stained_glass', data: 5, name: 'Lime Stained Glass' }], + ['95:6', { bedrock: 'stained_glass', data: 6, name: 'Pink Stained Glass' }], + ['95:7', { bedrock: 'stained_glass', data: 7, name: 'Gray Stained Glass' }], + ['95:8', { bedrock: 'stained_glass', data: 8, name: 'Light Gray Stained Glass' }], + ['95:9', { bedrock: 'stained_glass', data: 9, name: 'Cyan Stained Glass' }], + ['95:10', { bedrock: 'stained_glass', data: 10, name: 'Purple Stained Glass' }], + ['95:11', { bedrock: 'stained_glass', data: 11, name: 'Blue Stained Glass' }], + ['95:12', { bedrock: 'stained_glass', data: 12, name: 'Brown Stained Glass' }], + ['95:13', { bedrock: 'stained_glass', data: 13, name: 'Green Stained Glass' }], + ['95:14', { bedrock: 'stained_glass', data: 14, name: 'Red Stained Glass' }], + ['95:15', { bedrock: 'stained_glass', data: 15, name: 'Black Stained Glass' }], + + // ── Stained Glass Panes ── + ['160', { bedrock: 'stained_glass_pane', data: 0, name: 'White Stained Glass Pane' }], + ['160:1', { bedrock: 'stained_glass_pane', data: 1, name: 'Orange Stained Glass Pane' }], + ['160:2', { bedrock: 'stained_glass_pane', data: 2, name: 'Magenta Stained Glass Pane' }], + ['160:3', { bedrock: 'stained_glass_pane', data: 3, name: 'Light Blue Stained Glass Pane' }], + ['160:4', { bedrock: 'stained_glass_pane', data: 4, name: 'Yellow Stained Glass Pane' }], + ['160:5', { bedrock: 'stained_glass_pane', data: 5, name: 'Lime Stained Glass Pane' }], + ['160:6', { bedrock: 'stained_glass_pane', data: 6, name: 'Pink Stained Glass Pane' }], + ['160:7', { bedrock: 'stained_glass_pane', data: 7, name: 'Gray Stained Glass Pane' }], + ['160:8', { bedrock: 'stained_glass_pane', data: 8, name: 'Light Gray Stained Glass Pane' }], + ['160:9', { bedrock: 'stained_glass_pane', data: 9, name: 'Cyan Stained Glass Pane' }], + ['160:10', { bedrock: 'stained_glass_pane', data: 10, name: 'Purple Stained Glass Pane' }], + ['160:11', { bedrock: 'stained_glass_pane', data: 11, name: 'Blue Stained Glass Pane' }], + ['160:12', { bedrock: 'stained_glass_pane', data: 12, name: 'Brown Stained Glass Pane' }], + ['160:13', { bedrock: 'stained_glass_pane', data: 13, name: 'Green Stained Glass Pane' }], + ['160:14', { bedrock: 'stained_glass_pane', data: 14, name: 'Red Stained Glass Pane' }], + ['160:15', { bedrock: 'stained_glass_pane', data: 15, name: 'Black Stained Glass Pane' }], + + // ── Iron Bars ── + ['101', { bedrock: 'iron_bars', data: 0, name: 'Iron Bars' }], + + // ── Quartz ── + ['155', { bedrock: 'quartz_block', data: 0, name: 'Quartz Block' }], + ['155:1', { bedrock: 'quartz_block', data: 1, name: 'Chiseled Quartz' }], + ['155:2', { bedrock: 'quartz_block', data: 2, name: 'Pillar Quartz' }], + + // ── Terracotta (Hardened Clay) ── + ['159', { bedrock: 'stained_hardened_clay', data: 0, name: 'White Terracotta' }], + ['159:1', { bedrock: 'stained_hardened_clay', data: 1, name: 'Orange Terracotta' }], + ['159:2', { bedrock: 'stained_hardened_clay', data: 2, name: 'Magenta Terracotta' }], + ['159:3', { bedrock: 'stained_hardened_clay', data: 3, name: 'Light Blue Terracotta' }], + ['159:4', { bedrock: 'stained_hardened_clay', data: 4, name: 'Yellow Terracotta' }], + ['159:5', { bedrock: 'stained_hardened_clay', data: 5, name: 'Lime Terracotta' }], + ['159:6', { bedrock: 'stained_hardened_clay', data: 6, name: 'Pink Terracotta' }], + ['159:7', { bedrock: 'stained_hardened_clay', data: 7, name: 'Gray Terracotta' }], + ['159:8', { bedrock: 'stained_hardened_clay', data: 8, name: 'Light Gray Terracotta' }], + ['159:9', { bedrock: 'stained_hardened_clay', data: 9, name: 'Cyan Terracotta' }], + ['159:10', { bedrock: 'stained_hardened_clay', data: 10, name: 'Purple Terracotta' }], + ['159:11', { bedrock: 'stained_hardened_clay', data: 11, name: 'Blue Terracotta' }], + ['159:12', { bedrock: 'stained_hardened_clay', data: 12, name: 'Brown Terracotta' }], + ['159:13', { bedrock: 'stained_hardened_clay', data: 13, name: 'Green Terracotta' }], + ['159:14', { bedrock: 'stained_hardened_clay', data: 14, name: 'Red Terracotta' }], + ['159:15', { bedrock: 'stained_hardened_clay', data: 15, name: 'Black Terracotta' }], + ['172', { bedrock: 'hardened_clay', data: 0, name: 'Terracotta' }], + + // ── Concrete ── + ['251', { bedrock: 'concrete', data: 0, name: 'White Concrete' }], + ['251:1', { bedrock: 'concrete', data: 1, name: 'Orange Concrete' }], + ['251:2', { bedrock: 'concrete', data: 2, name: 'Magenta Concrete' }], + ['251:3', { bedrock: 'concrete', data: 3, name: 'Light Blue Concrete' }], + ['251:4', { bedrock: 'concrete', data: 4, name: 'Yellow Concrete' }], + ['251:5', { bedrock: 'concrete', data: 5, name: 'Lime Concrete' }], + ['251:6', { bedrock: 'concrete', data: 6, name: 'Pink Concrete' }], + ['251:7', { bedrock: 'concrete', data: 7, name: 'Gray Concrete' }], + ['251:8', { bedrock: 'concrete', data: 8, name: 'Light Gray Concrete' }], + ['251:9', { bedrock: 'concrete', data: 9, name: 'Cyan Concrete' }], + ['251:10', { bedrock: 'concrete', data: 10, name: 'Purple Concrete' }], + ['251:11', { bedrock: 'concrete', data: 11, name: 'Blue Concrete' }], + ['251:12', { bedrock: 'concrete', data: 12, name: 'Brown Concrete' }], + ['251:13', { bedrock: 'concrete', data: 13, name: 'Green Concrete' }], + ['251:14', { bedrock: 'concrete', data: 14, name: 'Red Concrete' }], + ['251:15', { bedrock: 'concrete', data: 15, name: 'Black Concrete' }], + + // ── Concrete Powder ── + ['252', { bedrock: 'concrete_powder', data: 0, name: 'White Concrete Powder' }], + ['252:1', { bedrock: 'concrete_powder', data: 1, name: 'Orange Concrete Powder' }], + ['252:2', { bedrock: 'concrete_powder', data: 2, name: 'Magenta Concrete Powder' }], + ['252:3', { bedrock: 'concrete_powder', data: 3, name: 'Light Blue Concrete Powder' }], + ['252:4', { bedrock: 'concrete_powder', data: 4, name: 'Yellow Concrete Powder' }], + ['252:5', { bedrock: 'concrete_powder', data: 5, name: 'Lime Concrete Powder' }], + ['252:6', { bedrock: 'concrete_powder', data: 6, name: 'Pink Concrete Powder' }], + ['252:7', { bedrock: 'concrete_powder', data: 7, name: 'Gray Concrete Powder' }], + ['252:8', { bedrock: 'concrete_powder', data: 8, name: 'Light Gray Concrete Powder' }], + ['252:9', { bedrock: 'concrete_powder', data: 9, name: 'Cyan Concrete Powder' }], + ['252:10', { bedrock: 'concrete_powder', data: 10, name: 'Purple Concrete Powder' }], + ['252:11', { bedrock: 'concrete_powder', data: 11, name: 'Blue Concrete Powder' }], + ['252:12', { bedrock: 'concrete_powder', data: 12, name: 'Brown Concrete Powder' }], + ['252:13', { bedrock: 'concrete_powder', data: 13, name: 'Green Concrete Powder' }], + ['252:14', { bedrock: 'concrete_powder', data: 14, name: 'Red Concrete Powder' }], + ['252:15', { bedrock: 'concrete_powder', data: 15, name: 'Black Concrete Powder' }], + + // ── Glazed Terracotta ── + ['235', { bedrock: 'white_glazed_terracotta', data: 0, name: 'White Glazed Terracotta' }], + ['236', { bedrock: 'orange_glazed_terracotta', data: 0, name: 'Orange Glazed Terracotta' }], + ['237', { bedrock: 'magenta_glazed_terracotta', data: 0, name: 'Magenta Glazed Terracotta' }], + ['238', { bedrock: 'light_blue_glazed_terracotta', data: 0, name: 'Light Blue Glazed Terracotta' }], + ['239', { bedrock: 'yellow_glazed_terracotta', data: 0, name: 'Yellow Glazed Terracotta' }], + ['240', { bedrock: 'lime_glazed_terracotta', data: 0, name: 'Lime Glazed Terracotta' }], + ['241', { bedrock: 'pink_glazed_terracotta', data: 0, name: 'Pink Glazed Terracotta' }], + ['242', { bedrock: 'gray_glazed_terracotta', data: 0, name: 'Gray Glazed Terracotta' }], + ['243', { bedrock: 'silver_glazed_terracotta', data: 0, name: 'Light Gray Glazed Terracotta' }], + ['244', { bedrock: 'cyan_glazed_terracotta', data: 0, name: 'Cyan Glazed Terracotta' }], + ['245', { bedrock: 'purple_glazed_terracotta', data: 0, name: 'Purple Glazed Terracotta' }], + ['246', { bedrock: 'blue_glazed_terracotta', data: 0, name: 'Blue Glazed Terracotta' }], + ['247', { bedrock: 'brown_glazed_terracotta', data: 0, name: 'Brown Glazed Terracotta' }], + ['248', { bedrock: 'green_glazed_terracotta', data: 0, name: 'Green Glazed Terracotta' }], + ['249', { bedrock: 'red_glazed_terracotta', data: 0, name: 'Red Glazed Terracotta' }], + ['250', { bedrock: 'black_glazed_terracotta', data: 0, name: 'Black Glazed Terracotta' }], + + // ── Carpet ── + ['171', { bedrock: 'carpet', data: 0, name: 'White Carpet' }], + ['171:1', { bedrock: 'carpet', data: 1, name: 'Orange Carpet' }], + ['171:2', { bedrock: 'carpet', data: 2, name: 'Magenta Carpet' }], + ['171:3', { bedrock: 'carpet', data: 3, name: 'Light Blue Carpet' }], + ['171:4', { bedrock: 'carpet', data: 4, name: 'Yellow Carpet' }], + ['171:5', { bedrock: 'carpet', data: 5, name: 'Lime Carpet' }], + ['171:6', { bedrock: 'carpet', data: 6, name: 'Pink Carpet' }], + ['171:7', { bedrock: 'carpet', data: 7, name: 'Gray Carpet' }], + ['171:8', { bedrock: 'carpet', data: 8, name: 'Light Gray Carpet' }], + ['171:9', { bedrock: 'carpet', data: 9, name: 'Cyan Carpet' }], + ['171:10', { bedrock: 'carpet', data: 10, name: 'Purple Carpet' }], + ['171:11', { bedrock: 'carpet', data: 11, name: 'Blue Carpet' }], + ['171:12', { bedrock: 'carpet', data: 12, name: 'Brown Carpet' }], + ['171:13', { bedrock: 'carpet', data: 13, name: 'Green Carpet' }], + ['171:14', { bedrock: 'carpet', data: 14, name: 'Red Carpet' }], + ['171:15', { bedrock: 'carpet', data: 15, name: 'Black Carpet' }], + + // ── Redstone ── + ['55', { bedrock: 'redstone_wire', data: 0, name: 'Redstone Wire' }], + ['73', { bedrock: 'redstone_ore', data: 0, name: 'Redstone Ore' }], + ['76', { bedrock: 'redstone_torch', data: 0, name: 'Redstone Torch' }], + ['69', { bedrock: 'lever', data: 0, name: 'Lever' }], + ['70', { bedrock: 'stone_pressure_plate', data: 0, name: 'Stone Pressure Plate' }], + ['72', { bedrock: 'wooden_pressure_plate', data: 0, name: 'Oak Pressure Plate' }], + ['77', { bedrock: 'stone_button', data: 0, name: 'Stone Button' }], + ['143', { bedrock: 'wooden_button', data: 0, name: 'Oak Button' }], + ['123', { bedrock: 'redstone_lamp', data: 0, name: 'Redstone Lamp' }], + ['33', { bedrock: 'piston', data: 0, name: 'Piston' }], + ['29', { bedrock: 'sticky_piston', data: 0, name: 'Sticky Piston' }], + ['93', { bedrock: 'unpowered_repeater', data: 0, name: 'Repeater' }], + ['149', { bedrock: 'unpowered_comparator', data: 0, name: 'Comparator' }], + ['152', { bedrock: 'redstone_block', data: 0, name: 'Block of Redstone' }], + ['151', { bedrock: 'daylight_detector', data: 0, name: 'Daylight Detector' }], + ['154', { bedrock: 'hopper', data: 0, name: 'Hopper' }], + ['158', { bedrock: 'dropper', data: 0, name: 'Dropper' }], + ['146', { bedrock: 'trapped_chest', data: 0, name: 'Trapped Chest' }], + ['147', { bedrock: 'light_weighted_pressure_plate', data: 0, name: 'Light Weighted Pressure Plate' }], + ['148', { bedrock: 'heavy_weighted_pressure_plate', data: 0, name: 'Heavy Weighted Pressure Plate' }], + + // ── Trapdoors ── + ['96', { bedrock: 'trapdoor', data: 0, name: 'Oak Trapdoor' }], + ['167', { bedrock: 'iron_trapdoor', data: 0, name: 'Iron Trapdoor' }], + + // ── Emerald ── + ['129', { bedrock: 'emerald_ore', data: 0, name: 'Emerald Ore' }], + ['133', { bedrock: 'emerald_block', data: 0, name: 'Block of Emerald' }], + + // ── End Stone ── + ['121', { bedrock: 'end_stone', data: 0, name: 'End Stone' }], + ['206', { bedrock: 'end_bricks', data: 0, name: 'End Stone Bricks' }], + + // ── Purpur ── + ['201', { bedrock: 'purpur_block', data: 0, name: 'Purpur Block' }], + ['202', { bedrock: 'purpur_pillar', data: 0, name: 'Purpur Pillar' }], + ['203', { bedrock: 'purpur_stairs', data: 0, name: 'Purpur Stairs' }], + + // ── Prismarine ── + ['168', { bedrock: 'prismarine', data: 0, name: 'Prismarine' }], + ['168:1', { bedrock: 'prismarine', data: 1, name: 'Prismarine Bricks' }], + ['168:2', { bedrock: 'prismarine', data: 2, name: 'Dark Prismarine' }], + ['169', { bedrock: 'sea_lantern', data: 0, name: 'Sea Lantern' }], + + // ── Hay Bale ── + ['170', { bedrock: 'hay_block', data: 0, name: 'Hay Bale' }], + + // ── Anvil ── + ['145', { bedrock: 'anvil', data: 0, name: 'Anvil' }], + + // ── Slime Block ── + ['165', { bedrock: 'slime', data: 0, name: 'Slime Block' }], + + // ── Coal Block ── + ['173', { bedrock: 'coal_block', data: 0, name: 'Block of Coal' }], + + // ── Red Sandstone ── + ['179', { bedrock: 'red_sandstone', data: 0, name: 'Red Sandstone' }], + ['179:1', { bedrock: 'red_sandstone', data: 1, name: 'Chiseled Red Sandstone' }], + ['179:2', { bedrock: 'red_sandstone', data: 2, name: 'Smooth Red Sandstone' }], + ['180', { bedrock: 'red_sandstone_stairs', data: 0, name: 'Red Sandstone Stairs' }], + + // ── Misc utility blocks ── + ['26', { bedrock: 'bed', data: 0, name: 'Bed' }], + ['30', { bedrock: 'web', data: 0, name: 'Cobweb' }], + ['31', { bedrock: 'tallgrass', data: 1, name: 'Grass' }], + ['31:2', { bedrock: 'tallgrass', data: 2, name: 'Fern' }], + ['32', { bedrock: 'deadbush', data: 0, name: 'Dead Bush' }], + ['37', { bedrock: 'yellow_flower', data: 0, name: 'Dandelion' }], + ['38', { bedrock: 'red_flower', data: 0, name: 'Poppy' }], + ['38:1', { bedrock: 'red_flower', data: 1, name: 'Blue Orchid' }], + ['38:2', { bedrock: 'red_flower', data: 2, name: 'Allium' }], + ['38:3', { bedrock: 'red_flower', data: 3, name: 'Azure Bluet' }], + ['38:4', { bedrock: 'red_flower', data: 4, name: 'Red Tulip' }], + ['38:5', { bedrock: 'red_flower', data: 5, name: 'Orange Tulip' }], + ['38:6', { bedrock: 'red_flower', data: 6, name: 'White Tulip' }], + ['38:7', { bedrock: 'red_flower', data: 7, name: 'Pink Tulip' }], + ['38:8', { bedrock: 'red_flower', data: 8, name: 'Oxeye Daisy' }], + ['39', { bedrock: 'brown_mushroom', data: 0, name: 'Brown Mushroom' }], + ['40', { bedrock: 'red_mushroom', data: 0, name: 'Red Mushroom' }], + ['83', { bedrock: 'reeds', data: 0, name: 'Sugar Cane' }], + ['100', { bedrock: 'red_mushroom_block', data: 0, name: 'Red Mushroom Block' }], + ['99', { bedrock: 'brown_mushroom_block', data: 0, name: 'Brown Mushroom Block' }], + ['104', { bedrock: 'pumpkin_stem', data: 0, name: 'Pumpkin Stem' }], + ['106', { bedrock: 'vine', data: 0, name: 'Vines' }], + ['110', { bedrock: 'mycelium', data: 0, name: 'Mycelium' }], + ['111', { bedrock: 'waterlily', data: 0, name: 'Lily Pad' }], + ['115', { bedrock: 'nether_wart', data: 0, name: 'Nether Wart' }], + ['116', { bedrock: 'enchanting_table', data: 0, name: 'Enchanting Table' }], + ['117', { bedrock: 'brewing_stand', data: 0, name: 'Brewing Stand' }], + ['118', { bedrock: 'cauldron', data: 0, name: 'Cauldron' }], + ['120', { bedrock: 'end_portal_frame', data: 0, name: 'End Portal Frame' }], + ['122', { bedrock: 'dragon_egg', data: 0, name: 'Dragon Egg' }], + ['130', { bedrock: 'ender_chest', data: 0, name: 'Ender Chest' }], + ['138', { bedrock: 'beacon', data: 0, name: 'Beacon' }], + ['166', { bedrock: 'barrier', data: 0, name: 'Barrier' }], + ['175', { bedrock: 'double_plant', data: 0, name: 'Sunflower' }], + ['175:1', { bedrock: 'double_plant', data: 1, name: 'Lilac' }], + ['175:2', { bedrock: 'double_plant', data: 2, name: 'Double Tallgrass' }], + ['175:3', { bedrock: 'double_plant', data: 3, name: 'Large Fern' }], + ['175:4', { bedrock: 'double_plant', data: 4, name: 'Rose Bush' }], + ['175:5', { bedrock: 'double_plant', data: 5, name: 'Peony' }], + ['198', { bedrock: 'end_rod', data: 0, name: 'End Rod' }], + ['199', { bedrock: 'chorus_plant', data: 0, name: 'Chorus Plant' }], + ['200', { bedrock: 'chorus_flower', data: 0, name: 'Chorus Flower' }], + ['207', { bedrock: 'beetroot', data: 0, name: 'Beetroot' }], + ['208', { bedrock: 'grass_path', data: 0, name: 'Grass Path' }], + ['209', { bedrock: 'end_gateway', data: 0, name: 'End Gateway' }], + ['213', { bedrock: 'magma', data: 0, name: 'Magma Block' }], + ['214', { bedrock: 'nether_wart_block', data: 0, name: 'Nether Wart Block' }], + ['215', { bedrock: 'red_nether_brick', data: 0, name: 'Red Nether Bricks' }], + ['216', { bedrock: 'bone_block', data: 0, name: 'Bone Block' }], + ['218', { bedrock: 'observer', data: 0, name: 'Observer' }], + ['219', { bedrock: 'shulker_box', data: 0, name: 'White Shulker Box' }], + + // ── Walls ── + ['139', { bedrock: 'cobblestone_wall', data: 0, name: 'Cobblestone Wall' }], + ['139:1', { bedrock: 'cobblestone_wall', data: 1, name: 'Mossy Cobblestone Wall' }], + + // ── Banners ── + ['176', { bedrock: 'standing_banner', data: 0, name: 'Banner' }], + + // ── Signs ── + ['63', { bedrock: 'standing_sign', data: 0, name: 'Sign' }], + ['68', { bedrock: 'wall_sign', data: 0, name: 'Wall Sign' }], + + // ── Flower Pot ── + ['140', { bedrock: 'flower_pot', data: 0, name: 'Flower Pot' }], + + // ── Skull / Head ── + ['144', { bedrock: 'skull', data: 0, name: 'Mob Head' }], + + // ── Armor Stand (entity, but GrabCraft uses it) ── + ['416', { bedrock: 'air', data: 0, name: 'Armor Stand (entity)' }], +]); + +/** + * Name-based fuzzy lookup table (lowercase name -> bedrock ID). + * Built from BLOCK_MAP for fallback matching when numeric ID fails. + */ +const NAME_MAP = new Map(); +for (const [, entry] of BLOCK_MAP) { + NAME_MAP.set(entry.name.toLowerCase(), entry); +} + +// Additional common name aliases +const ALIASES = new Map([ + ['dark oak wood plank', BLOCK_MAP.get('5:5')], + ['oak wood plank', BLOCK_MAP.get('5')], + ['spruce wood plank', BLOCK_MAP.get('5:1')], + ['birch wood plank', BLOCK_MAP.get('5:2')], + ['jungle wood plank', BLOCK_MAP.get('5:3')], + ['acacia wood plank', BLOCK_MAP.get('5:4')], + ['dark oak wood', BLOCK_MAP.get('162:1')], + ['oak wood', BLOCK_MAP.get('17')], + ['spruce wood', BLOCK_MAP.get('17:1')], + ['birch wood', BLOCK_MAP.get('17:2')], + ['jungle wood', BLOCK_MAP.get('17:3')], + ['acacia wood', BLOCK_MAP.get('162')], + ['stone brick', BLOCK_MAP.get('98')], + ['mossy stone brick', BLOCK_MAP.get('98:1')], + ['cracked stone brick', BLOCK_MAP.get('98:2')], + ['chiseled stone brick', BLOCK_MAP.get('98:3')], + ['brick', BLOCK_MAP.get('45')], + ['nether brick', BLOCK_MAP.get('112')], + ['glass pane', BLOCK_MAP.get('102')], + ['cobble', BLOCK_MAP.get('4')], + ['plank', BLOCK_MAP.get('5')], + ['planks', BLOCK_MAP.get('5')], + ['wooden plank', BLOCK_MAP.get('5')], + ['wooden planks', BLOCK_MAP.get('5')], + ['log', BLOCK_MAP.get('17')], + ['wood', BLOCK_MAP.get('17')], + ['leaves', BLOCK_MAP.get('18')], + ['torch', BLOCK_MAP.get('50')], + ['crafting table', BLOCK_MAP.get('58')], + ['workbench', BLOCK_MAP.get('58')], + ['furnace', BLOCK_MAP.get('61')], + ['chest', BLOCK_MAP.get('54')], + ['door', BLOCK_MAP.get('64')], + ['fence', BLOCK_MAP.get('85')], + ['wool', BLOCK_MAP.get('35')], + ['carpet', BLOCK_MAP.get('171')], + ['glass', BLOCK_MAP.get('20')], + ['sand', BLOCK_MAP.get('12')], + ['gravel', BLOCK_MAP.get('13')], + ['dirt', BLOCK_MAP.get('3')], + ['grass', BLOCK_MAP.get('2')], + ['grass block', BLOCK_MAP.get('2')], + ['water', BLOCK_MAP.get('8')], + ['lava', BLOCK_MAP.get('10')], + ['cobblestone wall', BLOCK_MAP.get('139')], + ['mossy cobblestone wall', BLOCK_MAP.get('139:1')], + ['redstone', BLOCK_MAP.get('55')], + ['redstone lamp', BLOCK_MAP.get('123')], + ['glowstone', BLOCK_MAP.get('89')], + ['sea lantern', BLOCK_MAP.get('169')], +]); + +for (const [name, entry] of ALIASES) { + if (entry) NAME_MAP.set(name, entry); +} + +/** Track unknown blocks for reporting */ +const unknownBlocks = new Map(); + +/** + * Resolve a GrabCraft block ID and/or name to a Bedrock setblock string. + * @param {string} gcId - GrabCraft numeric ID like "5:5" or "5" + * @param {string} [gcName] - English name from GrabCraft for fuzzy matching + * @returns {{ block: string, data: number, matched: boolean, name: string }} + */ +export function resolveBlock(gcId, gcName) { + // Try exact numeric ID first + if (gcId && BLOCK_MAP.has(gcId)) { + const entry = BLOCK_MAP.get(gcId); + return { block: entry.bedrock, data: entry.data, matched: true, name: entry.name }; + } + + // Try base ID without data value + if (gcId && gcId.includes(':')) { + const baseId = gcId.split(':')[0]; + if (BLOCK_MAP.has(baseId)) { + const entry = BLOCK_MAP.get(baseId); + return { block: entry.bedrock, data: entry.data, matched: true, name: entry.name }; + } + } + + // Try name-based lookup + if (gcName) { + const lower = gcName.toLowerCase().trim(); + if (NAME_MAP.has(lower)) { + const entry = NAME_MAP.get(lower); + return { block: entry.bedrock, data: entry.data, matched: true, name: entry.name }; + } + + // Fuzzy: try removing common suffixes/prefixes + const simplified = lower + .replace(/\b(block of|block)\b/g, '') + .replace(/\s+/g, ' ') + .trim(); + if (NAME_MAP.has(simplified)) { + const entry = NAME_MAP.get(simplified); + return { block: entry.bedrock, data: entry.data, matched: true, name: entry.name }; + } + + // Partial match: check if any name contains the search term + for (const [name, entry] of NAME_MAP) { + if (name.includes(lower) || lower.includes(name)) { + return { block: entry.bedrock, data: entry.data, matched: true, name: entry.name }; + } + } + } + + // Track unknown block + const key = `${gcId || 'unknown'}:${gcName || 'unnamed'}`; + unknownBlocks.set(key, (unknownBlocks.get(key) || 0) + 1); + + // Fallback to stone + log(TAG, `Unknown block: id=${gcId}, name=${gcName} — using stone fallback`); + return { block: 'stone', data: 0, matched: false, name: gcName || `Unknown(${gcId})` }; +} + +/** + * Format a resolved block for a setblock command. + * @param {{ block: string, data: number }} resolved + * @returns {string} e.g. "planks 5" or "stone 0" + */ +export function formatBlock(resolved) { + return resolved.data > 0 ? `${resolved.block} ${resolved.data}` : resolved.block; +} + +/** + * Get all unknown blocks encountered so far (for reporting). + * @returns {Map} + */ +export function getUnknownBlocks() { + return new Map(unknownBlocks); +} + +/** + * Clear the unknown blocks tracker. + */ +export function clearUnknownBlocks() { + unknownBlocks.clear(); +} + +/** + * Get all known block mappings (for MCP resource). + * @returns {Array<{ javaId: string, bedrockId: string, bedrockData: number, name: string }>} + */ +export function getAllBlocks() { + const blocks = []; + for (const [javaId, entry] of BLOCK_MAP) { + blocks.push({ + javaId, + bedrockId: entry.bedrock, + bedrockData: entry.data, + name: entry.name, + }); + } + return blocks; +} diff --git a/src/building-helpers.js b/src/building-helpers.js new file mode 100644 index 0000000..dc57665 --- /dev/null +++ b/src/building-helpers.js @@ -0,0 +1,272 @@ +/** + * Higher-level geometric primitives for Minecraft building. + * Generates arrays of setblock/fill commands. + * Uses `fill` for rectangular regions where possible for efficiency. + */ + +/** + * Generate a sphere of blocks. + * @param {{ x: number, y: number, z: number }} center + * @param {number} radius + * @param {string} block - Bedrock block ID (e.g. "stone", "glass") + * @param {boolean} [hollow=false] - If true, only the shell + * @returns {string[]} Array of setblock commands + */ +export function generateSphere(center, radius, block, hollow = false) { + const commands = []; + const r2 = radius * radius; + const inner2 = hollow ? (radius - 1) * (radius - 1) : -1; + + for (let y = -radius; y <= radius; y++) { + for (let x = -radius; x <= radius; x++) { + for (let z = -radius; z <= radius; z++) { + const dist2 = x * x + y * y + z * z; + if (dist2 <= r2) { + if (!hollow || dist2 > inner2) { + commands.push(`setblock ${center.x + x} ${center.y + y} ${center.z + z} ${block}`); + } + } + } + } + } + + return commands; +} + +/** + * Generate a cylinder of blocks. + * @param {{ x: number, y: number, z: number }} base - Bottom center + * @param {number} radius + * @param {number} height + * @param {string} block + * @param {boolean} [hollow=false] + * @returns {string[]} Array of commands (uses fill for full layers) + */ +export function generateCylinder(base, radius, height, block, hollow = false) { + const commands = []; + const r2 = radius * radius; + const inner2 = hollow ? (radius - 1) * (radius - 1) : -1; + + for (let y = 0; y < height; y++) { + if (!hollow) { + // For solid cylinders, collect rows and use fill where possible + const rows = collectCircleRows(base.x, base.z, radius); + for (const row of rows) { + commands.push(`fill ${row.x1} ${base.y + y} ${row.z} ${row.x2} ${base.y + y} ${row.z} ${block}`); + } + } else { + for (let x = -radius; x <= radius; x++) { + for (let z = -radius; z <= radius; z++) { + const dist2 = x * x + z * z; + if (dist2 <= r2 && dist2 > inner2) { + commands.push(`setblock ${base.x + x} ${base.y + y} ${base.z + z} ${block}`); + } + } + } + } + } + + return commands; +} + +/** + * Generate a dome (half-sphere on top of a base point). + * @param {{ x: number, y: number, z: number }} base - Center of the dome base + * @param {number} radius + * @param {string} block + * @returns {string[]} Array of setblock commands + */ +export function generateDome(base, radius, block) { + const commands = []; + const r2 = radius * radius; + const inner2 = (radius - 1) * (radius - 1); + + for (let y = 0; y <= radius; y++) { + for (let x = -radius; x <= radius; x++) { + for (let z = -radius; z <= radius; z++) { + const dist2 = x * x + y * y + z * z; + if (dist2 <= r2 && dist2 > inner2) { + commands.push(`setblock ${base.x + x} ${base.y + y} ${base.z + z} ${block}`); + } + } + } + } + + return commands; +} + +/** + * Generate a pyramid. + * @param {{ x: number, y: number, z: number }} base - Center of the pyramid base + * @param {number} size - Base half-width + * @param {string} block + * @param {boolean} [hollow=false] + * @returns {string[]} Array of commands (uses fill for layers) + */ +export function generatePyramid(base, size, block, hollow = false) { + const commands = []; + + for (let layer = 0; layer <= size; layer++) { + const halfWidth = size - layer; + const y = base.y + layer; + + if (halfWidth === 0) { + // Peak - single block + commands.push(`setblock ${base.x} ${y} ${base.z} ${block}`); + } else if (!hollow) { + // Solid layer - single fill command + commands.push( + `fill ${base.x - halfWidth} ${y} ${base.z - halfWidth} ${base.x + halfWidth} ${y} ${base.z + halfWidth} ${block}` + ); + } else { + // Hollow - only the perimeter of each layer + // Four edges using fill + commands.push( + `fill ${base.x - halfWidth} ${y} ${base.z - halfWidth} ${base.x + halfWidth} ${y} ${base.z - halfWidth} ${block}` + ); + commands.push( + `fill ${base.x - halfWidth} ${y} ${base.z + halfWidth} ${base.x + halfWidth} ${y} ${base.z + halfWidth} ${block}` + ); + if (halfWidth > 1) { + commands.push( + `fill ${base.x - halfWidth} ${y} ${base.z - halfWidth + 1} ${base.x - halfWidth} ${y} ${base.z + halfWidth - 1} ${block}` + ); + commands.push( + `fill ${base.x + halfWidth} ${y} ${base.z - halfWidth + 1} ${base.x + halfWidth} ${y} ${base.z + halfWidth - 1} ${block}` + ); + } + } + } + + return commands; +} + +/** + * Generate a wall between two points. + * @param {{ x: number, y: number, z: number }} start + * @param {{ x: number, y: number, z: number }} end + * @param {number} height + * @param {string} block + * @returns {string[]} Array of fill commands + */ +export function generateWall(start, end, height, block) { + const commands = []; + + // Use fill for the entire wall (works for axis-aligned and diagonal) + const x1 = Math.min(start.x, end.x); + const x2 = Math.max(start.x, end.x); + const z1 = Math.min(start.z, end.z); + const z2 = Math.max(start.z, end.z); + const y1 = Math.min(start.y, end.y); + const y2 = y1 + height - 1; + + // If axis-aligned, single fill command + if (x1 === x2 || z1 === z2) { + commands.push(`fill ${x1} ${y1} ${z1} ${x2} ${y2} ${z2} ${block}`); + } else { + // Diagonal wall: use Bresenham-style line of fill columns + const dx = end.x - start.x; + const dz = end.z - start.z; + const steps = Math.max(Math.abs(dx), Math.abs(dz)); + + for (let i = 0; i <= steps; i++) { + const t = steps === 0 ? 0 : i / steps; + const wx = Math.round(start.x + dx * t); + const wz = Math.round(start.z + dz * t); + commands.push(`fill ${wx} ${y1} ${wz} ${wx} ${y2} ${wz} ${block}`); + } + } + + return commands; +} + +/** + * Generate a box (rectangular prism). + * @param {{ x: number, y: number, z: number }} corner1 + * @param {{ x: number, y: number, z: number }} corner2 + * @param {string} block + * @param {boolean} [hollow=false] + * @returns {string[]} Array of fill commands + */ +export function generateBox(corner1, corner2, block, hollow = false) { + if (!hollow) { + return [ + `fill ${corner1.x} ${corner1.y} ${corner1.z} ${corner2.x} ${corner2.y} ${corner2.z} ${block}`, + ]; + } + + // Hollow box: 6 faces + const x1 = Math.min(corner1.x, corner2.x); + const x2 = Math.max(corner1.x, corner2.x); + const y1 = Math.min(corner1.y, corner2.y); + const y2 = Math.max(corner1.y, corner2.y); + const z1 = Math.min(corner1.z, corner2.z); + const z2 = Math.max(corner1.z, corner2.z); + + return [ + // Bottom and top faces + `fill ${x1} ${y1} ${z1} ${x2} ${y1} ${z2} ${block}`, + `fill ${x1} ${y2} ${z1} ${x2} ${y2} ${z2} ${block}`, + // Front and back walls + `fill ${x1} ${y1 + 1} ${z1} ${x2} ${y2 - 1} ${z1} ${block}`, + `fill ${x1} ${y1 + 1} ${z2} ${x2} ${y2 - 1} ${z2} ${block}`, + // Left and right walls + `fill ${x1} ${y1 + 1} ${z1 + 1} ${x1} ${y2 - 1} ${z2 - 1} ${block}`, + `fill ${x2} ${y1 + 1} ${z1 + 1} ${x2} ${y2 - 1} ${z2 - 1} ${block}`, + ]; +} + +/** + * Collect circle rows for efficient fill commands. + * Returns horizontal line segments that fill a circular cross-section. + */ +function collectCircleRows(cx, cz, radius) { + const rows = []; + const r2 = radius * radius; + + for (let z = -radius; z <= radius; z++) { + // Find the x extent at this z + const maxX = Math.floor(Math.sqrt(r2 - z * z)); + if (maxX >= 0) { + rows.push({ x1: cx - maxX, x2: cx + maxX, z: cz + z }); + } + } + + return rows; +} + +/** + * Mapping of shape names to generator functions and their parameters. + */ +export const SHAPES = { + sphere: { + generate: generateSphere, + params: ['center', 'radius', 'block', 'hollow'], + description: 'A sphere centered at a point', + }, + cylinder: { + generate: generateCylinder, + params: ['base', 'radius', 'height', 'block', 'hollow'], + description: 'A cylinder from a base point upward', + }, + dome: { + generate: generateDome, + params: ['base', 'radius', 'block'], + description: 'A half-sphere dome from a base point', + }, + pyramid: { + generate: generatePyramid, + params: ['base', 'size', 'block', 'hollow'], + description: 'A pyramid from a base center point', + }, + wall: { + generate: generateWall, + params: ['start', 'end', 'height', 'block'], + description: 'A wall between two points', + }, + box: { + generate: generateBox, + params: ['corner1', 'corner2', 'block', 'hollow'], + description: 'A rectangular box between two corners', + }, +}; diff --git a/src/command-queue.js b/src/command-queue.js index b87a6fa..0dcfb4f 100644 --- a/src/command-queue.js +++ b/src/command-queue.js @@ -1,181 +1,241 @@ -import { log, logError } from './utils.js'; - -const TAG = 'CommandQueue'; - -/** - * Rate-limited command dispatcher for Minecraft Bedrock. - * Bedrock has a hard limit of ~100 in-flight commands. - * We cap at 80 and throttle at 50ms between commands. - */ -export class CommandQueue { - /** - * @param {object} opts - * @param {number} opts.maxInFlight - Max concurrent commands (default 80) - * @param {number} opts.throttleMs - Delay between commands in ms (default 50) - * @param {number} opts.batchSize - Commands per batch for build mode (default 20) - * @param {number} opts.batchDelayMs - Delay between batches in ms (default 200) - */ - constructor(opts = {}) { - this.maxInFlight = opts.maxInFlight ?? 80; - this.throttleMs = opts.throttleMs ?? 50; - this.batchSize = opts.batchSize ?? 20; - this.batchDelayMs = opts.batchDelayMs ?? 200; - - /** @type {Map} */ - this._pending = new Map(); - this._queue = []; - this._processing = false; - this._sendFn = null; - this._totalSent = 0; - this._totalCompleted = 0; - } - - /** - * Set the function used to actually send a command over WebSocket. - * @param {(id: string, message: string) => void} fn - */ - setSendFunction(fn) { - this._sendFn = fn; - } - - /** - * Called when a command response comes back from Bedrock. - * @param {string} requestId - * @param {object} response - */ - handleResponse(requestId, response) { - const entry = this._pending.get(requestId); - if (entry) { - clearTimeout(entry.timer); - this._pending.delete(requestId); - this._totalCompleted++; - entry.resolve(response); - } - } - - /** - * Enqueue a single command for dispatch. - * @param {string} id - Request UUID - * @param {string} message - Serialized WS message - * @param {number} timeoutMs - Per-command timeout (default 10s) - * @returns {Promise} Resolves with Bedrock response - */ - enqueue(id, message, timeoutMs = 10000) { - return new Promise((resolve, reject) => { - this._queue.push({ id, message, resolve, reject, timeoutMs }); - this._processQueue(); - }); - } - - /** - * Enqueue a batch of commands (for building). - * Sends in groups of batchSize with batchDelayMs between groups. - * @param {Array<{id: string, message: string}>} commands - * @returns {Promise<{total: number, succeeded: number, failed: number, results: Array}>} - */ - async enqueueBatch(commands) { - const results = []; - let succeeded = 0; - let failed = 0; - - for (let i = 0; i < commands.length; i += this.batchSize) { - const batch = commands.slice(i, i + this.batchSize); - - const batchResults = await Promise.allSettled( - batch.map((cmd) => this.enqueue(cmd.id, cmd.message)) - ); - - for (const result of batchResults) { - if (result.status === 'fulfilled') { - succeeded++; - results.push(result.value); - } else { - failed++; - results.push({ error: result.reason?.message || 'unknown error' }); - } - } - - // Delay between batches (except after the last one) - if (i + this.batchSize < commands.length) { - await this._delay(this.batchDelayMs); - } - } - - return { total: commands.length, succeeded, failed, results }; - } - - /** Process queued commands respecting rate limits */ - async _processQueue() { - if (this._processing) return; - this._processing = true; - - while (this._queue.length > 0) { - // Wait if at capacity - if (this._pending.size >= this.maxInFlight) { - await this._delay(this.throttleMs); - continue; - } - - const item = this._queue.shift(); - if (!item) break; - - if (!this._sendFn) { - item.reject(new Error('No WebSocket connection')); - continue; - } - - // Set up timeout - const timer = setTimeout(() => { - const entry = this._pending.get(item.id); - if (entry) { - this._pending.delete(item.id); - entry.reject(new Error('Command timed out')); - } - }, item.timeoutMs); - - this._pending.set(item.id, { - resolve: item.resolve, - reject: item.reject, - timer, - }); - - try { - this._sendFn(item.id, item.message); - this._totalSent++; - } catch (err) { - clearTimeout(timer); - this._pending.delete(item.id); - item.reject(err); - } - - // Throttle between sends - await this._delay(this.throttleMs); - } - - this._processing = false; - } - - /** @returns {{queueSize: number, inFlight: number, totalSent: number, totalCompleted: number}} */ - getStatus() { - return { - queueSize: this._queue.length, - inFlight: this._pending.size, - totalSent: this._totalSent, - totalCompleted: this._totalCompleted, - }; - } - - _delay(ms) { - return new Promise((r) => setTimeout(r, ms)); - } - - /** Clean up all pending timeouts */ - destroy() { - for (const entry of this._pending.values()) { - clearTimeout(entry.timer); - entry.reject(new Error('Queue destroyed')); - } - this._pending.clear(); - this._queue = []; - } -} +import { log, logError } from './utils.js'; + +const TAG = 'CommandQueue'; + +/** + * Rate-limited command dispatcher for Minecraft Bedrock. + * Bedrock has a hard limit of ~100 in-flight commands. + * We cap at 80 and throttle at 50ms between commands. + */ +export class CommandQueue { + /** + * @param {object} opts + * @param {number} opts.maxInFlight - Max concurrent commands (default 80) + * @param {number} opts.throttleMs - Delay between commands in ms (default 50) + * @param {number} opts.batchSize - Commands per batch for build mode (default 20) + * @param {number} opts.batchDelayMs - Delay between batches in ms (default 200) + * @param {number} opts.maxBuildCommands - Max commands per build (default from env or 5000) + */ + constructor(opts = {}) { + this.maxInFlight = opts.maxInFlight ?? 80; + this.throttleMs = opts.throttleMs ?? 50; + this.batchSize = opts.batchSize ?? 20; + this.batchDelayMs = opts.batchDelayMs ?? 200; + this.maxBuildCommands = opts.maxBuildCommands ?? parseInt(process.env.MAX_BUILD_COMMANDS || '5000', 10); + + /** @type {Map} */ + this._pending = new Map(); + this._queue = []; + this._processing = false; + this._sendFn = null; + this._totalSent = 0; + this._totalCompleted = 0; + this._cancelBuild = false; + } + + /** + * Set the function used to actually send a command over WebSocket. + * @param {(id: string, message: string) => void} fn + */ + setSendFunction(fn) { + this._sendFn = fn; + } + + /** + * Called when a command response comes back from Bedrock. + * @param {string} requestId + * @param {object} response + */ + handleResponse(requestId, response) { + const entry = this._pending.get(requestId); + if (entry) { + clearTimeout(entry.timer); + this._pending.delete(requestId); + this._totalCompleted++; + entry.resolve(response); + } + } + + /** + * Enqueue a single command for dispatch. + * @param {string} id - Request UUID + * @param {string} message - Serialized WS message + * @param {number} timeoutMs - Per-command timeout (default 10s) + * @returns {Promise} Resolves with Bedrock response + */ + enqueue(id, message, timeoutMs = 10000) { + return new Promise((resolve, reject) => { + this._queue.push({ id, message, resolve, reject, timeoutMs }); + this._processQueue(); + }); + } + + /** + * Enqueue a batch of commands (for building). + * Sends in groups of batchSize with batchDelayMs between groups. + * @param {Array<{id: string, message: string}>} commands + * @returns {Promise<{total: number, succeeded: number, failed: number, results: Array}>} + */ + async enqueueBatch(commands) { + const results = []; + let succeeded = 0; + let failed = 0; + + for (let i = 0; i < commands.length; i += this.batchSize) { + const batch = commands.slice(i, i + this.batchSize); + + const batchResults = await Promise.allSettled( + batch.map((cmd) => this.enqueue(cmd.id, cmd.message)) + ); + + for (const result of batchResults) { + if (result.status === 'fulfilled') { + succeeded++; + results.push(result.value); + } else { + failed++; + results.push({ error: result.reason?.message || 'unknown error' }); + } + } + + // Delay between batches (except after the last one) + if (i + this.batchSize < commands.length) { + await this._delay(this.batchDelayMs); + } + } + + return { total: commands.length, succeeded, failed, results }; + } + + /** + * Enqueue a batch with progress reporting. + * Calls progressFn with status updates between layer batches. + * @param {Array<{id: string, message: string}>} commands + * @param {(progress: { completed: number, total: number, percent: number }) => void} [progressFn] + * @returns {Promise<{total: number, succeeded: number, failed: number, cancelled: boolean, results: Array}>} + */ + async enqueueBatchWithProgress(commands, progressFn) { + this._cancelBuild = false; + const results = []; + let succeeded = 0; + let failed = 0; + + for (let i = 0; i < commands.length; i += this.batchSize) { + // Check cancellation + if (this._cancelBuild) { + log(TAG, `Build cancelled at ${i}/${commands.length} commands`); + return { total: commands.length, succeeded, failed, cancelled: true, results }; + } + + const batch = commands.slice(i, i + this.batchSize); + + const batchResults = await Promise.allSettled( + batch.map((cmd) => this.enqueue(cmd.id, cmd.message)) + ); + + for (const result of batchResults) { + if (result.status === 'fulfilled') { + succeeded++; + results.push(result.value); + } else { + failed++; + results.push({ error: result.reason?.message || 'unknown error' }); + } + } + + // Report progress + const completed = i + batch.length; + const percent = Math.round((completed / commands.length) * 100); + if (progressFn) { + progressFn({ completed, total: commands.length, percent }); + } + + // Delay between batches (except after the last one) + if (i + this.batchSize < commands.length) { + await this._delay(this.batchDelayMs); + } + } + + return { total: commands.length, succeeded, failed, cancelled: false, results }; + } + + /** Cancel an in-progress build */ + cancelBuild() { + this._cancelBuild = true; + } + + /** Process queued commands respecting rate limits */ + async _processQueue() { + if (this._processing) return; + this._processing = true; + + while (this._queue.length > 0) { + // Wait if at capacity + if (this._pending.size >= this.maxInFlight) { + await this._delay(this.throttleMs); + continue; + } + + const item = this._queue.shift(); + if (!item) break; + + if (!this._sendFn) { + item.reject(new Error('No WebSocket connection')); + continue; + } + + // Set up timeout + const timer = setTimeout(() => { + const entry = this._pending.get(item.id); + if (entry) { + this._pending.delete(item.id); + entry.reject(new Error('Command timed out')); + } + }, item.timeoutMs); + + this._pending.set(item.id, { + resolve: item.resolve, + reject: item.reject, + timer, + }); + + try { + this._sendFn(item.id, item.message); + this._totalSent++; + } catch (err) { + clearTimeout(timer); + this._pending.delete(item.id); + item.reject(err); + } + + // Throttle between sends + await this._delay(this.throttleMs); + } + + this._processing = false; + } + + /** @returns {{queueSize: number, inFlight: number, totalSent: number, totalCompleted: number}} */ + getStatus() { + return { + queueSize: this._queue.length, + inFlight: this._pending.size, + totalSent: this._totalSent, + totalCompleted: this._totalCompleted, + }; + } + + _delay(ms) { + return new Promise((r) => setTimeout(r, ms)); + } + + /** Clean up all pending timeouts */ + destroy() { + for (const entry of this._pending.values()) { + clearTimeout(entry.timer); + entry.reject(new Error('Queue destroyed')); + } + this._pending.clear(); + this._queue = []; + } +} diff --git a/src/encryption.js b/src/encryption.js new file mode 100644 index 0000000..5a9df51 --- /dev/null +++ b/src/encryption.js @@ -0,0 +1,107 @@ +import { createECDH, createHash, createCipheriv, createDecipheriv, randomBytes } from 'node:crypto'; +import { log, logError } from './utils.js'; + +const TAG = 'Encryption'; + +// ASN.1 DER header for a P-384 (secp384r1) uncompressed public key +const ASN1_HEADER = Buffer.from( + '3076301006072a8648ce3d020106052b81040022036200', + 'hex' +); + +/** + * Handles Bedrock's application-level encryption handshake. + * + * Protocol: + * 1. Server generates ECDH keypair on secp384r1 + * 2. Server sends `enableencryption` with its public key + random salt + * 3. Client responds with its public key + * 4. Both derive: key = SHA-256(salt + ECDH_shared_secret) + * 5. IV = key[0..16], cipher = AES-256-CFB8 (streaming, stateful) + */ +export class ServerEncryption { + constructor() { + this._ecdh = createECDH('secp384r1'); + this._ecdh.generateKeys(); + this._salt = randomBytes(16); + + /** @type {import('node:crypto').Cipher | null} */ + this._cipher = null; + /** @type {import('node:crypto').Decipher | null} */ + this._decipher = null; + this._enabled = false; + } + + /** + * Get the parameters needed for the enableencryption command. + * @returns {{ publicKey: string, salt: string }} base64-encoded values + */ + getKeyExchangeParams() { + // Bedrock expects the public key wrapped in ASN.1 DER format + const rawPub = this._ecdh.getPublicKey(); + const derPub = Buffer.concat([ASN1_HEADER, rawPub]); + + return { + publicKey: derPub.toString('base64'), + salt: this._salt.toString('base64'), + }; + } + + /** + * Complete the key exchange using the client's public key. + * After this call, encrypt() and decrypt() are operational. + * @param {string} clientPubKeyBase64 - Client's base64-encoded public key (may have ASN.1 header) + */ + completeKeyExchange(clientPubKeyBase64) { + let clientPubRaw = Buffer.from(clientPubKeyBase64, 'base64'); + + // Strip ASN.1 header if present (Bedrock sends the raw key wrapped in DER) + if (clientPubRaw.length > 97) { + // P-384 uncompressed point is 97 bytes (0x04 + 48 + 48) + clientPubRaw = clientPubRaw.slice(clientPubRaw.length - 97); + } + + const sharedSecret = this._ecdh.computeSecret(clientPubRaw); + + // key = SHA-256(salt + shared_secret) + const hash = createHash('sha256'); + hash.update(this._salt); + hash.update(sharedSecret); + const key = hash.digest(); + + // IV = first 16 bytes of key + const iv = key.slice(0, 16); + + this._cipher = createCipheriv('aes-256-cfb8', key, iv); + this._decipher = createDecipheriv('aes-256-cfb8', key, iv); + this._enabled = true; + + log(TAG, 'Encryption handshake complete!'); + } + + /** + * Encrypt a plaintext message. + * @param {string|Buffer} plaintext + * @returns {Buffer} ciphertext + */ + encrypt(plaintext) { + if (!this._cipher) throw new Error('Encryption not initialized'); + const input = typeof plaintext === 'string' ? Buffer.from(plaintext, 'utf8') : plaintext; + return this._cipher.update(input); + } + + /** + * Decrypt a ciphertext message. + * @param {Buffer} ciphertext + * @returns {string} plaintext UTF-8 string + */ + decrypt(ciphertext) { + if (!this._decipher) throw new Error('Encryption not initialized'); + return this._decipher.update(ciphertext).toString('utf8'); + } + + /** @returns {boolean} Whether encryption is active */ + get enabled() { + return this._enabled; + } +} diff --git a/src/event-store.js b/src/event-store.js index 831faa7..fc5d494 100644 --- a/src/event-store.js +++ b/src/event-store.js @@ -1,61 +1,87 @@ -/** - * In-memory ring buffer for Minecraft game events. - * Privacy-first: no disk persistence, lost on restart. - */ -export class EventStore { - /** @param {number} capacity - Maximum events to retain */ - constructor(capacity = 100) { - this._capacity = capacity; - /** @type {Array<{type: string, timestamp: string, data: object}>} */ - this._events = []; - } - - /** - * Push a new event into the ring buffer. - * @param {string} type - Event type (e.g. "PlayerMessage") - * @param {object} data - Event payload - */ - push(type, data) { - this._events.push({ - type, - timestamp: new Date().toISOString(), - data, - }); - - // Trim to capacity - if (this._events.length > this._capacity) { - this._events = this._events.slice(-this._capacity); - } - } - - /** - * Get the most recent events. - * @param {number} count - Number of events to return - * @returns {Array} - */ - getRecent(count = 20) { - return this._events.slice(-count); - } - - /** - * Get recent events filtered by type. - * @param {string} type - Event type to filter by - * @param {number} count - Max number to return - * @returns {Array} - */ - getByType(type, count = 20) { - return this._events - .filter((e) => e.type === type) - .slice(-count); - } - - /** @returns {number} Total events currently stored */ - get size() { - return this._events.length; - } - - /** Clear all events */ - clear() { - this._events = []; - } -} +/** + * In-memory ring buffer for Minecraft game events. + * Privacy-first: no disk persistence by default, lost on restart. + * Capacity configurable via EVENT_BUFFER_SIZE env var. + */ +export class EventStore { + /** @param {number} capacity - Maximum events to retain */ + constructor(capacity) { + this._capacity = capacity ?? parseInt(process.env.EVENT_BUFFER_SIZE || '1000', 10); + /** @type {Array<{type: string, timestamp: string, data: object}>} */ + this._events = []; + } + + /** + * Push a new event into the ring buffer. + * @param {string} type - Event type (e.g. "PlayerMessage") + * @param {object} data - Event payload + */ + push(type, data) { + this._events.push({ + type, + timestamp: new Date().toISOString(), + data, + }); + + // Trim to capacity + if (this._events.length > this._capacity) { + this._events = this._events.slice(-this._capacity); + } + } + + /** + * Get the most recent events. + * @param {number} count - Number of events to return + * @returns {Array} + */ + getRecent(count = 20) { + return this._events.slice(-count); + } + + /** + * Get recent events filtered by type. + * @param {string} type - Event type to filter by + * @param {number} count - Max number to return + * @returns {Array} + */ + getByType(type, count = 20) { + return this._events + .filter((e) => e.type === type) + .slice(-count); + } + + /** + * Get recent events filtered by multiple types. + * @param {string[]} types - Event types to include + * @param {number} count - Max number to return + * @returns {Array} + */ + getByTypes(types, count = 20) { + const typeSet = new Set(types); + return this._events + .filter((e) => typeSet.has(e.type)) + .slice(-count); + } + + /** + * Get events since a given timestamp. + * @param {string} timestamp - ISO timestamp + * @param {number} count - Max number to return + * @returns {Array} + */ + getSince(timestamp, count = 100) { + return this._events + .filter((e) => e.timestamp >= timestamp) + .slice(-count); + } + + /** @returns {number} Total events currently stored */ + get size() { + return this._events.length; + } + + /** Clear all events */ + clear() { + this._events = []; + } +} diff --git a/src/grabcraft.js b/src/grabcraft.js new file mode 100644 index 0000000..924e274 --- /dev/null +++ b/src/grabcraft.js @@ -0,0 +1,396 @@ +import { log, logError } from './utils.js'; +import { resolveBlock, formatBlock, getUnknownBlocks, clearUnknownBlocks } from './block-map.js'; + +const TAG = 'GrabCraft'; + +const GRABCRAFT_BASE = 'https://www.grabcraft.com'; +const USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'; + +/** + * LRU cache for fetched blueprints. + */ +class LRUCache { + constructor(maxSize = 50, ttlMs = 3600000) { + this._maxSize = maxSize; + this._ttlMs = ttlMs; + /** @type {Map} */ + this._cache = new Map(); + } + + get(key) { + const entry = this._cache.get(key); + if (!entry) return null; + if (Date.now() - entry.timestamp > this._ttlMs) { + this._cache.delete(key); + return null; + } + // Move to end (most recently used) + this._cache.delete(key); + this._cache.set(key, entry); + return entry.data; + } + + set(key, data) { + this._cache.delete(key); + if (this._cache.size >= this._maxSize) { + // Delete oldest (first entry) + const firstKey = this._cache.keys().next().value; + this._cache.delete(firstKey); + } + this._cache.set(key, { data, timestamp: Date.now() }); + } +} + +const blueprintCache = new LRUCache(50, 3600000); + +/** + * Search GrabCraft for blueprints matching a query. + * @param {string} query - Search term + * @param {number} [page=1] - Page number + * @returns {Promise<{ results: Array<{ name: string, url: string, category: string, blocks: string }>, total: number, page: number }>} + */ +export async function searchBlueprints(query, page = 1) { + const searchUrl = `${GRABCRAFT_BASE}/search/${encodeURIComponent(query)}/${page}`; + log(TAG, `Searching: ${searchUrl}`); + + const html = await fetchPage(searchUrl); + + const results = []; + + // GrabCraft search results are in div.browse-item or similar card elements + // Pattern: ...
Name
... + const itemRegex = /]+href="(\/minecraft\/[^"]+)"[^>]*>[\s\S]*?]*class="[^"]*browse-item-title[^"]*"[^>]*>([\s\S]*?)<\/div>/gi; + let match; + while ((match = itemRegex.exec(html)) !== null) { + const url = GRABCRAFT_BASE + match[1]; + const name = match[2].replace(/<[^>]+>/g, '').trim(); + + // Try to extract category and block count from surrounding context + const contextStart = Math.max(0, match.index - 500); + const contextEnd = Math.min(html.length, match.index + match[0].length + 500); + const context = html.substring(contextStart, contextEnd); + + const categoryMatch = context.match(/category[^>]*>([^<]+)/i); + const blockMatch = context.match(/(\d+)\s*blocks?/i); + + results.push({ + name, + url, + category: categoryMatch ? categoryMatch[1].trim() : 'Unknown', + blocks: blockMatch ? blockMatch[1] : 'Unknown', + }); + } + + // Fallback: try alternate HTML structure + if (results.length === 0) { + const altRegex = /]+href="(\/minecraft\/[^"]+)"[^>]*class="[^"]*"[^>]*>[\s\S]*?<[^>]+>([^<]{3,})<\/[^>]+>/gi; + while ((match = altRegex.exec(html)) !== null) { + const url = GRABCRAFT_BASE + match[1]; + const name = match[2].replace(/<[^>]+>/g, '').trim(); + if (name && name.length > 2 && !name.includes('{') && !name.includes('function')) { + results.push({ name, url, category: 'Unknown', blocks: 'Unknown' }); + } + } + } + + // Try to get total count + const totalMatch = html.match(/(\d+)\s*results?/i); + const total = totalMatch ? parseInt(totalMatch[1], 10) : results.length; + + log(TAG, `Found ${results.length} results (total: ${total})`); + return { results, total, page }; +} + +/** + * Fetch and parse a GrabCraft blueprint page. + * Extracts the embedded voxel data (myRenderObject), materials, and layer map. + * @param {string} url - Full GrabCraft blueprint URL + * @returns {Promise} Parsed blueprint data + */ +export async function fetchBlueprint(url) { + // Check cache + const cached = blueprintCache.get(url); + if (cached) { + log(TAG, `Cache hit: ${url}`); + return cached; + } + + log(TAG, `Fetching blueprint: ${url}`); + const html = await fetchPage(url); + + // Extract the name from page title + const titleMatch = html.match(/([^<]+)<\/title>/i); + const name = titleMatch + ? titleMatch[1].replace(/\s*[-|].*$/, '').replace(/GrabCraft/i, '').trim() + : 'Unknown Blueprint'; + + // Extract render object (3D voxel data) + const voxels = extractRenderObject(html); + + // Extract materials list + const materials = extractMaterials(html); + + // Calculate dimensions + let minX = Infinity, minY = Infinity, minZ = Infinity; + let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity; + + for (const voxel of voxels) { + minX = Math.min(minX, voxel.x); + minY = Math.min(minY, voxel.y); + minZ = Math.min(minZ, voxel.z); + maxX = Math.max(maxX, voxel.x); + maxY = Math.max(maxY, voxel.y); + maxZ = Math.max(maxZ, voxel.z); + } + + const dimensions = voxels.length > 0 + ? { width: maxX - minX + 1, height: maxY - minY + 1, depth: maxZ - minZ + 1 } + : { width: 0, height: 0, depth: 0 }; + + const blueprint = { + name, + url, + voxels, + materials, + dimensions, + totalBlocks: voxels.filter(v => v.matId !== '0').length, + origin: { x: minX, y: minY, z: minZ }, + }; + + // Cache it + blueprintCache.set(url, blueprint); + + log(TAG, `Parsed "${name}": ${blueprint.totalBlocks} blocks, ${dimensions.width}x${dimensions.height}x${dimensions.depth}`); + return blueprint; +} + +/** + * Convert a blueprint to Bedrock setblock commands. + * @param {object} blueprint - Parsed blueprint from fetchBlueprint() + * @param {number} originX - World X coordinate for build origin + * @param {number} originY - World Y coordinate for build origin + * @param {number} originZ - World Z coordinate for build origin + * @returns {{ commands: string[], summary: object }} + */ +export function blueprintToCommands(blueprint, originX, originY, originZ) { + clearUnknownBlocks(); + + const { voxels, origin } = blueprint; + const commands = []; + const materialCounts = new Map(); + let skippedAir = 0; + + // Sort voxels bottom-up (y ascending) for structural integrity + const sorted = [...voxels].sort((a, b) => { + if (a.y !== b.y) return a.y - b.y; + if (a.z !== b.z) return a.z - b.z; + return a.x - b.x; + }); + + for (const voxel of sorted) { + // Skip air blocks + if (voxel.matId === '0' || voxel.matId === 'air') { + skippedAir++; + continue; + } + + const resolved = resolveBlock(voxel.matId, voxel.matName); + const blockStr = formatBlock(resolved); + + // Translate to world coordinates relative to build origin + const wx = originX + (voxel.x - origin.x); + const wy = originY + (voxel.y - origin.y); + const wz = originZ + (voxel.z - origin.z); + + commands.push(`setblock ${wx} ${wy} ${wz} ${blockStr}`); + + const matKey = resolved.name; + materialCounts.set(matKey, (materialCounts.get(matKey) || 0) + 1); + } + + const unknowns = getUnknownBlocks(); + + const summary = { + name: blueprint.name, + totalVoxels: voxels.length, + totalCommands: commands.length, + skippedAir, + dimensions: blueprint.dimensions, + materials: Object.fromEntries(materialCounts), + unmappedBlocks: unknowns.size > 0 ? Object.fromEntries(unknowns) : null, + buildOrigin: { x: originX, y: originY, z: originZ }, + }; + + return { commands, summary }; +} + +/** + * Extract myRenderObject voxel data from the page HTML. + * GrabCraft embeds data like: + * myRenderObject[y][x][z] = { mat_id: "5:5", rgb: {...}, hex: "#...", ... } + */ +function extractRenderObject(html) { + const voxels = []; + + // Pattern 1: myRenderObject[y][x][z] = {...} + const renderRegex = /myRenderObject\[(\d+)\]\[(\d+)\]\[(\d+)\]\s*=\s*(\{[^}]+\})/g; + let match; + while ((match = renderRegex.exec(html)) !== null) { + const y = parseInt(match[1], 10); + const x = parseInt(match[2], 10); + const z = parseInt(match[3], 10); + const objStr = match[4]; + + const matIdMatch = objStr.match(/mat_id\s*:\s*["']([^"']+)["']/); + const matId = matIdMatch ? matIdMatch[1] : '0'; + + const hexMatch = objStr.match(/hex\s*:\s*["']([^"']+)["']/); + const hex = hexMatch ? hexMatch[1] : null; + + voxels.push({ x, y, z, matId, hex, matName: null }); + } + + // Pattern 2: layerMap-based data + // layerMap[y] = [{x:..., z:..., mat_id:...}, ...] + if (voxels.length === 0) { + const layerRegex = /layerMap\[(\d+)\]\s*=\s*\[([\s\S]*?)\];/g; + while ((match = layerRegex.exec(html)) !== null) { + const y = parseInt(match[1], 10); + const arrayContent = match[2]; + + const blockRegex = /\{[^}]*x\s*:\s*(\d+)[^}]*z\s*:\s*(\d+)[^}]*mat_id\s*:\s*["']([^"']+)["'][^}]*\}/g; + let blockMatch; + while ((blockMatch = blockRegex.exec(arrayContent)) !== null) { + voxels.push({ + x: parseInt(blockMatch[1], 10), + y, + z: parseInt(blockMatch[2], 10), + matId: blockMatch[3], + hex: null, + matName: null, + }); + } + } + } + + // Pattern 3: JSON array of blocks + if (voxels.length === 0) { + const jsonRegex = /var\s+blocks?\s*=\s*(\[[\s\S]*?\]);/; + const jsonMatch = html.match(jsonRegex); + if (jsonMatch) { + try { + const blocks = JSON.parse(jsonMatch[1]); + for (const b of blocks) { + voxels.push({ + x: b.x || 0, + y: b.y || 0, + z: b.z || 0, + matId: String(b.mat_id || b.id || '0'), + hex: b.hex || null, + matName: b.name || null, + }); + } + } catch { + logError(TAG, 'Failed to parse blocks JSON'); + } + } + } + + log(TAG, `Extracted ${voxels.length} voxels from page`); + + // Cross-reference with materials if names are missing + return voxels; +} + +/** + * Extract materials list from Highcharts series data. + * GrabCraft pages include chart data like: + * series: [{ name: "Stone", id: "1", data: [{y: 500}], ... }] + */ +function extractMaterials(html) { + const materials = []; + + // Pattern: series data in Highcharts config + // Look for objects with id, name, and y (count) fields + const seriesRegex = /\{\s*(?:name|id)\s*:\s*["']([^"']+)["']\s*,\s*(?:id|name)\s*:\s*["']([^"']+)["'][^}]*y\s*:\s*(\d+)/g; + let match; + while ((match = seriesRegex.exec(html)) !== null) { + // Determine which is name vs id based on content + let id, name, count; + if (match[1].match(/^\d/)) { + id = match[1]; + name = match[2]; + } else { + name = match[1]; + id = match[2]; + } + count = parseInt(match[3], 10); + + materials.push({ id, name, count }); + } + + // Alternative: look for material list in a different format + if (materials.length === 0) { + const matRegex = /id\s*:\s*["'](\d+(?::\d+)?)["']\s*,\s*name\s*:\s*["']([^"']+)["']\s*,\s*y\s*:\s*(\d+)/g; + while ((match = matRegex.exec(html)) !== null) { + materials.push({ + id: match[1], + name: match[2], + count: parseInt(match[3], 10), + }); + } + } + + log(TAG, `Extracted ${materials.length} materials`); + return materials; +} + +/** + * Fetch a page with standard headers. + * @param {string} url + * @returns {Promise<string>} HTML content + */ +async function fetchPage(url) { + const response = await fetch(url, { + headers: { + 'User-Agent': USER_AGENT, + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + 'Accept-Language': 'en-US,en;q=0.5', + }, + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText} for ${url}`); + } + + return response.text(); +} + +/** + * Get available GrabCraft categories. + * @returns {Array<{ name: string, slug: string }>} + */ +export function getCategories() { + return [ + { name: 'Houses & Mansions', slug: 'houses' }, + { name: 'Castles & Fortresses', slug: 'castles' }, + { name: 'Medieval Buildings', slug: 'medieval' }, + { name: 'Modern Buildings', slug: 'modern' }, + { name: 'Towers', slug: 'towers' }, + { name: 'Churches & Temples', slug: 'churches' }, + { name: 'Ships & Boats', slug: 'ships' }, + { name: 'Bridges', slug: 'bridges' }, + { name: 'Farms', slug: 'farms' }, + { name: 'Statues & Sculptures', slug: 'statues' }, + { name: 'Pixel Art', slug: 'pixel-art' }, + { name: 'Redstone Devices', slug: 'redstone' }, + { name: 'Gardens & Parks', slug: 'gardens' }, + { name: 'Furniture & Decor', slug: 'furniture' }, + { name: 'Vehicles', slug: 'vehicles' }, + { name: 'Fantasy', slug: 'fantasy' }, + { name: 'Sci-Fi', slug: 'sci-fi' }, + { name: 'Animals', slug: 'animals' }, + { name: 'Trees & Nature', slug: 'trees' }, + { name: 'Underground', slug: 'underground' }, + ]; +} diff --git a/src/index.js b/src/index.js index 7ede939..0e3f616 100644 --- a/src/index.js +++ b/src/index.js @@ -1,36 +1,36 @@ -import { BedrockWebSocket } from './bedrock-ws.js'; -import { startMcpServer } from './mcp-server.js'; -import { log } from './utils.js'; - -const TAG = 'Main'; - -const WS_PORT = parseInt(process.env.WS_PORT || '3001', 10); -const MCP_PORT = parseInt(process.env.MCP_PORT || '3002', 10); - -log(TAG, 'Minecraft Bedrock AI Bridge starting...'); - -// Start the Bedrock WebSocket server -const bedrock = new BedrockWebSocket({ port: WS_PORT }); -bedrock.start(); - -// Start the MCP server (Streamable HTTP transport) -startMcpServer(bedrock, MCP_PORT); - -log(TAG, ''); -log(TAG, '=== READY ==='); -log(TAG, `Minecraft WebSocket : ws://0.0.0.0:${WS_PORT}`); -log(TAG, `MCP HTTP endpoint : http://0.0.0.0:${MCP_PORT}/mcp`); -log(TAG, `Health check : http://0.0.0.0:${MCP_PORT}/health`); -log(TAG, ''); -log(TAG, 'In Minecraft, type: /connect ws://<your-ip>:' + WS_PORT); -log(TAG, ''); - -// Graceful shutdown -function shutdown(signal) { - log(TAG, `${signal} received, shutting down...`); - bedrock.stop(); - process.exit(0); -} - -process.on('SIGINT', () => shutdown('SIGINT')); -process.on('SIGTERM', () => shutdown('SIGTERM')); +import { BedrockWebSocket } from './bedrock-ws.js'; +import { startMcpServer } from './mcp-server.js'; +import { log } from './utils.js'; + +const TAG = 'Main'; + +const WS_PORT = parseInt(process.env.WS_PORT || '3001', 10); +const MCP_PORT = parseInt(process.env.MCP_PORT || '3002', 10); + +log(TAG, 'Minecraft Bedrock AI Bridge starting...'); + +// Start the Bedrock WebSocket server +const bedrock = new BedrockWebSocket({ port: WS_PORT }); +bedrock.start(); + +// Start the MCP server (Streamable HTTP transport) +startMcpServer(bedrock, MCP_PORT); + +log(TAG, ''); +log(TAG, '=== READY ==='); +log(TAG, `Minecraft WebSocket : ws://0.0.0.0:${WS_PORT}`); +log(TAG, `MCP HTTP endpoint : http://0.0.0.0:${MCP_PORT}/mcp`); +log(TAG, `Health check : http://0.0.0.0:${MCP_PORT}/health`); +log(TAG, ''); +log(TAG, 'In Minecraft, type: /connect ws://<your-ip>:' + WS_PORT); +log(TAG, ''); + +// Graceful shutdown +function shutdown(signal) { + log(TAG, `${signal} received, shutting down...`); + bedrock.stop(); + process.exit(0); +} + +process.on('SIGINT', () => shutdown('SIGINT')); +process.on('SIGTERM', () => shutdown('SIGTERM')); diff --git a/src/mcp-server.js b/src/mcp-server.js index 06dca9e..13174f5 100644 --- a/src/mcp-server.js +++ b/src/mcp-server.js @@ -1,409 +1,877 @@ -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; -import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; -import { randomUUID } from 'node:crypto'; -import express from 'express'; -import { z } from 'zod'; -import { log, logError } from './utils.js'; - -const TAG = 'MCP'; - -// Available Bedrock event types for subscription -const BEDROCK_EVENTS = [ - 'PlayerMessage', - 'BlockChanged', - 'PlayerTransform', - 'ItemUsed', - 'ItemAcquired', - 'ItemCrafted', - 'MobKilled', - 'MobInteracted', - 'PlayerTravelled', - 'PlayerDied', - 'BossKilled', -]; - -/** - * Create and configure the MCP server with all Minecraft tools. - * @param {import('./bedrock-ws.js').BedrockWebSocket} bedrock - The Bedrock WS bridge - * @param {number} port - HTTP port for MCP transport (default 3002) - */ -export function startMcpServer(bedrock, port = 3002) { - const app = express(); - app.use(express.json()); - - // Track active transports by session - const transports = {}; - - // Create a fresh McpServer for each session, wired to the same bedrock instance - function createServer() { - const server = new McpServer( - { - name: 'minecraft-bridge', - version: '1.0.0', - }, - { - capabilities: { logging: {} }, - } - ); - - // ── Tool: minecraft_command ────────────────────────────────────────── - server.registerTool( - 'minecraft_command', - { - title: 'Minecraft Command', - description: - 'Execute any slash command in Minecraft Bedrock. Examples: "give @p diamond 64", "tp @p 100 64 200", "time set night", "weather thunder". Do NOT include the leading slash.', - inputSchema: z.object({ - command: z - .string() - .describe( - 'The command to execute (without leading slash). E.g. "give @p diamond 64"' - ), - }), - }, - async ({ command }) => { - try { - const response = await bedrock.sendCommand(command); - const statusMessage = response?.statusMessage || 'Command executed'; - const statusCode = response?.statusCode ?? -1; - return { - content: [ - { - type: 'text', - text: `[${statusCode === 0 ? 'OK' : 'ERROR'}] ${statusMessage}`, - }, - ], - }; - } catch (err) { - return { - content: [{ type: 'text', text: `Error: ${err.message}` }], - isError: true, - }; - } - } - ); - - // ── Tool: minecraft_chat ──────────────────────────────────────────── - server.registerTool( - 'minecraft_chat', - { - title: 'Minecraft Chat', - description: - 'Send a chat message visible in Minecraft. Uses /say for broadcast or /tell for a specific player.', - inputSchema: z.object({ - message: z.string().describe('The message to send'), - player: z - .string() - .optional() - .describe( - 'Target player name for /tell. Omit to broadcast with /say.' - ), - }), - }, - async ({ message, player }) => { - try { - const cmd = player - ? `tell ${player} ${message}` - : `say ${message}`; - const response = await bedrock.sendCommand(cmd); - return { - content: [ - { - type: 'text', - text: `Message sent${player ? ` to ${player}` : ''}: "${message}"`, - }, - ], - }; - } catch (err) { - return { - content: [{ type: 'text', text: `Error: ${err.message}` }], - isError: true, - }; - } - } - ); - - // ── Tool: minecraft_build ─────────────────────────────────────────── - server.registerTool( - 'minecraft_build', - { - title: 'Minecraft Build', - description: - 'Execute a batch of build commands (setblock, fill, clone) with rate limiting. Max 200 commands per call. Commands should NOT have a leading slash.', - inputSchema: z.object({ - commands: z - .array(z.string()) - .max(200) - .describe( - 'Array of build commands. E.g. ["setblock 10 64 10 stone", "fill 10 64 10 20 64 20 glass"]' - ), - }), - }, - async ({ commands }) => { - // Validate: only allow build commands - const allowed = ['setblock', 'fill', 'clone', 'structure']; - const invalid = commands.filter((cmd) => { - const base = cmd.replace(/^\//, '').split(' ')[0].toLowerCase(); - return !allowed.includes(base); - }); - - if (invalid.length > 0) { - return { - content: [ - { - type: 'text', - text: `Error: Only setblock/fill/clone/structure commands allowed. Invalid: ${invalid.slice(0, 3).join(', ')}`, - }, - ], - isError: true, - }; - } - - try { - const result = await bedrock.sendBatch(commands); - return { - content: [ - { - type: 'text', - text: `Build complete: ${result.succeeded}/${result.total} commands succeeded, ${result.failed} failed`, - }, - ], - }; - } catch (err) { - return { - content: [{ type: 'text', text: `Error: ${err.message}` }], - isError: true, - }; - } - } - ); - - // ── Tool: minecraft_get_events ────────────────────────────────────── - server.registerTool( - 'minecraft_get_events', - { - title: 'Minecraft Get Events', - description: - 'Get recent game events (chat messages, block changes, etc.) from the in-memory event buffer.', - inputSchema: z.object({ - count: z - .number() - .int() - .min(1) - .max(100) - .optional() - .describe('Number of events to return (default 20, max 100)'), - type: z - .string() - .optional() - .describe( - 'Filter by event type. E.g. "PlayerMessage", "BlockChanged"' - ), - }), - }, - async ({ count, type }) => { - const n = count ?? 20; - const events = type - ? bedrock.events.getByType(type, n) - : bedrock.events.getRecent(n); - - if (events.length === 0) { - return { - content: [ - { - type: 'text', - text: 'No events recorded yet. Make sure Minecraft is connected and events are subscribed.', - }, - ], - }; - } - - const formatted = events - .map( - (e) => - `[${e.timestamp}] ${e.type}: ${JSON.stringify(e.data)}` - ) - .join('\n'); - - return { - content: [ - { - type: 'text', - text: `${events.length} event(s):\n${formatted}`, - }, - ], - }; - } - ); - - // ── Tool: minecraft_get_status ────────────────────────────────────── - server.registerTool( - 'minecraft_get_status', - { - title: 'Minecraft Status', - description: - 'Check if Minecraft is connected and get status info (player name, uptime, queue, event count).', - inputSchema: z.object({}), - }, - async () => { - const status = bedrock.getStatus(); - const lines = [ - `Connected: ${status.connected ? 'YES' : 'NO'}`, - `Player: ${status.playerName || 'unknown'}`, - `Connected since: ${status.connectedAt || 'N/A'}`, - `Subscriptions: ${status.subscriptions.join(', ') || 'none'}`, - `Events stored: ${status.eventCount}`, - `Commands sent: ${status.totalSent}`, - `Commands completed: ${status.totalCompleted}`, - `Queue size: ${status.queueSize}`, - `In-flight: ${status.inFlight}`, - ]; - - return { - content: [{ type: 'text', text: lines.join('\n') }], - }; - } - ); - - // ── Tool: minecraft_subscribe ─────────────────────────────────────── - server.registerTool( - 'minecraft_subscribe', - { - title: 'Minecraft Subscribe', - description: `Subscribe to additional Bedrock event types. Available: ${BEDROCK_EVENTS.join(', ')}`, - inputSchema: z.object({ - event: z - .string() - .describe( - `Event type to subscribe to. One of: ${BEDROCK_EVENTS.join(', ')}` - ), - }), - }, - async ({ event }) => { - if (!BEDROCK_EVENTS.includes(event)) { - return { - content: [ - { - type: 'text', - text: `Unknown event "${event}". Available: ${BEDROCK_EVENTS.join(', ')}`, - }, - ], - isError: true, - }; - } - - const ok = bedrock.subscribe(event); - if (ok) { - return { - content: [ - { type: 'text', text: `Subscribed to ${event} events` }, - ], - }; - } else { - return { - content: [ - { - type: 'text', - text: `Failed to subscribe to ${event}. Is Minecraft connected?`, - }, - ], - isError: true, - }; - } - } - ); - - return server; - } - - // ── SSE Transport endpoint ────────────────────────────────────────── - // SSE is the most widely supported MCP transport for Claude Code - app.get('/sse', async (req, res) => { - log(TAG, 'SSE client connected'); - const transport = new SSEServerTransport('/messages', res); - const sessionId = transport.sessionId; - transports[sessionId] = transport; - - transport.onclose = () => { - delete transports[sessionId]; - log(TAG, `SSE session closed: ${sessionId}`); - }; - - const server = createServer(); - await server.connect(transport); - log(TAG, `SSE session started: ${sessionId}`); - }); - - app.post('/messages', async (req, res) => { - const sessionId = req.query.sessionId; - const transport = transports[sessionId]; - if (transport) { - await transport.handlePostMessage(req, res, req.body); - } else { - res.status(400).send('Invalid or missing session'); - } - }); - - // ── Streamable HTTP Transport (modern alternative) ───────────────── - app.post('/mcp', async (req, res) => { - const sessionId = req.headers['mcp-session-id']; - - if (sessionId && transports[sessionId]) { - await transports[sessionId].handleRequest(req, res, req.body); - return; - } - - // New session - const transport = new StreamableHTTPServerTransport({ - sessionIdGenerator: () => randomUUID(), - onsessioninitialized: (id) => { - transports[id] = transport; - log(TAG, `Streamable HTTP session created: ${id}`); - }, - }); - - transport.onclose = () => { - if (transport.sessionId) { - delete transports[transport.sessionId]; - log(TAG, `Streamable HTTP session closed: ${transport.sessionId}`); - } - }; - - const server = createServer(); - await server.connect(transport); - await transport.handleRequest(req, res, req.body); - }); - - app.get('/mcp', async (req, res) => { - const sessionId = req.headers['mcp-session-id']; - if (sessionId && transports[sessionId]) { - await transports[sessionId].handleRequest(req, res); - } else { - res.status(400).send('Invalid or missing session'); - } - }); - - app.delete('/mcp', async (req, res) => { - const sessionId = req.headers['mcp-session-id']; - if (sessionId && transports[sessionId]) { - await transports[sessionId].handleRequest(req, res); - } else { - res.status(400).send('Invalid or missing session'); - } - }); - - // Health check endpoint - app.get('/health', (_req, res) => { - res.json({ - status: 'ok', - minecraft: bedrock.isConnected(), - uptime: process.uptime(), - }); - }); - - app.listen(port, '0.0.0.0', () => { - log(TAG, `MCP SSE endpoint : http://0.0.0.0:${port}/sse`); - log(TAG, `MCP HTTP endpoint : http://0.0.0.0:${port}/mcp`); - }); -} +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; +import { randomUUID } from 'node:crypto'; +import express from 'express'; +import { z } from 'zod'; +import { log, logError, createCommandMessage } from './utils.js'; +import { searchBlueprints, fetchBlueprint, blueprintToCommands, getCategories } from './grabcraft.js'; +import { getAllBlocks } from './block-map.js'; +import { SHAPES } from './building-helpers.js'; + +const TAG = 'MCP'; + +// Available Bedrock event types for subscription +const BEDROCK_EVENTS = [ + 'PlayerMessage', + 'BlockChanged', + 'PlayerTransform', + 'ItemUsed', + 'ItemAcquired', + 'ItemCrafted', + 'MobKilled', + 'MobInteracted', + 'PlayerTravelled', + 'PlayerDied', + 'BossKilled', +]; + +/** + * Create and configure the MCP server with all Minecraft tools. + * @param {import('./bedrock-ws.js').BedrockWebSocket} bedrock - The Bedrock WS bridge + * @param {number} port - HTTP port for MCP transport (default 3002) + */ +export function startMcpServer(bedrock, port = 3002) { + const app = express(); + app.use(express.json()); + + // Track active transports by session + const transports = {}; + + // Create a fresh McpServer for each session, wired to the same bedrock instance + function createServer() { + const server = new McpServer( + { + name: 'minecraft-bridge', + version: '2.0.0', + }, + { + capabilities: { logging: {}, resources: {} }, + } + ); + + // ── Tool: minecraft_command ────────────────────────────────────────── + server.registerTool( + 'minecraft_command', + { + title: 'Minecraft Command', + description: + 'Execute any slash command in Minecraft Bedrock. Examples: "give @p diamond 64", "tp @p 100 64 200", "time set night", "weather thunder". Do NOT include the leading slash.', + inputSchema: z.object({ + command: z + .string() + .describe( + 'The command to execute (without leading slash). E.g. "give @p diamond 64"' + ), + }), + }, + async ({ command }) => { + try { + const response = await bedrock.sendCommand(command); + const statusMessage = response?.statusMessage || 'Command executed'; + const statusCode = response?.statusCode ?? -1; + return { + content: [ + { + type: 'text', + text: `[${statusCode === 0 ? 'OK' : 'ERROR'}] ${statusMessage}`, + }, + ], + }; + } catch (err) { + return { + content: [{ type: 'text', text: `Error: ${err.message}` }], + isError: true, + }; + } + } + ); + + // ── Tool: minecraft_chat ──────────────────────────────────────────── + server.registerTool( + 'minecraft_chat', + { + title: 'Minecraft Chat', + description: + 'Send a chat message visible in Minecraft. Uses /say for broadcast or /tell for a specific player.', + inputSchema: z.object({ + message: z.string().describe('The message to send'), + player: z + .string() + .optional() + .describe( + 'Target player name for /tell. Omit to broadcast with /say.' + ), + }), + }, + async ({ message, player }) => { + try { + const cmd = player + ? `tell ${player} ${message}` + : `say ${message}`; + const response = await bedrock.sendCommand(cmd); + return { + content: [ + { + type: 'text', + text: `Message sent${player ? ` to ${player}` : ''}: "${message}"`, + }, + ], + }; + } catch (err) { + return { + content: [{ type: 'text', text: `Error: ${err.message}` }], + isError: true, + }; + } + } + ); + + // ── Tool: minecraft_build ─────────────────────────────────────────── + server.registerTool( + 'minecraft_build', + { + title: 'Minecraft Build', + description: + `Execute a batch of build commands (setblock, fill, clone) with rate limiting. Max ${bedrock.commandQueue.maxBuildCommands} commands per call. Commands should NOT have a leading slash.`, + inputSchema: z.object({ + commands: z + .array(z.string()) + .max(bedrock.commandQueue.maxBuildCommands) + .describe( + 'Array of build commands. E.g. ["setblock 10 64 10 stone", "fill 10 64 10 20 64 20 glass"]' + ), + }), + }, + async ({ commands }, { sendNotification }) => { + // Validate: only allow build commands + const allowed = ['setblock', 'fill', 'clone', 'structure']; + const invalid = commands.filter((cmd) => { + const base = cmd.replace(/^\//, '').split(' ')[0].toLowerCase(); + return !allowed.includes(base); + }); + + if (invalid.length > 0) { + return { + content: [ + { + type: 'text', + text: `Error: Only setblock/fill/clone/structure commands allowed. Invalid: ${invalid.slice(0, 3).join(', ')}`, + }, + ], + isError: true, + }; + } + + try { + const prepared = commands.map((line) => createCommandMessage(line)); + + const progressFn = (progress) => { + try { + sendNotification({ + method: 'notifications/message', + params: { + level: 'info', + logger: 'minecraft-build', + data: `Building: ${progress.percent}% (${progress.completed}/${progress.total})`, + }, + }); + } catch { + // Notification failures are non-critical + } + }; + + const result = await bedrock.commandQueue.enqueueBatchWithProgress(prepared, progressFn); + + const status = result.cancelled ? 'cancelled' : 'complete'; + return { + content: [ + { + type: 'text', + text: `Build ${status}: ${result.succeeded}/${result.total} commands succeeded, ${result.failed} failed${result.cancelled ? ' (cancelled)' : ''}`, + }, + ], + }; + } catch (err) { + return { + content: [{ type: 'text', text: `Error: ${err.message}` }], + isError: true, + }; + } + } + ); + + // ── Tool: minecraft_get_events ────────────────────────────────────── + server.registerTool( + 'minecraft_get_events', + { + title: 'Minecraft Get Events', + description: + 'Get recent game events (chat messages, block changes, etc.) from the in-memory event buffer.', + inputSchema: z.object({ + count: z + .number() + .int() + .min(1) + .max(100) + .optional() + .describe('Number of events to return (default 20, max 100)'), + type: z + .string() + .optional() + .describe( + 'Filter by event type. E.g. "PlayerMessage", "BlockChanged"' + ), + types: z + .array(z.string()) + .optional() + .describe('Filter by multiple event types'), + since: z + .string() + .optional() + .describe('ISO timestamp to get events after'), + }), + }, + async ({ count, type, types, since }) => { + const n = count ?? 20; + let events; + + if (since) { + events = bedrock.events.getSince(since, n); + } else if (types && types.length > 0) { + events = bedrock.events.getByTypes(types, n); + } else if (type) { + events = bedrock.events.getByType(type, n); + } else { + events = bedrock.events.getRecent(n); + } + + if (events.length === 0) { + return { + content: [ + { + type: 'text', + text: 'No events recorded yet. Make sure Minecraft is connected and events are subscribed.', + }, + ], + }; + } + + const formatted = events + .map( + (e) => + `[${e.timestamp}] ${e.type}: ${JSON.stringify(e.data)}` + ) + .join('\n'); + + return { + content: [ + { + type: 'text', + text: `${events.length} event(s):\n${formatted}`, + }, + ], + }; + } + ); + + // ── Tool: minecraft_get_status ────────────────────────────────────── + server.registerTool( + 'minecraft_get_status', + { + title: 'Minecraft Status', + description: + 'Check if Minecraft is connected and get status info (player name, uptime, queue, event count).', + inputSchema: z.object({}), + }, + async () => { + const status = bedrock.getStatus(); + const lines = [ + `Connected: ${status.connected ? 'YES' : 'NO'}`, + `Encrypted: ${status.encrypted ? 'YES' : 'NO'}`, + `Player: ${status.playerName || 'unknown'}`, + `Connected since: ${status.connectedAt || 'N/A'}`, + `Subscriptions: ${status.subscriptions.join(', ') || 'none'}`, + `Events stored: ${status.eventCount}`, + `Commands sent: ${status.totalSent}`, + `Commands completed: ${status.totalCompleted}`, + `Queue size: ${status.queueSize}`, + `In-flight: ${status.inFlight}`, + `Max build commands: ${bedrock.commandQueue.maxBuildCommands}`, + ]; + + return { + content: [{ type: 'text', text: lines.join('\n') }], + }; + } + ); + + // ── Tool: minecraft_subscribe ─────────────────────────────────────── + server.registerTool( + 'minecraft_subscribe', + { + title: 'Minecraft Subscribe', + description: `Subscribe to additional Bedrock event types. Available: ${BEDROCK_EVENTS.join(', ')}`, + inputSchema: z.object({ + event: z + .string() + .describe( + `Event type to subscribe to. One of: ${BEDROCK_EVENTS.join(', ')}` + ), + }), + }, + async ({ event }) => { + if (!BEDROCK_EVENTS.includes(event)) { + return { + content: [ + { + type: 'text', + text: `Unknown event "${event}". Available: ${BEDROCK_EVENTS.join(', ')}`, + }, + ], + isError: true, + }; + } + + const ok = bedrock.subscribe(event); + if (ok) { + return { + content: [ + { type: 'text', text: `Subscribed to ${event} events` }, + ], + }; + } else { + return { + content: [ + { + type: 'text', + text: `Failed to subscribe to ${event}. Is Minecraft connected?`, + }, + ], + isError: true, + }; + } + } + ); + + // ── Tool: minecraft_get_player_position (Phase 1) ─────────────────── + server.registerTool( + 'minecraft_get_player_position', + { + title: 'Get Player Position', + description: + 'Get the player\'s current position, rotation, and dimension in the world.', + inputSchema: z.object({}), + }, + async () => { + try { + const pos = await bedrock.getPlayerPosition(); + return { + content: [ + { + type: 'text', + text: `Position: x=${pos.x}, y=${pos.y}, z=${pos.z}\nRotation: rx=${pos.rx}, ry=${pos.ry}\nDimension: ${pos.dimension} (0=Overworld, 1=Nether, 2=End)`, + }, + ], + }; + } catch (err) { + return { + content: [{ type: 'text', text: `Error: ${err.message}` }], + isError: true, + }; + } + } + ); + + // ── Tool: minecraft_test_for_block (Phase 1) ──────────────────────── + server.registerTool( + 'minecraft_test_for_block', + { + title: 'Test For Block', + description: + 'Test if a specific block exists at given coordinates. Useful for checking what block is at a location.', + inputSchema: z.object({ + x: z.number().int().describe('X coordinate'), + y: z.number().int().describe('Y coordinate'), + z: z.number().int().describe('Z coordinate'), + blockId: z + .string() + .optional() + .describe('Block ID to test for (e.g. "stone", "air"). Omit to get block at position.'), + }), + }, + async ({ x, y, z: zCoord, blockId }) => { + try { + const response = await bedrock.testForBlock(x, y, zCoord, blockId); + const statusMessage = response?.statusMessage || 'Test complete'; + const statusCode = response?.statusCode ?? -1; + return { + content: [ + { + type: 'text', + text: `[${statusCode === 0 ? 'MATCH' : 'NO MATCH'}] ${statusMessage}`, + }, + ], + }; + } catch (err) { + return { + content: [{ type: 'text', text: `Error: ${err.message}` }], + isError: true, + }; + } + } + ); + + // ── Tool: minecraft_search_blueprints (Phase 2) ───────────────────── + server.registerTool( + 'minecraft_search_blueprints', + { + title: 'Search GrabCraft Blueprints', + description: + 'Search GrabCraft.com for Minecraft building blueprints. Returns names, URLs, and block counts. Use the URL with minecraft_build_blueprint to construct the building.', + inputSchema: z.object({ + query: z.string().describe('Search query, e.g. "medieval house", "castle", "modern apartment"'), + page: z.number().int().min(1).optional().describe('Page number (default 1)'), + }), + }, + async ({ query, page }) => { + try { + const results = await searchBlueprints(query, page ?? 1); + + if (results.results.length === 0) { + return { + content: [ + { + type: 'text', + text: `No blueprints found for "${query}". Try a different search term.`, + }, + ], + }; + } + + const formatted = results.results + .map((r, i) => `${i + 1}. ${r.name}\n Blocks: ${r.blocks} | Category: ${r.category}\n URL: ${r.url}`) + .join('\n\n'); + + return { + content: [ + { + type: 'text', + text: `Found ${results.total} blueprints (page ${results.page}):\n\n${formatted}`, + }, + ], + }; + } catch (err) { + return { + content: [{ type: 'text', text: `Error searching blueprints: ${err.message}` }], + isError: true, + }; + } + } + ); + + // ── Tool: minecraft_build_blueprint (Phase 2) ─────────────────────── + server.registerTool( + 'minecraft_build_blueprint', + { + title: 'Build GrabCraft Blueprint', + description: + 'Fetch a GrabCraft blueprint and build it in Minecraft. If no coordinates given, builds at the player\'s current position. Use dryRun to preview materials and dimensions without building.', + inputSchema: z.object({ + url: z.string().describe('GrabCraft blueprint URL'), + x: z.number().int().optional().describe('Build origin X (default: player position)'), + y: z.number().int().optional().describe('Build origin Y (default: player position)'), + z: z.number().int().optional().describe('Build origin Z (default: player position)'), + dryRun: z.boolean().optional().describe('If true, returns material list and dimensions without building'), + }), + }, + async ({ url, x, y, z: zCoord, dryRun }, { sendNotification }) => { + try { + // Determine build origin + let originX = x, originY = y, originZ = zCoord; + + if (originX === undefined || originY === undefined || originZ === undefined) { + try { + const pos = await bedrock.getPlayerPosition(); + originX = originX ?? pos.x; + originY = originY ?? pos.y; + originZ = originZ ?? pos.z; + } catch { + return { + content: [ + { + type: 'text', + text: 'Error: Could not get player position. Specify x, y, z coordinates manually or ensure Minecraft is connected.', + }, + ], + isError: true, + }; + } + } + + // Fetch and parse blueprint + const blueprint = await fetchBlueprint(url); + + if (blueprint.voxels.length === 0) { + return { + content: [ + { + type: 'text', + text: `Blueprint "${blueprint.name}" has no voxel data. The page structure may have changed.`, + }, + ], + isError: true, + }; + } + + // Convert to commands + const { commands, summary } = blueprintToCommands(blueprint, originX, originY, originZ); + + if (dryRun) { + const materialLines = Object.entries(summary.materials) + .sort((a, b) => b[1] - a[1]) + .map(([name, count]) => ` ${name}: ${count}`) + .join('\n'); + + let text = `Blueprint: ${summary.name}\n`; + text += `Dimensions: ${summary.dimensions.width}W x ${summary.dimensions.height}H x ${summary.dimensions.depth}D\n`; + text += `Total blocks: ${summary.totalCommands}\n`; + text += `Build origin: ${summary.buildOrigin.x}, ${summary.buildOrigin.y}, ${summary.buildOrigin.z}\n\n`; + text += `Materials:\n${materialLines}`; + + if (summary.unmappedBlocks) { + text += `\n\nUnmapped blocks (using stone fallback):\n`; + text += Object.entries(summary.unmappedBlocks) + .map(([k, v]) => ` ${k}: ${v}`) + .join('\n'); + } + + return { content: [{ type: 'text', text }] }; + } + + // Check command limit + if (commands.length > bedrock.commandQueue.maxBuildCommands) { + return { + content: [ + { + type: 'text', + text: `Blueprint has ${commands.length} commands, exceeding the limit of ${bedrock.commandQueue.maxBuildCommands}. Use dryRun to preview, or increase MAX_BUILD_COMMANDS.`, + }, + ], + isError: true, + }; + } + + // Build it + const prepared = commands.map((line) => createCommandMessage(line)); + + const progressFn = (progress) => { + try { + sendNotification({ + method: 'notifications/message', + params: { + level: 'info', + logger: 'minecraft-blueprint', + data: `Building "${summary.name}": ${progress.percent}% (${progress.completed}/${progress.total})`, + }, + }); + } catch { + // Non-critical + } + }; + + const result = await bedrock.commandQueue.enqueueBatchWithProgress(prepared, progressFn); + + let text = `Blueprint "${summary.name}" build ${result.cancelled ? 'cancelled' : 'complete'}!\n`; + text += `Blocks placed: ${result.succeeded}/${result.total}\n`; + text += `Failed: ${result.failed}\n`; + text += `Dimensions: ${summary.dimensions.width}W x ${summary.dimensions.height}H x ${summary.dimensions.depth}D\n`; + text += `Build origin: ${summary.buildOrigin.x}, ${summary.buildOrigin.y}, ${summary.buildOrigin.z}`; + + if (summary.unmappedBlocks) { + text += `\n\nUnmapped blocks (used stone): ${Object.keys(summary.unmappedBlocks).join(', ')}`; + } + + return { content: [{ type: 'text', text }] }; + } catch (err) { + return { + content: [{ type: 'text', text: `Error building blueprint: ${err.message}` }], + isError: true, + }; + } + } + ); + + // ── Tool: minecraft_build_shape (Phase 4) ─────────────────────────── + server.registerTool( + 'minecraft_build_shape', + { + title: 'Build Geometric Shape', + description: + `Build a geometric shape in Minecraft. Available shapes: ${Object.keys(SHAPES).join(', ')}. Each shape requires specific parameters - use the shape name to see required params.`, + inputSchema: z.object({ + shape: z.enum(['sphere', 'cylinder', 'dome', 'pyramid', 'wall', 'box']) + .describe('Shape type to build'), + block: z.string().describe('Bedrock block ID, e.g. "stone", "glass", "diamond_block"'), + hollow: z.boolean().optional().describe('Make the shape hollow (default false)'), + // Point parameters - used depending on shape + x: z.number().int().optional().describe('Center/base/start X (default: player position)'), + y: z.number().int().optional().describe('Center/base/start Y (default: player position)'), + z: z.number().int().optional().describe('Center/base/start Z (default: player position)'), + // For shapes needing a second point (wall, box) + x2: z.number().int().optional().describe('End/corner2 X (wall, box)'), + y2: z.number().int().optional().describe('End/corner2 Y (box)'), + z2: z.number().int().optional().describe('End/corner2 Z (wall, box)'), + // Size parameters + radius: z.number().int().min(1).max(50).optional().describe('Radius (sphere, cylinder, dome)'), + height: z.number().int().min(1).max(100).optional().describe('Height (cylinder, wall)'), + size: z.number().int().min(1).max(50).optional().describe('Base half-width (pyramid)'), + }), + }, + async ({ shape, block, hollow, x, y, z: zCoord, x2, y2, z2, radius, height, size }, { sendNotification }) => { + try { + // Get player position as default + let px = x, py = y, pz = zCoord; + if (px === undefined || py === undefined || pz === undefined) { + try { + const pos = await bedrock.getPlayerPosition(); + px = px ?? pos.x; + py = py ?? pos.y; + pz = pz ?? pos.z; + } catch { + return { + content: [{ type: 'text', text: 'Error: Could not get player position. Specify coordinates manually.' }], + isError: true, + }; + } + } + + let commands; + const point = { x: px, y: py, z: pz }; + + switch (shape) { + case 'sphere': + if (!radius) return { content: [{ type: 'text', text: 'Error: radius is required for sphere' }], isError: true }; + commands = SHAPES.sphere.generate(point, radius, block, hollow ?? false); + break; + case 'cylinder': + if (!radius || !height) return { content: [{ type: 'text', text: 'Error: radius and height are required for cylinder' }], isError: true }; + commands = SHAPES.cylinder.generate(point, radius, height, block, hollow ?? false); + break; + case 'dome': + if (!radius) return { content: [{ type: 'text', text: 'Error: radius is required for dome' }], isError: true }; + commands = SHAPES.dome.generate(point, radius, block); + break; + case 'pyramid': + if (!size) return { content: [{ type: 'text', text: 'Error: size is required for pyramid' }], isError: true }; + commands = SHAPES.pyramid.generate(point, size, block, hollow ?? false); + break; + case 'wall': + if (x2 === undefined || z2 === undefined || !height) return { content: [{ type: 'text', text: 'Error: x2, z2, and height are required for wall' }], isError: true }; + commands = SHAPES.wall.generate(point, { x: x2, y: py, z: z2 }, height, block); + break; + case 'box': + if (x2 === undefined || y2 === undefined || z2 === undefined) return { content: [{ type: 'text', text: 'Error: x2, y2, z2 are required for box' }], isError: true }; + commands = SHAPES.box.generate(point, { x: x2, y: y2, z: z2 }, block, hollow ?? false); + break; + default: + return { content: [{ type: 'text', text: `Unknown shape: ${shape}` }], isError: true }; + } + + if (commands.length > bedrock.commandQueue.maxBuildCommands) { + return { + content: [{ type: 'text', text: `Shape would require ${commands.length} commands, exceeding limit of ${bedrock.commandQueue.maxBuildCommands}. Try a smaller size.` }], + isError: true, + }; + } + + // Execute + const prepared = commands.map((line) => createCommandMessage(line)); + + const progressFn = (progress) => { + try { + sendNotification({ + method: 'notifications/message', + params: { + level: 'info', + logger: 'minecraft-shape', + data: `Building ${shape}: ${progress.percent}% (${progress.completed}/${progress.total})`, + }, + }); + } catch { /* non-critical */ } + }; + + const result = await bedrock.commandQueue.enqueueBatchWithProgress(prepared, progressFn); + + return { + content: [ + { + type: 'text', + text: `${shape} build ${result.cancelled ? 'cancelled' : 'complete'}: ${result.succeeded}/${result.total} commands succeeded, ${result.failed} failed`, + }, + ], + }; + } catch (err) { + return { + content: [{ type: 'text', text: `Error: ${err.message}` }], + isError: true, + }; + } + } + ); + + // ── Tool: minecraft_cancel_build ───────────────────────────────────── + server.registerTool( + 'minecraft_cancel_build', + { + title: 'Cancel Build', + description: 'Cancel an in-progress build operation (blueprint or shape build).', + inputSchema: z.object({}), + }, + async () => { + bedrock.commandQueue.cancelBuild(); + return { + content: [{ type: 'text', text: 'Build cancellation requested. The current batch will finish before stopping.' }], + }; + } + ); + + // ── MCP Resources (Phase 6) ───────────────────────────────────────── + + // Resource: Block ID reference + server.resource( + 'blocks', + 'minecraft://blocks', + { + description: 'Block ID mapping reference (Java Edition ID -> Bedrock Edition ID)', + mimeType: 'application/json', + }, + async () => { + const blocks = getAllBlocks(); + return { + contents: [ + { + uri: 'minecraft://blocks', + mimeType: 'application/json', + text: JSON.stringify(blocks, null, 2), + }, + ], + }; + } + ); + + // Resource: GrabCraft categories + server.resource( + 'grabcraft-categories', + 'grabcraft://categories', + { + description: 'Available GrabCraft blueprint categories for searching', + mimeType: 'application/json', + }, + async () => { + const categories = getCategories(); + return { + contents: [ + { + uri: 'grabcraft://categories', + mimeType: 'application/json', + text: JSON.stringify(categories, null, 2), + }, + ], + }; + } + ); + + return server; + } + + // ── SSE Transport endpoint ────────────────────────────────────────── + // SSE is the most widely supported MCP transport for Claude Code + app.get('/sse', async (req, res) => { + log(TAG, 'SSE client connected'); + const transport = new SSEServerTransport('/messages', res); + const sessionId = transport.sessionId; + transports[sessionId] = transport; + + transport.onclose = () => { + delete transports[sessionId]; + log(TAG, `SSE session closed: ${sessionId}`); + }; + + const server = createServer(); + await server.connect(transport); + log(TAG, `SSE session started: ${sessionId}`); + }); + + app.post('/messages', async (req, res) => { + const sessionId = req.query.sessionId; + const transport = transports[sessionId]; + if (transport) { + await transport.handlePostMessage(req, res, req.body); + } else { + res.status(400).send('Invalid or missing session'); + } + }); + + // ── Streamable HTTP Transport (modern alternative) ───────────────── + app.post('/mcp', async (req, res) => { + const sessionId = req.headers['mcp-session-id']; + + if (sessionId && transports[sessionId]) { + await transports[sessionId].handleRequest(req, res, req.body); + return; + } + + // New session + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => randomUUID(), + onsessioninitialized: (id) => { + transports[id] = transport; + log(TAG, `Streamable HTTP session created: ${id}`); + }, + }); + + transport.onclose = () => { + if (transport.sessionId) { + delete transports[transport.sessionId]; + log(TAG, `Streamable HTTP session closed: ${transport.sessionId}`); + } + }; + + const server = createServer(); + await server.connect(transport); + await transport.handleRequest(req, res, req.body); + }); + + app.get('/mcp', async (req, res) => { + const sessionId = req.headers['mcp-session-id']; + if (sessionId && transports[sessionId]) { + await transports[sessionId].handleRequest(req, res); + } else { + res.status(400).send('Invalid or missing session'); + } + }); + + app.delete('/mcp', async (req, res) => { + const sessionId = req.headers['mcp-session-id']; + if (sessionId && transports[sessionId]) { + await transports[sessionId].handleRequest(req, res); + } else { + res.status(400).send('Invalid or missing session'); + } + }); + + // Health check endpoint + app.get('/health', (_req, res) => { + res.json({ + status: 'ok', + minecraft: bedrock.isConnected(), + uptime: process.uptime(), + }); + }); + + app.listen(port, '0.0.0.0', () => { + log(TAG, `MCP SSE endpoint : http://0.0.0.0:${port}/sse`); + log(TAG, `MCP HTTP endpoint : http://0.0.0.0:${port}/mcp`); + }); +} diff --git a/src/utils.js b/src/utils.js index 93f6581..38c4d69 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,80 +1,108 @@ -import { randomUUID } from 'node:crypto'; - -/** - * Create a Bedrock WebSocket subscribe message envelope. - * @param {string} eventName - e.g. "PlayerMessage", "BlockChanged" - * @returns {string} JSON string ready to send over WS - */ -export function createSubscribeMessage(eventName) { - return JSON.stringify({ - header: { - version: 1, - requestId: randomUUID(), - messageType: 'commandRequest', - messagePurpose: 'subscribe', - }, - body: { - eventName, - }, - }); -} - -/** - * Create a Bedrock WebSocket command message envelope. - * @param {string} commandLine - e.g. "give @p diamond 64" (no leading slash) - * @returns {{ id: string, message: string }} request ID and JSON string - */ -export function createCommandMessage(commandLine) { - // Strip leading slash if present - const cmd = commandLine.startsWith('/') ? commandLine.slice(1) : commandLine; - const requestId = randomUUID(); - - const message = JSON.stringify({ - header: { - version: 1, - requestId, - messageType: 'commandRequest', - messagePurpose: 'commandRequest', - }, - body: { - version: 1, - commandLine: cmd, - origin: { - type: 'player', - }, - }, - }); - - return { id: requestId, message }; -} - -/** - * Strip Minecraft formatting codes (section sign + character). - * @param {string} text - * @returns {string} - */ -export function sanitize(text) { - if (!text) return ''; - // Remove section sign formatting codes like §a, §l, §r etc. - return text.replace(/\u00A7[0-9a-fk-or]/gi, ''); -} - -/** - * Timestamped log helper. - * @param {string} tag - Module tag - * @param {...any} args - Log arguments - */ -export function log(tag, ...args) { - const ts = new Date().toISOString(); - console.log(`[${ts}] [${tag}]`, ...args); -} - -/** - * Timestamped error log helper. - * @param {string} tag - Module tag - * @param {...any} args - Log arguments - */ -export function logError(tag, ...args) { - const ts = new Date().toISOString(); - console.error(`[${ts}] [${tag}]`, ...args); -} +import { randomUUID } from 'node:crypto'; + +/** + * Create a Bedrock WebSocket subscribe message envelope. + * @param {string} eventName - e.g. "PlayerMessage", "BlockChanged" + * @returns {string} JSON string ready to send over WS + */ +export function createSubscribeMessage(eventName) { + return JSON.stringify({ + header: { + version: 1, + requestId: randomUUID(), + messageType: 'commandRequest', + messagePurpose: 'subscribe', + }, + body: { + eventName, + }, + }); +} + +/** + * Create a Bedrock WebSocket command message envelope. + * @param {string} commandLine - e.g. "give @p diamond 64" (no leading slash) + * @returns {{ id: string, message: string }} request ID and JSON string + */ +export function createCommandMessage(commandLine) { + // Strip leading slash if present + const cmd = commandLine.startsWith('/') ? commandLine.slice(1) : commandLine; + const requestId = randomUUID(); + + const message = JSON.stringify({ + header: { + version: 1, + requestId, + messageType: 'commandRequest', + messagePurpose: 'commandRequest', + }, + body: { + version: 1, + commandLine: cmd, + origin: { + type: 'player', + }, + }, + }); + + return { id: requestId, message }; +} + +/** + * Create a Bedrock WebSocket enableencryption command message. + * @param {string} publicKeyBase64 - Server's base64-encoded public key + * @param {string} saltBase64 - Base64-encoded 16-byte salt + * @returns {{ id: string, message: string }} request ID and JSON string + */ +export function createEnableEncryptionMessage(publicKeyBase64, saltBase64) { + const requestId = randomUUID(); + + const message = JSON.stringify({ + header: { + version: 1, + requestId, + messageType: 'commandRequest', + messagePurpose: 'commandRequest', + }, + body: { + version: 1, + commandLine: `enableencryption "${publicKeyBase64}" "${saltBase64}"`, + origin: { + type: 'player', + }, + }, + }); + + return { id: requestId, message }; +} + +/** + * Strip Minecraft formatting codes (section sign + character). + * @param {string} text + * @returns {string} + */ +export function sanitize(text) { + if (!text) return ''; + // Remove section sign formatting codes like §a, §l, §r etc. + return text.replace(/\u00A7[0-9a-fk-or]/gi, ''); +} + +/** + * Timestamped log helper. + * @param {string} tag - Module tag + * @param {...any} args - Log arguments + */ +export function log(tag, ...args) { + const ts = new Date().toISOString(); + console.log(`[${ts}] [${tag}]`, ...args); +} + +/** + * Timestamped error log helper. + * @param {string} tag - Module tag + * @param {...any} args - Log arguments + */ +export function logError(tag, ...args) { + const ts = new Date().toISOString(); + console.error(`[${ts}] [${tag}]`, ...args); +}