diff --git a/.github/workflows/commit.yml b/.github/workflows/commit.yml index dc1cc79..3148794 100644 --- a/.github/workflows/commit.yml +++ b/.github/workflows/commit.yml @@ -20,5 +20,7 @@ jobs: run: pnpm install --frozen-lockfile - name: Format run: pnpm format + - name: Test + run: pnpm test - name: Build run: pnpm build diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index d6f5f26..053534b 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -21,5 +21,6 @@ jobs: registry-url: 'https://registry.npmjs.org' - run: pnpm install --frozen-lockfile - run: pnpm format + - run: pnpm test - run: pnpm build - run: pnpm publish --provenance --access public --no-git-checks diff --git a/README.md b/README.md index 0430184..947aa8a 100644 --- a/README.md +++ b/README.md @@ -34,10 +34,29 @@ bitrefill search-products --query "Netflix" cp .env.example .env ``` +Node does not load `.env` files automatically. After editing `.env`, either export variables in your shell (`set -a && source .env && set +a` in bash/zsh) or pass `--api-key` on the command line. + ## Usage ```bash -bitrefill [--api-key ] [options] +bitrefill [--api-key ] [--json] [options] +``` + +### Human-readable output (default) + +Tool results are pretty-printed JSON on stdout. Status messages (OAuth prompts, etc.) also go to stdout. + +### Machine-readable output (`--json`) + +Pass `--json` anywhere before the subcommand so scripts and `jq` get a single JSON value per invocation on stdout: + +- **stdout**: Only the tool result (JSON). Text payloads from the server may be JSON or [TOON](https://toonformat.dev/); the CLI decodes TOON to JSON when needed. +- **stderr**: Progress messages, errors, and client errors (JSON `{ "error": "..." }` for failures). + +Example: + +```bash +bitrefill --json search-products --query "Amazon" --per_page 1 | jq '.products[0].name' ``` ### Examples @@ -62,6 +81,18 @@ bitrefill --help bitrefill logout ``` +## Development + +From the repository root (requires [pnpm](https://pnpm.io/)): + +```bash +pnpm install +pnpm format # Prettier check +pnpm test # Vitest unit tests +pnpm build # Compile to dist/ +pnpm dev -- --help # Run CLI via tsx without building +``` + ## Paying **Flow:** `get-product-details` → pick `product_id` + `package_id` → `buy-products` with `--cart_items` and `--payment_method`. diff --git a/package.json b/package.json index f1d3a53..0edc479 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "build": "tsc", "start": "node dist/index.js", "dev": "tsx src/index.ts", - "format": "prettier --check ./src" + "format": "prettier --check ./src", + "test": "vitest run" }, "repository": { "type": "git", @@ -22,13 +23,15 @@ "packageManager": "pnpm@10.27.0", "dependencies": { "@modelcontextprotocol/sdk": "^1.27.1", + "@toon-format/toon": "^2.1.0", "commander": "^14.0.3" }, "devDependencies": { "@types/node": "^25.3.0", "prettier": "^3.8.1", "tsx": "^4.21.0", - "typescript": "^5.8.3" + "typescript": "^5.8.3", + "vitest": "^3.2.4" }, "keywords": [ "mcp", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 01d7eb4..f7b6f8d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@modelcontextprotocol/sdk': specifier: ^1.27.1 version: 1.27.1(zod@4.3.6) + '@toon-format/toon': + specifier: ^2.1.0 + version: 2.1.0 commander: specifier: ^14.0.3 version: 14.0.3 @@ -27,6 +30,9 @@ importers: typescript: specifier: ^5.8.3 version: 5.9.3 + vitest: + specifier: ^3.2.4 + version: 3.2.4(@types/node@25.3.0)(tsx@4.21.0) packages: @@ -192,6 +198,9 @@ packages: peerDependencies: hono: ^4 + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@modelcontextprotocol/sdk@1.27.1': resolution: {integrity: sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==} engines: {node: '>=18'} @@ -202,9 +211,175 @@ packages: '@cfworker/json-schema': optional: true + '@rollup/rollup-android-arm-eabi@4.60.1': + resolution: {integrity: sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.60.1': + resolution: {integrity: sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.60.1': + resolution: {integrity: sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.60.1': + resolution: {integrity: sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.60.1': + resolution: {integrity: sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.60.1': + resolution: {integrity: sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.60.1': + resolution: {integrity: sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.60.1': + resolution: {integrity: sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.60.1': + resolution: {integrity: sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.60.1': + resolution: {integrity: sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.60.1': + resolution: {integrity: sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.60.1': + resolution: {integrity: sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.60.1': + resolution: {integrity: sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.60.1': + resolution: {integrity: sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.60.1': + resolution: {integrity: sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.60.1': + resolution: {integrity: sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.60.1': + resolution: {integrity: sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.60.1': + resolution: {integrity: sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.60.1': + resolution: {integrity: sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.60.1': + resolution: {integrity: sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.60.1': + resolution: {integrity: sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.60.1': + resolution: {integrity: sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.60.1': + resolution: {integrity: sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.60.1': + resolution: {integrity: sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.60.1': + resolution: {integrity: sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==} + cpu: [x64] + os: [win32] + + '@toon-format/toon@2.1.0': + resolution: {integrity: sha512-JwWptdF5eOA0HaQxbKAzkpQtR4wSWTEfDlEy/y3/4okmOAX1qwnpLZMmtEWr+ncAhTTY1raCKH0kteHhSXnQqg==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/node@25.3.0': resolution: {integrity: sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==} + '@vitest/expect@3.2.4': + resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + + '@vitest/mocker@3.2.4': + resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + + '@vitest/runner@3.2.4': + resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} + + '@vitest/snapshot@3.2.4': + resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} + + '@vitest/spy@3.2.4': + resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + + '@vitest/utils@3.2.4': + resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} @@ -220,6 +395,10 @@ packages: ajv@8.18.0: resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + body-parser@2.2.2: resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} @@ -228,6 +407,10 @@ packages: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -236,6 +419,14 @@ packages: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + commander@14.0.3: resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} engines: {node: '>=20'} @@ -273,6 +464,10 @@ packages: supports-color: optional: true + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -296,6 +491,9 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -308,6 +506,9 @@ packages: escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + etag@1.8.1: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} @@ -320,6 +521,10 @@ packages: resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} engines: {node: '>=18.0.0'} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + express-rate-limit@8.2.1: resolution: {integrity: sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==} engines: {node: '>= 16'} @@ -336,6 +541,15 @@ packages: fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + finalhandler@2.1.1: resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} engines: {node: '>= 18.0.0'} @@ -411,12 +625,21 @@ packages: jose@6.1.3: resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} json-schema-typed@8.0.2: resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -440,6 +663,11 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + negotiator@1.0.0: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} @@ -470,10 +698,28 @@ packages: path-to-regexp@8.3.0: resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + pkce-challenge@5.0.1: resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} engines: {node: '>=16.20.0'} + postcss@8.5.8: + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + engines: {node: ^10 || ^12 || >=14} + prettier@3.8.1: resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} engines: {node: '>=14'} @@ -502,6 +748,11 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + rollup@4.60.1: + resolution: {integrity: sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + router@2.2.0: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} @@ -544,10 +795,48 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.4: + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + engines: {node: '>=14.0.0'} + toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} @@ -577,11 +866,89 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite@7.3.1: + resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@3.2.4: + resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.4 + '@vitest/ui': 3.2.4 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -677,6 +1044,8 @@ snapshots: dependencies: hono: 4.12.2 + '@jridgewell/sourcemap-codec@1.5.5': {} + '@modelcontextprotocol/sdk@1.27.1(zod@4.3.6)': dependencies: '@hono/node-server': 1.19.9(hono@4.12.2) @@ -699,10 +1068,138 @@ snapshots: transitivePeerDependencies: - supports-color + '@rollup/rollup-android-arm-eabi@4.60.1': + optional: true + + '@rollup/rollup-android-arm64@4.60.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.60.1': + optional: true + + '@rollup/rollup-darwin-x64@4.60.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.60.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.60.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.60.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.60.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.60.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.60.1': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.60.1': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.60.1': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.60.1': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.60.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.60.1': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.60.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.60.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.60.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.60.1': + optional: true + + '@rollup/rollup-openbsd-x64@4.60.1': + optional: true + + '@rollup/rollup-openharmony-arm64@4.60.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.60.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.60.1': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.60.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.60.1': + optional: true + + '@toon-format/toon@2.1.0': {} + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.8': {} + '@types/node@25.3.0': dependencies: undici-types: 7.18.2 + '@vitest/expect@3.2.4': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.4(vite@7.3.1(@types/node@25.3.0)(tsx@4.21.0))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.1(@types/node@25.3.0)(tsx@4.21.0) + + '@vitest/pretty-format@3.2.4': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.4': + dependencies: + '@vitest/utils': 3.2.4 + pathe: 2.0.3 + strip-literal: 3.1.0 + + '@vitest/snapshot@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@3.2.4': + dependencies: + tinyspy: 4.0.4 + + '@vitest/utils@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + accepts@2.0.0: dependencies: mime-types: 3.0.2 @@ -719,6 +1216,8 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + assertion-error@2.0.1: {} + body-parser@2.2.2: dependencies: bytes: 3.1.2 @@ -735,6 +1234,8 @@ snapshots: bytes@3.1.2: {} + cac@6.7.14: {} + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -745,6 +1246,16 @@ snapshots: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + check-error@2.1.3: {} + commander@14.0.3: {} content-disposition@1.0.1: {} @@ -770,6 +1281,8 @@ snapshots: dependencies: ms: 2.1.3 + deep-eql@5.0.2: {} + depd@2.0.0: {} dunder-proto@1.0.1: @@ -786,6 +1299,8 @@ snapshots: es-errors@1.3.0: {} + es-module-lexer@1.7.0: {} + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -821,6 +1336,10 @@ snapshots: escape-html@1.0.3: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + etag@1.8.1: {} eventsource-parser@3.0.6: {} @@ -829,6 +1348,8 @@ snapshots: dependencies: eventsource-parser: 3.0.6 + expect-type@1.3.0: {} + express-rate-limit@8.2.1(express@5.2.1): dependencies: express: 5.2.1 @@ -871,6 +1392,10 @@ snapshots: fast-uri@3.1.0: {} + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + finalhandler@2.1.1: dependencies: debug: 4.4.3 @@ -947,10 +1472,18 @@ snapshots: jose@6.1.3: {} + js-tokens@9.0.1: {} + json-schema-traverse@1.0.0: {} json-schema-typed@8.0.2: {} + loupe@3.2.1: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + math-intrinsics@1.1.0: {} media-typer@1.1.0: {} @@ -965,6 +1498,8 @@ snapshots: ms@2.1.3: {} + nanoid@3.3.11: {} + negotiator@1.0.0: {} object-assign@4.1.1: {} @@ -985,8 +1520,22 @@ snapshots: path-to-regexp@8.3.0: {} + pathe@2.0.3: {} + + pathval@2.0.1: {} + + picocolors@1.1.1: {} + + picomatch@4.0.4: {} + pkce-challenge@5.0.1: {} + postcss@8.5.8: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + prettier@3.8.1: {} proxy-addr@2.0.7: @@ -1011,6 +1560,37 @@ snapshots: resolve-pkg-maps@1.0.0: {} + rollup@4.60.1: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.60.1 + '@rollup/rollup-android-arm64': 4.60.1 + '@rollup/rollup-darwin-arm64': 4.60.1 + '@rollup/rollup-darwin-x64': 4.60.1 + '@rollup/rollup-freebsd-arm64': 4.60.1 + '@rollup/rollup-freebsd-x64': 4.60.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.1 + '@rollup/rollup-linux-arm-musleabihf': 4.60.1 + '@rollup/rollup-linux-arm64-gnu': 4.60.1 + '@rollup/rollup-linux-arm64-musl': 4.60.1 + '@rollup/rollup-linux-loong64-gnu': 4.60.1 + '@rollup/rollup-linux-loong64-musl': 4.60.1 + '@rollup/rollup-linux-ppc64-gnu': 4.60.1 + '@rollup/rollup-linux-ppc64-musl': 4.60.1 + '@rollup/rollup-linux-riscv64-gnu': 4.60.1 + '@rollup/rollup-linux-riscv64-musl': 4.60.1 + '@rollup/rollup-linux-s390x-gnu': 4.60.1 + '@rollup/rollup-linux-x64-gnu': 4.60.1 + '@rollup/rollup-linux-x64-musl': 4.60.1 + '@rollup/rollup-openbsd-x64': 4.60.1 + '@rollup/rollup-openharmony-arm64': 4.60.1 + '@rollup/rollup-win32-arm64-msvc': 4.60.1 + '@rollup/rollup-win32-ia32-msvc': 4.60.1 + '@rollup/rollup-win32-x64-gnu': 4.60.1 + '@rollup/rollup-win32-x64-msvc': 4.60.1 + fsevents: 2.3.3 + router@2.2.0: dependencies: debug: 4.4.3 @@ -1084,8 +1664,35 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + statuses@2.0.2: {} + std-env@3.10.0: {} + + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.4: {} + toidentifier@1.0.1: {} tsx@4.21.0: @@ -1109,10 +1716,90 @@ snapshots: vary@1.1.2: {} + vite-node@3.2.4(@types/node@25.3.0)(tsx@4.21.0): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.3.1(@types/node@25.3.0)(tsx@4.21.0) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite@7.3.1(@types/node@25.3.0)(tsx@4.21.0): + dependencies: + esbuild: 0.27.3 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.8 + rollup: 4.60.1 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 25.3.0 + fsevents: 2.3.3 + tsx: 4.21.0 + + vitest@3.2.4(@types/node@25.3.0)(tsx@4.21.0): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@25.3.0)(tsx@4.21.0)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.3.0 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.3.1(@types/node@25.3.0)(tsx@4.21.0) + vite-node: 3.2.4(@types/node@25.3.0)(tsx@4.21.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 25.3.0 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + which@2.0.2: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + wrappy@1.0.2: {} zod-to-json-schema@3.25.1(zod@4.3.6): diff --git a/src/index.ts b/src/index.ts index 4d45885..3ae4df1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,7 +8,6 @@ import { ListToolsResultSchema, CallToolResultSchema, } from '@modelcontextprotocol/sdk/types.js'; -import type { Tool } from '@modelcontextprotocol/sdk/types.js'; import type { OAuthClientProvider, OAuthDiscoveryState, @@ -23,6 +22,12 @@ import fs from 'node:fs'; import path from 'node:path'; import os from 'node:os'; import { execSync } from 'node:child_process'; +import { + createHumanFormatter, + createJsonFormatter, + type OutputFormatter, +} from './output.js'; +import { buildOptionsForTool, parseToolArgs } from './tools.js'; const BASE_MCP_URL = 'https://api.bitrefill.com/mcp'; const CALLBACK_PORT = 8098; @@ -43,6 +48,14 @@ function resolveMcpUrl(apiKey?: string): string { return BASE_MCP_URL; } +function resolveJsonMode(): boolean { + return process.argv.some((arg) => arg === '--json'); +} + +function createOutputFormatter(jsonMode: boolean): OutputFormatter { + return jsonMode ? createJsonFormatter() : createHumanFormatter(); +} + // --- Persistent OAuth state --- interface PersistedState { @@ -74,7 +87,10 @@ function saveState(serverUrl: string, state: PersistedState): void { // --- OAuth --- -function createOAuthProvider(serverUrl: string): OAuthClientProvider { +function createOAuthProvider( + serverUrl: string, + formatter: OutputFormatter +): OAuthClientProvider { let state = loadState(serverUrl); const persist = () => saveState(serverUrl, state); @@ -106,7 +122,9 @@ function createOAuthProvider(serverUrl: string): OAuthClientProvider { persist(); }, redirectToAuthorization(url: URL) { - console.log(`\nOpen this URL to authorize:\n ${url.toString()}\n`); + formatter.info( + `\nOpen this URL to authorize:\n ${url.toString()}\n` + ); openBrowser(url.toString()); }, saveCodeVerifier(v: string) { @@ -186,13 +204,14 @@ function waitForCallback(): Promise { async function createMcpClient( url: string, - useOAuth: boolean + useOAuth: boolean, + formatter: OutputFormatter ): Promise<{ client: Client; transport: StreamableHTTPClientTransport }> { const suppressNoise = (err: Error) => { if (err instanceof UnauthorizedError) return; if (err.message?.includes('SSE stream disconnected')) return; if (err.message?.includes('Failed to open SSE stream')) return; - console.error('Client error:', err); + formatter.clientError(err); }; if (!useOAuth) { @@ -203,7 +222,7 @@ async function createMcpClient( return { client: c, transport: t }; } - const authProvider = createOAuthProvider(url); + const authProvider = createOAuthProvider(url, formatter); const tryConnect = async () => { const c = new Client({ name: 'bitrefill-cli', version: '0.1.1' }); @@ -220,9 +239,9 @@ async function createMcpClient( } catch (err) { if (!(err instanceof UnauthorizedError)) throw err; - console.log('Authorization required...'); + formatter.info('Authorization required...'); const code = await waitForCallback(); - console.log('Authorization code received.'); + formatter.info('Authorization code received.'); const c = new Client({ name: 'bitrefill-cli', version: '0.1.1' }); c.onerror = suppressNoise; @@ -235,112 +254,20 @@ async function createMcpClient( } } -// --- Tool execution --- - -function printResult(result: { - content: Array<{ type: string; text?: string; [key: string]: unknown }>; -}): void { - for (const item of result.content) { - if (item.type === 'text' && item.text) { - try { - console.log(JSON.stringify(JSON.parse(item.text), null, 2)); - } catch { - console.log(item.text); - } - } else { - console.log(`[${item.type}]`, item); - } - } -} - -interface JsonSchemaProperty { - type?: string; - description?: string; - default?: unknown; - enum?: unknown[]; -} - -function coerceValue(raw: string, prop: JsonSchemaProperty): unknown { - if (prop.enum) { - if (!prop.enum.includes(raw)) - throw new Error(`Must be one of: ${prop.enum.join(', ')}`); - return raw; - } - switch (prop.type) { - case 'number': - case 'integer': { - const n = Number(raw); - if (Number.isNaN(n)) throw new Error('Must be a number'); - return n; - } - case 'boolean': - if (['true', '1', 'yes'].includes(raw)) return true; - if (['false', '0', 'no'].includes(raw)) return false; - throw new Error('Must be true/false'); - case 'object': - case 'array': - return JSON.parse(raw); - default: - return raw; - } -} - -function buildOptionsForTool(cmd: Command, tool: Tool): void { - const schema = tool.inputSchema as { - properties?: Record; - required?: string[]; - }; - - if (!schema.properties) return; - - const required = new Set(schema.required ?? []); - - for (const [name, prop] of Object.entries(schema.properties)) { - const flag = `--${name} `; - let desc = prop.description ?? ''; - if (prop.enum) desc += ` (${prop.enum.join(', ')})`; - - if (prop.default !== undefined) { - cmd.option(flag, desc, String(prop.default)); - } else if (required.has(name)) { - cmd.requiredOption(flag, desc); - } else { - cmd.option(flag, desc); - } - } -} - -function parseToolArgs( - opts: Record, - tool: Tool -): Record { - const schema = tool.inputSchema as { - properties?: Record; - }; - if (!schema.properties) return {}; - - const args: Record = {}; - for (const [name, prop] of Object.entries(schema.properties)) { - const raw = opts[optionKey(name)]; - if (raw === undefined) continue; - args[name] = coerceValue(raw, prop); - } - return args; -} - -function optionKey(s: string): string { - return s.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase()); -} - // --- Main --- async function main(): Promise { const apiKey = resolveApiKey(); + const formatter = createOutputFormatter(resolveJsonMode()); const mcpUrl = resolveMcpUrl(apiKey); const useOAuth = !apiKey && !process.env.MCP_URL; // Phase 1: connect and discover tools - const { client, transport } = await createMcpClient(mcpUrl, useOAuth); + const { client, transport } = await createMcpClient( + mcpUrl, + useOAuth, + formatter + ); const toolsResult = await client.request( { method: 'tools/list', params: {} }, @@ -358,6 +285,10 @@ async function main(): Promise { .option( '--api-key ', 'Bitrefill API key (overrides BITREFILL_API_KEY env var)' + ) + .option( + '--json', + 'Output raw JSON (TOON decoded); use with jq. Non-result messages go to stderr.' ); program @@ -365,16 +296,16 @@ async function main(): Promise { .description('Clear stored OAuth credentials') .action(() => { if (!useOAuth) { - console.log( + formatter.info( 'Using API key authentication — no stored credentials to clear.' ); return; } try { fs.unlinkSync(stateFilePath(mcpUrl)); - console.log('Cleared stored credentials.'); + formatter.info('Cleared stored credentials.'); } catch { - console.log('No stored credentials to clear.'); + formatter.info('No stored credentials to clear.'); } }); @@ -395,7 +326,7 @@ async function main(): Promise { }, CallToolResultSchema ); - printResult(result); + formatter.result(result.content ?? []); }); } @@ -408,6 +339,7 @@ async function main(): Promise { } main().catch((err) => { - console.error('Error:', err instanceof Error ? err.message : err); + const formatter = createOutputFormatter(resolveJsonMode()); + formatter.error(err); process.exit(1); }); diff --git a/src/output.test.ts b/src/output.test.ts new file mode 100644 index 0000000..99f3baf --- /dev/null +++ b/src/output.test.ts @@ -0,0 +1,108 @@ +import { describe, it, expect, vi, afterEach } from 'vitest'; +import { + parseToonToJson, + createHumanFormatter, + createJsonFormatter, +} from './output.js'; + +describe('parseToonToJson', () => { + it('returns plain text when neither JSON nor TOON', () => { + expect(parseToonToJson('not json')).toBe('not json'); + }); +}); + +describe('createHumanFormatter', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('writes info to stdout', () => { + const log = vi.spyOn(console, 'log').mockImplementation(() => {}); + const fmt = createHumanFormatter(); + fmt.info('hello'); + expect(log).toHaveBeenCalledWith('hello'); + }); + + it('writes top-level errors to stderr', () => { + const err = vi.spyOn(console, 'error').mockImplementation(() => {}); + const fmt = createHumanFormatter(); + fmt.error(new Error('boom')); + expect(err).toHaveBeenCalledWith('Error:', 'boom'); + }); + + it('writes client errors to stderr with prefix', () => { + const err = vi.spyOn(console, 'error').mockImplementation(() => {}); + const fmt = createHumanFormatter(); + const e = new Error('sse'); + fmt.clientError(e); + expect(err).toHaveBeenCalledWith('Client error:', e); + }); + + it('pretty-prints text tool content', () => { + const log = vi.spyOn(console, 'log').mockImplementation(() => {}); + const fmt = createHumanFormatter(); + fmt.result([{ type: 'text', text: '{"a":2}' }]); + expect(log).toHaveBeenCalledWith(JSON.stringify({ a: 2 }, null, 2)); + }); + + it('logs non-text content with type label', () => { + const log = vi.spyOn(console, 'log').mockImplementation(() => {}); + const fmt = createHumanFormatter(); + fmt.result([{ type: 'image', url: 'x' }]); + expect(log).toHaveBeenCalledWith('[image]', { + type: 'image', + url: 'x', + }); + }); +}); + +describe('createJsonFormatter', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('writes info to stderr', () => { + const err = vi.spyOn(console, 'error').mockImplementation(() => {}); + const fmt = createJsonFormatter(); + fmt.info('status'); + expect(err).toHaveBeenCalledWith('status'); + }); + + it('writes errors as JSON to stderr', () => { + const err = vi.spyOn(console, 'error').mockImplementation(() => {}); + const fmt = createJsonFormatter(); + fmt.error(new Error('bad')); + expect(err).toHaveBeenCalledWith(JSON.stringify({ error: 'bad' })); + }); + + it('writes client errors as JSON to stderr', () => { + const err = vi.spyOn(console, 'error').mockImplementation(() => {}); + const fmt = createJsonFormatter(); + fmt.clientError(new Error('net')); + expect(err).toHaveBeenCalledWith(JSON.stringify({ error: 'net' })); + }); + + it('writes compact JSON result to stdout', () => { + const log = vi.spyOn(console, 'log').mockImplementation(() => {}); + const fmt = createJsonFormatter(); + fmt.result([{ type: 'text', text: '{"k":1}' }]); + expect(log).toHaveBeenCalledWith(JSON.stringify({ k: 1 })); + }); + + it('outputs null for empty content', () => { + const log = vi.spyOn(console, 'log').mockImplementation(() => {}); + const fmt = createJsonFormatter(); + fmt.result([]); + expect(log).toHaveBeenCalledWith('null'); + }); + + it('wraps multiple values in an array', () => { + const log = vi.spyOn(console, 'log').mockImplementation(() => {}); + const fmt = createJsonFormatter(); + fmt.result([ + { type: 'text', text: '1' }, + { type: 'text', text: '2' }, + ]); + expect(log).toHaveBeenCalledWith(JSON.stringify([1, 2])); + }); +}); diff --git a/src/output.ts b/src/output.ts new file mode 100644 index 0000000..c5f19c3 --- /dev/null +++ b/src/output.ts @@ -0,0 +1,97 @@ +import { decode as decodeToon } from '@toon-format/toon'; + +export type ToolResultContentItem = { + type: string; + text?: string; + [key: string]: unknown; +}; + +export interface OutputFormatter { + /** Status / info: stdout in human mode, stderr in JSON mode so stdout stays pure JSON. */ + info(message: string): void; + /** Top-level fatal error (e.g. main catch). */ + error(error: unknown): void; + /** MCP transport client error (suppressed noise still logs here). */ + clientError(err: Error): void; + /** Tool call result payload. */ + result(content: ToolResultContentItem[]): void; +} + +export function parseToonToJson(text: string): unknown { + try { + return JSON.parse(text); + } catch { + try { + return decodeToon(text); + } catch { + return text; + } + } +} + +function formatResultPayload(content: ToolResultContentItem[]): { + values: unknown[]; +} { + const values: unknown[] = []; + for (const item of content) { + if (item.type === 'text' && item.text !== undefined) { + values.push(parseToonToJson(item.text)); + } else { + values.push({ ...item }); + } + } + return { values }; +} + +export function createHumanFormatter(): OutputFormatter { + return { + info(message: string): void { + console.log(message); + }, + error(error: unknown): void { + const message = + error instanceof Error ? error.message : String(error); + console.error('Error:', message); + }, + clientError(err: Error): void { + console.error('Client error:', err); + }, + result(content: ToolResultContentItem[]): void { + for (const item of content) { + if (item.type === 'text' && item.text !== undefined) { + const value = parseToonToJson(item.text); + console.log(JSON.stringify(value, null, 2)); + } else { + console.log(`[${item.type}]`, item); + } + } + }, + }; +} + +export function createJsonFormatter(): OutputFormatter { + return { + info(message: string): void { + console.error(message); + }, + error(error: unknown): void { + const message = + error instanceof Error ? error.message : String(error); + console.error(JSON.stringify({ error: message })); + }, + clientError(err: Error): void { + console.error( + JSON.stringify({ error: err.message ?? String(err) }) + ); + }, + result(content: ToolResultContentItem[]): void { + const { values } = formatResultPayload(content); + if (values.length === 0) { + console.log('null'); + return; + } + const payload = values.length === 1 ? values[0] : values; + console.log(JSON.stringify(payload)); + }, + }; +} diff --git a/src/tools.test.ts b/src/tools.test.ts new file mode 100644 index 0000000..d6489af --- /dev/null +++ b/src/tools.test.ts @@ -0,0 +1,79 @@ +import { describe, it, expect } from 'vitest'; +import type { Tool } from '@modelcontextprotocol/sdk/types.js'; +import { coerceValue, optionKey, parseToolArgs } from './tools.js'; + +describe('optionKey', () => { + it('converts kebab-case flag names to camelCase keys', () => { + expect(optionKey('foo-bar')).toBe('fooBar'); + expect(optionKey('a-b-c')).toBe('aBC'); + }); +}); + +describe('coerceValue', () => { + it('coerces numbers and integers', () => { + expect(coerceValue('42', { type: 'number' })).toBe(42); + expect(coerceValue('3', { type: 'integer' })).toBe(3); + }); + + it('rejects invalid numbers', () => { + expect(() => coerceValue('x', { type: 'number' })).toThrow( + 'Must be a number' + ); + }); + + it('coerces booleans', () => { + expect(coerceValue('true', { type: 'boolean' })).toBe(true); + expect(coerceValue('0', { type: 'boolean' })).toBe(false); + }); + + it('rejects invalid booleans', () => { + expect(() => coerceValue('maybe', { type: 'boolean' })).toThrow( + 'Must be true/false' + ); + }); + + it('validates enum values', () => { + expect(coerceValue('a', { enum: ['a', 'b'] })).toBe('a'); + expect(() => coerceValue('c', { enum: ['a', 'b'] })).toThrow( + 'Must be one of: a, b' + ); + }); + + it('returns raw string for default schema', () => { + expect(coerceValue('hello', {})).toBe('hello'); + }); +}); + +describe('parseToolArgs', () => { + const tool: Tool = { + name: 't', + inputSchema: { + type: 'object', + properties: { + count: { type: 'integer' }, + label: { type: 'string' }, + }, + }, + }; + + it('maps commander opts (camelCase) to tool argument names', () => { + const args = parseToolArgs( + { count: '5', label: 'hi' } as Record, + tool + ); + expect(args).toEqual({ count: 5, label: 'hi' }); + }); + + it('omits undefined options', () => { + const args = parseToolArgs({ count: '1' }, tool); + expect(args).toEqual({ count: 1 }); + }); + + it('returns empty object when tool has no properties', () => { + const noProps: Tool = { + name: 't', + inputSchema: { type: 'object' }, + }; + expect(parseToolArgs({ a: '1' }, noProps)).toEqual({}); + }); +}); diff --git a/src/tools.ts b/src/tools.ts new file mode 100644 index 0000000..a871e85 --- /dev/null +++ b/src/tools.ts @@ -0,0 +1,81 @@ +import { Command } from 'commander'; +import type { Tool } from '@modelcontextprotocol/sdk/types.js'; + +export interface JsonSchemaProperty { + type?: string; + description?: string; + default?: unknown; + enum?: unknown[]; +} + +export function coerceValue(raw: string, prop: JsonSchemaProperty): unknown { + if (prop.enum) { + if (!prop.enum.includes(raw)) + throw new Error(`Must be one of: ${prop.enum.join(', ')}`); + return raw; + } + switch (prop.type) { + case 'number': + case 'integer': { + const n = Number(raw); + if (Number.isNaN(n)) throw new Error('Must be a number'); + return n; + } + case 'boolean': + if (['true', '1', 'yes'].includes(raw)) return true; + if (['false', '0', 'no'].includes(raw)) return false; + throw new Error('Must be true/false'); + case 'object': + case 'array': + return JSON.parse(raw); + default: + return raw; + } +} + +export function buildOptionsForTool(cmd: Command, tool: Tool): void { + const schema = tool.inputSchema as { + properties?: Record; + required?: string[]; + }; + + if (!schema.properties) return; + + const required = new Set(schema.required ?? []); + + for (const [name, prop] of Object.entries(schema.properties)) { + const flag = `--${name} `; + let desc = prop.description ?? ''; + if (prop.enum) desc += ` (${prop.enum.join(', ')})`; + + if (prop.default !== undefined) { + cmd.option(flag, desc, String(prop.default)); + } else if (required.has(name)) { + cmd.requiredOption(flag, desc); + } else { + cmd.option(flag, desc); + } + } +} + +export function parseToolArgs( + opts: Record, + tool: Tool +): Record { + const schema = tool.inputSchema as { + properties?: Record; + }; + if (!schema.properties) return {}; + + const args: Record = {}; + for (const [name, prop] of Object.entries(schema.properties)) { + const raw = opts[optionKey(name)]; + if (raw === undefined) continue; + args[name] = coerceValue(raw, prop); + } + return args; +} + +export function optionKey(s: string): string { + return s.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase()); +} diff --git a/tsconfig.json b/tsconfig.json index 05a2263..f81fe05 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,5 +11,6 @@ "declaration": false, "sourceMap": false }, - "include": ["src"] + "include": ["src"], + "exclude": ["src/**/*.test.ts"] }