From a221f2c319bc02fb98e0ede93eeccd3ae3c864cd Mon Sep 17 00:00:00 2001 From: Tomas Grasl Date: Sun, 8 Mar 2026 09:33:50 +0100 Subject: [PATCH] feat: add full source, toolchain, CI/CD, tests, and documentation - Source code: 6 MCP tools, Gemini engine, PNG pipeline, prompt builder - Toolchain: ESLint 10 + Prettier + Vitest + tsup - CI: GitHub Actions for lint/format/typecheck/test/build (Node 20+22) - Publish: GitHub Actions for npm publish on release/tag - Tests: 19 unit tests (image-ops, models, PNG encode/decode) - Plugin: Claude Code plugin config with skills - README: installation guide, tool docs, model reference Co-Authored-By: Claude Opus 4.6 --- .claude-plugin/marketplace.json | 16 + .claude-plugin/plugin.json | 14 + .github/workflows/ci.yml | 55 + .github/workflows/publish.yml | 56 + .mcp.json | 11 + .prettierrc.json | 11 + README.md | 164 +- eslint.config.js | 34 + package-lock.json | 4236 ++++++++++++++++++++++++++ package.json | 75 + skills/pixel-art-generation/SKILL.md | 87 + skills/sprite-processing/SKILL.md | 67 + src/config/constants.ts | 2 + src/engine/gemini.ts | 129 + src/engine/models.ts | 48 + src/index.ts | 111 + src/pipeline/image-ops.ts | 585 ++++ src/pipeline/png.ts | 175 ++ src/pipeline/prompt-builder.ts | 155 + src/prompts/pixel-art-guide.ts | 83 + src/tools/forge-animation.ts | 156 + src/tools/forge-background.ts | 87 + src/tools/forge-sprite.ts | 113 + src/tools/forge-thumbnail.ts | 93 + src/tools/index.ts | 11 + src/tools/optimize-sprite.ts | 108 + src/tools/process-sprite.ts | 132 + src/types/common.ts | 35 + src/utils/logger.ts | 11 + src/utils/response-helpers.ts | 22 + tests/unit/image-ops.test.ts | 98 + tests/unit/models.test.ts | 29 + tests/unit/png.test.ts | 78 + tsconfig.json | 27 + tsup.config.ts | 16 + vitest.config.ts | 15 + 36 files changed, 7144 insertions(+), 1 deletion(-) create mode 100644 .claude-plugin/marketplace.json create mode 100644 .claude-plugin/plugin.json create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/publish.yml create mode 100644 .mcp.json create mode 100644 .prettierrc.json create mode 100644 eslint.config.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 skills/pixel-art-generation/SKILL.md create mode 100644 skills/sprite-processing/SKILL.md create mode 100644 src/config/constants.ts create mode 100644 src/engine/gemini.ts create mode 100644 src/engine/models.ts create mode 100644 src/index.ts create mode 100644 src/pipeline/image-ops.ts create mode 100644 src/pipeline/png.ts create mode 100644 src/pipeline/prompt-builder.ts create mode 100644 src/prompts/pixel-art-guide.ts create mode 100644 src/tools/forge-animation.ts create mode 100644 src/tools/forge-background.ts create mode 100644 src/tools/forge-sprite.ts create mode 100644 src/tools/forge-thumbnail.ts create mode 100644 src/tools/index.ts create mode 100644 src/tools/optimize-sprite.ts create mode 100644 src/tools/process-sprite.ts create mode 100644 src/types/common.ts create mode 100644 src/utils/logger.ts create mode 100644 src/utils/response-helpers.ts create mode 100644 tests/unit/image-ops.test.ts create mode 100644 tests/unit/models.test.ts create mode 100644 tests/unit/png.test.ts create mode 100644 tsconfig.json create mode 100644 tsup.config.ts create mode 100644 vitest.config.ts diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 0000000..13d2938 --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,16 @@ +{ + "name": "pixelforge-plugins", + "version": "1.0.0", + "description": "Pixel art generation and sprite processing tools for game development.", + "owner": { + "name": "Tomas Grasl", + "url": "https://github.com/freema" + }, + "plugins": [ + { + "name": "pixelforge-mcp", + "source": "./", + "description": "Forge pixel art sprites, animations, backgrounds & thumbnails using Google Gemini" + } + ] +} diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..48ff8b0 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,14 @@ +{ + "name": "pixelforge-mcp", + "version": "0.1.0", + "description": "Forge pixel art sprites, animations, backgrounds & thumbnails using Google Gemini. Auto prompt engineering, crop, background removal, and sprite sheet splitting.", + "mcpServers": { + "pixelforge": { + "command": "npx", + "args": ["-y", "pixelforge-mcp@latest"], + "env": { + "GEMINI_API_KEY": "" + } + } + } +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e3e1469 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,55 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node-version: [20, 22] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + + - name: Install dependencies + run: npm ci --no-audit --no-fund + + - name: Lint + run: npm run lint + + - name: Format check + run: npm run format:check + + - name: Type check + run: npm run typecheck + + - name: Test + run: npm run test:run + + - name: Build + run: npm run build + + - name: Upload artifacts + if: matrix.node-version == 20 + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + retention-days: 7 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..cf8260e --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,56 @@ +name: Publish to npm + +on: + release: + types: [published] + push: + tags: + - 'v*.*.*' + workflow_dispatch: + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + registry-url: 'https://registry.npmjs.org/' + cache: 'npm' + always-auth: true + + - name: Configure npm auth + run: | + echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" > ~/.npmrc + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Install dependencies + run: npm ci + + - name: Lint + run: npm run lint + + - name: Format check + run: npm run format:check + + - name: Type check + run: npm run typecheck + + - name: Test + run: npm run test:run + + - name: Build + run: npm run build + + - name: Publish + run: npm publish --access public --provenance + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..b6161b9 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,11 @@ +{ + "mcpServers": { + "pixelforge": { + "command": "npx", + "args": ["-y", "pixelforge-mcp@latest"], + "env": { + "GEMINI_API_KEY": "" + } + } + } +} diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..36df54d --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,11 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": true, + "printWidth": 100, + "tabWidth": 2, + "useTabs": false, + "arrowParens": "always", + "endOfLine": "lf", + "bracketSpacing": true +} diff --git a/README.md b/README.md index 8383678..dec3a0f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,164 @@ # pixelforge-mcp -🎮 MCP server that forges pixel art sprites & game assets using Google Gemini — generate, crop, split & process, all from your AI + +MCP server that forges pixel art sprites & game assets using Google Gemini — generate, crop, split & process, all from your AI assistant. + +## Features + +- **AI-powered generation** — sprites, animations, backgrounds, thumbnails via Google Gemini +- **Smart post-processing** — background removal, auto-crop, pixelation downscale, square padding +- **Sprite sheet splitting** — auto-detect and split sheets into individual frames +- **Style presets** — neon, retro, gameboy, snes, clean +- **Pure PNG pipeline** — zero-dependency PNG encoder/decoder, no native modules +- **Reference matching** — pass existing sprites to match visual style + +## Installation + +### Claude Code (CLI) + +```bash +claude mcp add pixelforge npx pixelforge-mcp@latest \ + --env GEMINI_API_KEY=your-api-key +``` + +### Claude Code (Plugin) + +```bash +/plugin marketplace add freema/pixelforge-mcp +/plugin install pixelforge-mcp +``` + +Restart Claude Code to load the MCP server (check with `/mcp`). + +### Claude Desktop + +Add to your `claude_desktop_config.json`: + +```json +{ + "mcpServers": { + "pixelforge": { + "command": "npx", + "args": ["-y", "pixelforge-mcp@latest"], + "env": { + "GEMINI_API_KEY": "your-api-key" + } + } + } +} +``` + +### Manual (any MCP client) + +Add to your `.mcp.json` or equivalent config: + +```json +{ + "mcpServers": { + "pixelforge": { + "command": "npx", + "args": ["-y", "pixelforge-mcp@latest"], + "env": { + "GEMINI_API_KEY": "your-api-key" + } + } + } +} +``` + +### Requirements + +- Node.js >= 20 +- [Google Gemini API key](https://aistudio.google.com/apikey) + +## Tools + +### `forge_sprite` + +Generate a single pixel art sprite with automatic post-processing (crop, bg removal, downscale, square padding). + +``` +"Generate a green slime enemy sprite, 48px, retro style" +``` + +**Required:** `description`, `outputPath` +**Optional:** `size` (default: 48), `style`, `background`, `aspect`, `square`, `model`, `references` + +### `forge_animation` + +Generate animation frames as a sprite sheet, then auto-split into individual frame PNGs. + +``` +"Animate a slime bouncing, 4 frames" +``` + +**Required:** `description`, `action`, `outputPrefix` +**Optional:** `frames` (default: 3), `frameDescriptions`, `names`, `size`, `style`, `model`, `references` + +### `forge_background` + +Generate a full game background — no cropping, outputs the image as-is. + +``` +"Deep space background with stars and nebula, 16:9" +``` + +**Required:** `description`, `outputPath`, `aspect` +**Optional:** `style`, `model` + +### `forge_thumbnail` + +Generate a game thumbnail/screenshot. Pass `references` for visual consistency with your sprites. + +``` +"Space shooter scene with player ship vs alien rows" +``` + +**Required:** `description`, `outputPath` +**Optional:** `references`, `aspect` (default: 4:3), `style`, `model` + +### `process_sprite` + +Post-process an existing PNG — background removal, auto-crop, sprite sheet splitting. + +``` +"Split this sprite sheet into individual frames" +``` + +**Required:** `inputPath` +**Optional:** `outputPath`, `split`, `names`, `threshold`, `square`, `padding`, `skipCrop`, `skipTransparent` + +### `optimize_sprite` + +Downscale oversized AI images to true pixel art resolution using area-averaging (not blurry bilinear). + +``` +"Optimize this 1024px image down to 48px pixel art" +``` + +**Required:** `inputPath`, `size` +**Optional:** `outputPath`, `removeBackground`, `square` + +> Full parameter docs: see [docs/tools.md](docs/tools.md) + +## Models + +| Alias | Model ID | Notes | +|-------|----------|-------| +| `nano-banana`, `banana` | nano-banana-pro-preview | **Default** — best for pixel art | +| `flash`, `gemini-flash` | gemini-3.1-flash-image-preview | Fast, reliable | +| `pro`, `gemini-pro` | gemini-3-pro-image-preview | Best quality | +| `25`, `gemini-25` | gemini-2.5-flash-image | Stable fallback | + +## Prompts + +### `pixel_art_guide` + +Built-in MCP prompt with comprehensive pixel art generation guidelines — prompting rules, style tips, size recommendations, and best practices. + +## License + +MIT — see [LICENSE](LICENSE) + +--- + +Built by [Tomas Grasl](https://tomasgrasl.cz) diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..36294ec --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,34 @@ +import tseslint from '@typescript-eslint/eslint-plugin'; +import tsparser from '@typescript-eslint/parser'; +import prettier from 'eslint-config-prettier'; + +export default [ + { + files: ['src/**/*.ts'], + languageOptions: { + parser: tsparser, + parserOptions: { + project: './tsconfig.json', + sourceType: 'module', + }, + }, + plugins: { + '@typescript-eslint': tseslint, + }, + rules: { + ...tseslint.configs.recommended.rules, + 'prefer-const': 'error', + 'no-var': 'error', + eqeqeq: ['error', 'always'], + '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], + '@typescript-eslint/no-explicit-any': 'off', + }, + }, + { + files: ['src/**/*.ts'], + ...prettier, + }, + { + ignores: ['dist/', 'node_modules/', '*.config.*'], + }, +]; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..ee631c4 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4236 @@ +{ + "name": "pixelforge-mcp", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "pixelforge-mcp", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.17.1", + "jpeg-js": "^0.4.4" + }, + "bin": { + "pixelforge-mcp": "dist/index.js" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@typescript-eslint/eslint-plugin": "^8.56.1", + "@typescript-eslint/parser": "^8.56.1", + "eslint": "^10.0.3", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.5", + "prettier": "^3.8.1", + "tsup": "^8.0.0", + "tsx": "^4.7.0", + "typescript": "^5.3.3", + "vitest": "^4.0.18" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.3", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.3.tgz", + "integrity": "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.3", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.3.tgz", + "integrity": "sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.1.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.1.tgz", + "integrity": "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.3.tgz", + "integrity": "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz", + "integrity": "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.1.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "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/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "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/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", + "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz", + "integrity": "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/type-utils": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.56.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.1.tgz", + "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.1.tgz", + "integrity": "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.56.1", + "@typescript-eslint/types": "^8.56.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz", + "integrity": "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz", + "integrity": "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz", + "integrity": "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.1.tgz", + "integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz", + "integrity": "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.56.1", + "@typescript-eslint/tsconfig-utils": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.1.tgz", + "integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz", + "integrity": "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitest/expect": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", + "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", + "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.18", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", + "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", + "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.18", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", + "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", + "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", + "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "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/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/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/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/bundle-require": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", + "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.18" + } + }, + "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/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=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/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "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/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.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": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/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/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-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "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/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.0.3.tgz", + "integrity": "sha512-COV33RzXZkqhG9P2rZCFl9ZmJ7WL+gQSCRzE7RhkbclbQPtLAWReL7ysA0Sh4c8Im2U9ynybdR56PV0XcKvqaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.3", + "@eslint/config-helpers": "^0.5.2", + "@eslint/core": "^1.1.1", + "@eslint/plugin-kit": "^0.6.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.1.1", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.5.tgz", + "integrity": "sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.1", + "synckit": "^0.11.12" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "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/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-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.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/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/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/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fix-dts-default-cjs-exports": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", + "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "rollup": "^4.34.8" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.4.tgz", + "integrity": "sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA==", + "dev": true, + "license": "ISC" + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/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/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/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.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/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/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-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/jpeg-js": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", + "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==", + "license": "BSD-3-Clause" + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.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/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mlly": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.1.tgz", + "integrity": "sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "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/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-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": "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/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "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/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz", + "integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "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/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "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/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "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/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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/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/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/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "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/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/synckit": { + "version": "0.11.12", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", + "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tsup": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", + "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-require": "^5.1.0", + "cac": "^6.7.14", + "chokidar": "^4.0.3", + "consola": "^3.4.0", + "debug": "^4.4.0", + "esbuild": "^0.27.0", + "fix-dts-default-cjs-exports": "^1.0.0", + "joycon": "^3.1.1", + "picocolors": "^1.1.1", + "postcss-load-config": "^6.0.1", + "resolve-from": "^5.0.0", + "rollup": "^4.34.8", + "source-map": "^0.7.6", + "sucrase": "^3.35.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.11", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@microsoft/api-extractor": "^7.36.0", + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/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/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/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/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "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 + } + } + }, + "node_modules/vitest": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", + "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.18", + "@vitest/mocker": "4.0.18", + "@vitest/pretty-format": "4.0.18", + "@vitest/runner": "4.0.18", + "@vitest/snapshot": "4.0.18", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.18", + "@vitest/browser-preview": "4.0.18", + "@vitest/browser-webdriverio": "4.0.18", + "@vitest/ui": "4.0.18", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "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/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "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 new file mode 100644 index 0000000..026ceb7 --- /dev/null +++ b/package.json @@ -0,0 +1,75 @@ +{ + "name": "pixelforge-mcp", + "version": "0.1.0", + "description": "MCP server that forges pixel art sprites & game assets using Google Gemini", + "author": "Tomas Grasl", + "license": "MIT", + "type": "module", + "main": "dist/index.js", + "bin": { + "pixelforge-mcp": "./dist/index.js" + }, + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsup", + "start": "node dist/index.js", + "clean": "rm -rf dist", + "typecheck": "tsc --noEmit", + "lint": "eslint src", + "lint:fix": "eslint src --fix", + "format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\"", + "format:check": "prettier --check \"src/**/*.ts\" \"tests/**/*.ts\"", + "test": "vitest", + "test:run": "vitest run", + "test:coverage": "vitest run --coverage", + "check": "npm run typecheck && npm run lint && npm run format:check", + "check:all": "npm run check && npm run test:run && npm run build", + "prepublishOnly": "npm run clean && npm run check && npm run test:run && npm run build", + "inspector": "npx @modelcontextprotocol/inspector node dist/index.js", + "inspector:dev": "npx @modelcontextprotocol/inspector npx tsx src/index.ts" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.17.1", + "jpeg-js": "^0.4.4" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@typescript-eslint/eslint-plugin": "^8.56.1", + "@typescript-eslint/parser": "^8.56.1", + "eslint": "^10.0.3", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.5", + "prettier": "^3.8.1", + "tsup": "^8.0.0", + "tsx": "^4.7.0", + "typescript": "^5.3.3", + "vitest": "^4.0.18" + }, + "engines": { + "node": ">=20.0.0" + }, + "files": [ + "dist", + "README.md", + "LICENSE" + ], + "keywords": [ + "mcp", + "mcp-server", + "pixel-art", + "sprite", + "game-assets", + "gemini", + "imagen", + "ai-art", + "model-context-protocol" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/freema/pixelforge-mcp.git" + }, + "homepage": "https://github.com/freema/pixelforge-mcp#readme", + "publishConfig": { + "access": "public" + } +} diff --git a/skills/pixel-art-generation/SKILL.md b/skills/pixel-art-generation/SKILL.md new file mode 100644 index 0000000..f1676e9 --- /dev/null +++ b/skills/pixel-art-generation/SKILL.md @@ -0,0 +1,87 @@ +--- +name: pixel-art-generation +description: Use when generating pixel art sprites, animations, backgrounds, thumbnails, or processing sprite sheets for games. Triggers on requests like "create a sprite", "generate pixel art", "make game assets", "split sprite sheet", "remove background from sprite". +--- + +# Pixel Art Generation with PixelForge + +Use PixelForge MCP tools to generate and process pixel art game assets. + +## Tools Overview + +| Tool | When to Use | +|------|-------------| +| `forge_sprite` | Single sprite — character, item, icon, UI element | +| `forge_animation` | Animation frames — walk cycle, idle, attack, effects | +| `forge_background` | Full game background/scene — no processing applied | +| `forge_thumbnail` | Game thumbnail/screenshot — uses reference sprites | +| `process_sprite` | Post-process existing PNG — crop, bg removal, split | + +## Quick Start + +### Single Sprite +``` +forge_sprite + description: "green slime enemy with horns and glowing eyes" + outputPath: "public/assets/games/rpg/slime.png" + style: "neon" + background: "black" +``` + +### Animation Frames +``` +forge_animation + description: "green slime enemy" + action: "bouncing up and down" + frames: 4 + outputPrefix: "public/assets/games/rpg/slime" + names: ["bounce-0", "bounce-1", "bounce-2", "bounce-3"] + style: "neon" +``` +Returns individual frame files: `slime-bounce-0.png`, `slime-bounce-1.png`, etc. + +### Background +``` +forge_background + description: "deep space with stars, subtle neon grid on horizon, nebula wisps cyan and purple" + outputPath: "public/assets/games/invaders/bg.png" + aspect: "3:4" + style: "neon" +``` + +### Thumbnail (with style-matching references) +``` +forge_thumbnail + description: "space shooter, cyan spaceship vs rows of colorful aliens, dark space" + outputPath: "public/assets/games/invaders/thumbnail.png" + references: ["public/assets/games/invaders/ship.png", "public/assets/games/invaders/alien.png"] + aspect: "4:3" +``` + +## Style Presets + +| Style | Best For | Look | +|-------|----------|------| +| `neon` | Arcade, space, cyberpunk | Glowing, vibrant, dark outline | +| `retro` | Classic NES/arcade | 8-bit, limited palette, chunky | +| `gameboy` | Monochrome | 4-color green, dithered | +| `snes` | RPG, detailed | 16-bit, rich colors, detailed shading | +| `clean` | Generic/versatile | Solid colors, dark outline | + +## Model Selection + +| Alias | Speed | Quality | Notes | +|-------|-------|---------|-------| +| `gemini-pro` | Medium | Best | Default — best for final assets | +| `gemini-flash` | Fast | Good | Good for quick iteration | +| `imagen` | Medium | High | Supports negative prompts | +| `imagen-fast` | Fast | Good | Fastest option | + +## Guidelines + +1. **forge_sprite and forge_animation auto-process** — output is always cropped, transparent, and ready to use +2. **forge_background outputs raw** — no cropping, full size for game scenes +3. **Use references for thumbnails** — pass actual game sprites so thumbnail matches the game's look +4. **Choose aspect ratio wisely** — 1:1 for sprites, 4:3 for sheets, 3:4 or 16:9 for backgrounds +5. **After generating, verify with Read tool** — Claude Code can display PNG files natively +6. **Phaser sprite sizing** — generated images are large (~1000px), always use `setDisplaySize()` in game code diff --git a/skills/sprite-processing/SKILL.md b/skills/sprite-processing/SKILL.md new file mode 100644 index 0000000..b31022b --- /dev/null +++ b/skills/sprite-processing/SKILL.md @@ -0,0 +1,67 @@ +--- +name: sprite-processing +description: Use when processing, cropping, splitting, or cleaning up existing sprite images or sprite sheets. Triggers on "crop sprite", "remove background", "split sprite sheet", "make transparent", "process PNG". +--- + +# Sprite Processing with PixelForge + +Use `process_sprite` to clean up existing images into game-ready sprites. + +## Single Sprite Processing + +Remove background and auto-crop: +``` +process_sprite + inputPath: "raw-enemy.png" + outputPath: "public/assets/games/rpg/enemy.png" + background: "black" + square: true +``` + +## Split Sprite Sheet + +Split a horizontal sprite sheet into individual frames: +``` +process_sprite + inputPath: "animation-sheet.png" + outputPath: "public/assets/games/rpg/hero" + split: true + names: ["idle", "walk-1", "walk-2", "attack"] + background: "white" + square: true +``` +Outputs: `hero-idle.png`, `hero-walk-1.png`, `hero-walk-2.png`, `hero-attack.png` + +## Two-Pass Processing + +When sprite sheet has white background but individual sprites have black background: +``` +# 1. Split sheet (removes white bg between sprites) +process_sprite + inputPath: "sheet.png" + outputPath: "public/assets/sprites/char" + split: true + names: ["head", "body", "legs"] + background: "white" + square: true + +# 2. Remove black bg from each sprite +process_sprite + inputPath: "public/assets/sprites/char-head.png" + background: "black" + threshold: 8 + skipCrop: true +``` + +## Options Reference + +| Option | Default | Description | +|--------|---------|-------------| +| `background` | auto-detect | `"black"`, `"white"`, or `"auto"` | +| `threshold` | 20 | Color detection sensitivity (0-255) | +| `square` | false | Pad output to square dimensions | +| `padding` | 2 | Pixels of padding around content | +| `split` | false | Split sheet into individual sprites | +| `names` | 0, 1, 2... | Names for split sprites | +| `skipCrop` | false | Keep original dimensions | +| `skipTransparent` | false | Don't remove background | diff --git a/src/config/constants.ts b/src/config/constants.ts new file mode 100644 index 0000000..75b9642 --- /dev/null +++ b/src/config/constants.ts @@ -0,0 +1,2 @@ +export const SERVER_NAME = 'pixelforge-mcp'; +export const SERVER_VERSION = '0.1.0'; diff --git a/src/engine/gemini.ts b/src/engine/gemini.ts new file mode 100644 index 0000000..7b62bcf --- /dev/null +++ b/src/engine/gemini.ts @@ -0,0 +1,129 @@ +import { readFile } from 'fs/promises'; +import { resolve, extname } from 'path'; +import { resolveModel, DEFAULT_MODEL } from './models.js'; +import { log } from '../utils/logger.js'; +import type { GeneratedImage } from '../types/common.js'; + +const BASE = 'https://generativelanguage.googleapis.com/v1beta/models'; + +function getApiKey(): string { + const key = process.env.GEMINI_API_KEY; + if (!key) throw new Error('GEMINI_API_KEY environment variable is not set'); + return key; +} + +function mimeFromPath(p: string): string { + const e = extname(p).toLowerCase(); + const map: Record = { + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.webp': 'image/webp', + }; + return map[e] ?? 'image/png'; +} + +interface RefImage { + b64: string; + mime: string; + path: string; +} + +export async function loadRefImages(paths: string[]): Promise { + return Promise.all( + paths.map(async (p) => { + const buf = await readFile(resolve(p)); + return { b64: buf.toString('base64'), mime: mimeFromPath(p), path: p }; + }) + ); +} + +async function generateViaGemini( + prompt: string, + modelId: string, + count: number, + refs: RefImage[], + aspect?: string +): Promise { + const url = `${BASE}/${modelId}:generateContent?key=${getApiKey()}`; + + const parts: unknown[] = []; + for (const ref of refs) { + parts.push({ inlineData: { mimeType: ref.mime, data: ref.b64 } }); + } + parts.push({ text: prompt }); + + if (refs.length) { + log(`Reference images: ${refs.map((r) => r.path).join(', ')}`); + } + + const generationConfig: Record = { + responseModalities: ['IMAGE'], + candidateCount: count, + }; + + // Aspect ratio goes inside imageConfig per Gemini API docs + if (aspect) { + generationConfig.imageConfig = { aspectRatio: aspect }; + log(`Aspect ratio: ${aspect} (via imageConfig)`); + } + + const res = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + contents: [{ parts }], + generationConfig, + }), + }); + + if (!res.ok) { + const body = await res.text(); + throw new Error(`Gemini API ${res.status}: ${body}`); + } + + const data = (await res.json()) as { + candidates?: Array<{ + content?: { parts?: Array<{ inlineData?: { mimeType?: string; data?: string } }> }; + }>; + }; + const images: GeneratedImage[] = []; + + for (const candidate of data.candidates ?? []) { + for (const part of candidate.content?.parts ?? []) { + if (part.inlineData?.mimeType?.startsWith('image/')) { + images.push({ + b64: part.inlineData.data!, + ext: part.inlineData.mimeType.split('/')[1] ?? 'png', + }); + } + } + } + + return images; +} + +export interface GenerateOptions { + prompt: string; + model?: string; + aspect?: string; + count?: number; + references?: string[]; +} + +export async function generate(opts: GenerateOptions): Promise { + const modelDef = resolveModel(opts.model ?? DEFAULT_MODEL); + const count = Math.min(4, Math.max(1, opts.count ?? 1)); + + log(`Generating: "${opts.prompt.slice(0, 80)}..."`); + log(`Model: ${modelDef.id} | Count: ${count}`); + + const refs = opts.references?.length ? await loadRefImages(opts.references) : []; + const images = await generateViaGemini(opts.prompt, modelDef.id, count, refs, opts.aspect); + + if (!images.length) { + throw new Error('No images returned. Try a different model or prompt.'); + } + + return images; +} diff --git a/src/engine/models.ts b/src/engine/models.ts new file mode 100644 index 0000000..26330b0 --- /dev/null +++ b/src/engine/models.ts @@ -0,0 +1,48 @@ +import type { ModelDef } from '../types/common.js'; +import { log } from '../utils/logger.js'; + +const NANO_BANANA: ModelDef = { + id: 'nano-banana-pro-preview', + engine: 'gemini', + description: 'Nano Banana Pro — best for pixel art', +}; +const GEMINI_FLASH: ModelDef = { + id: 'gemini-3.1-flash-image-preview', + engine: 'gemini', + description: 'Gemini 3.1 Flash — fast, reliable', +}; +const GEMINI_PRO: ModelDef = { + id: 'gemini-3-pro-image-preview', + engine: 'gemini', + description: 'Gemini 3 Pro — best quality', +}; +const GEMINI_25: ModelDef = { + id: 'gemini-2.5-flash-image', + engine: 'gemini', + description: 'Gemini 2.5 Flash — stable fallback', +}; + +export const MODELS: Record = { + 'nano-banana': NANO_BANANA, + banana: NANO_BANANA, + 'banana-2': GEMINI_FLASH, + 'nano-banana-2': GEMINI_FLASH, + 'gemini-flash': GEMINI_FLASH, + flash: GEMINI_FLASH, + 'gemini-pro': GEMINI_PRO, + pro: GEMINI_PRO, + 'gemini-25': GEMINI_25, + 'gemini-flash-25': GEMINI_25, + '25': GEMINI_25, +}; + +export const MODEL_ALIASES = Object.keys(MODELS); +export const DEFAULT_MODEL = 'nano-banana'; + +export function resolveModel(nameOrAlias: string): ModelDef { + if (MODELS[nameOrAlias]) return MODELS[nameOrAlias]!; + log( + `Unknown model "${nameOrAlias}", falling back to ${DEFAULT_MODEL} (${MODELS[DEFAULT_MODEL]!.id})` + ); + return MODELS[DEFAULT_MODEL]!; +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..4c3325d --- /dev/null +++ b/src/index.ts @@ -0,0 +1,111 @@ +#!/usr/bin/env node + +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { + CallToolRequestSchema, + ListToolsRequestSchema, + ListPromptsRequestSchema, + GetPromptRequestSchema, +} from '@modelcontextprotocol/sdk/types.js'; +import type { CallToolRequest, GetPromptRequest } from '@modelcontextprotocol/sdk/types.js'; + +import { SERVER_NAME, SERVER_VERSION } from './config/constants.js'; +import { log, logError } from './utils/logger.js'; +import * as tools from './tools/index.js'; +import { + pixelArtGuideName, + pixelArtGuideDescription, + getPixelArtGuide, +} from './prompts/pixel-art-guide.js'; +import type { McpToolResponse } from './types/common.js'; + +const toolHandlers = new Map Promise>([ + ['forge_sprite', tools.handleForgeSprite], + ['forge_animation', tools.handleForgeAnimation], + ['forge_background', tools.handleForgeBackground], + ['forge_thumbnail', tools.handleForgeThumbnail], + ['process_sprite', tools.handleProcessSprite], + ['optimize_sprite', tools.handleOptimizeSprite], +]); + +const allTools = [ + tools.forgeSpriteTool, + tools.forgeAnimationTool, + tools.forgeBackgroundTool, + tools.forgeThumbnailTool, + tools.processSpriteTool, + tools.optimizeSpriteTool, +]; + +async function main() { + log(`Starting ${SERVER_NAME} v${SERVER_VERSION}`); + + if (!process.env.GEMINI_API_KEY) { + log('WARNING: GEMINI_API_KEY not set — generation tools will fail until it is provided'); + } + + const server = new Server( + { name: SERVER_NAME, version: SERVER_VERSION }, + { + capabilities: { + tools: {}, + prompts: {}, + }, + } + ); + + // List tools + server.setRequestHandler(ListToolsRequestSchema, async () => { + log('Listing tools'); + return { tools: allTools }; + }); + + // Execute tool + server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest) => { + const { name, arguments: args } = request.params; + log(`Executing: ${name}`); + + const handler = toolHandlers.get(name); + if (!handler) { + throw new Error(`Unknown tool: ${name}`); + } + + try { + return await handler(args); + } catch (error) { + logError(`Tool ${name} failed`, error); + throw error; + } + }); + + // List prompts + server.setRequestHandler(ListPromptsRequestSchema, async () => { + return { + prompts: [{ name: pixelArtGuideName, description: pixelArtGuideDescription }], + }; + }); + + // Get prompt + server.setRequestHandler(GetPromptRequestSchema, async (request: GetPromptRequest) => { + const { name } = request.params; + if (name === pixelArtGuideName) { + return getPixelArtGuide(); + } + throw new Error(`Unknown prompt: ${name}`); + }); + + const transport = new StdioServerTransport(); + await server.connect(transport); + + log(`${SERVER_NAME} running on stdio`); + log( + 'Tools: forge_sprite, forge_animation, forge_background, forge_thumbnail, process_sprite, optimize_sprite' + ); + log('Prompts: pixel_art_guide'); +} + +main().catch((error) => { + logError('Fatal error', error); + process.exit(1); +}); diff --git a/src/pipeline/image-ops.ts b/src/pipeline/image-ops.ts new file mode 100644 index 0000000..6825e63 --- /dev/null +++ b/src/pipeline/image-ops.ts @@ -0,0 +1,585 @@ +import type { ImageData } from '../types/common.js'; + +/** Standard pixel art sizes (multiples of 8/16 that are industry standard) */ +const STANDARD_SIZES = [16, 24, 32, 48, 64, 96, 128]; + +/** + * Snap a size to the nearest standard pixel art size. + * Standard sizes: 16, 24, 32, 48, 64, 96, 128. + * Returns 0 unchanged (means "skip downscale"). + */ +export function snapToPixelArtSize(size: number): number { + if (size <= 0) return 0; + let best = STANDARD_SIZES[0]!; + let bestDist = Math.abs(size - best); + for (const s of STANDARD_SIZES) { + const dist = Math.abs(size - s); + if (dist < bestDist) { + best = s; + bestDist = dist; + } + } + return best; +} + +/** Detected background — actual RGB color sampled from edges */ +export interface BgColor { + r: number; + g: number; + b: number; +} + +// Legacy alias for backwards compat +type Bg = 'black' | 'white'; + +/** + * Detect actual background color by sampling edge regions. + * Returns the average RGB of the border pixels. + */ +export function detectBgColor(pixels: Buffer, width: number, height: number): BgColor { + let totalR = 0, + totalG = 0, + totalB = 0, + count = 0; + const sampleSize = Math.max(4, Math.floor(Math.min(width, height) * 0.05)); + + // Sample top, bottom, left, right edges + for (let x = 0; x < width; x++) { + for (let dy = 0; dy < sampleSize && dy < height; dy++) { + // Top edge + const ti = (dy * width + x) * 4; + totalR += pixels[ti]!; + totalG += pixels[ti + 1]!; + totalB += pixels[ti + 2]!; + count++; + // Bottom edge + const bi = ((height - 1 - dy) * width + x) * 4; + totalR += pixels[bi]!; + totalG += pixels[bi + 1]!; + totalB += pixels[bi + 2]!; + count++; + } + } + for (let y = sampleSize; y < height - sampleSize; y++) { + for (let dx = 0; dx < sampleSize && dx < width; dx++) { + // Left edge + const li = (y * width + dx) * 4; + totalR += pixels[li]!; + totalG += pixels[li + 1]!; + totalB += pixels[li + 2]!; + count++; + // Right edge + const ri = (y * width + (width - 1 - dx)) * 4; + totalR += pixels[ri]!; + totalG += pixels[ri + 1]!; + totalB += pixels[ri + 2]!; + count++; + } + } + + return { + r: Math.round(totalR / count), + g: Math.round(totalG / count), + b: Math.round(totalB / count), + }; +} + +function isBgPixelColor(r: number, g: number, b: number, bg: BgColor, thresh: number): boolean { + return Math.abs(r - bg.r) < thresh && Math.abs(g - bg.g) < thresh && Math.abs(b - bg.b) < thresh; +} + +// Legacy wrappers +function isBgPixel(r: number, g: number, b: number, bg: Bg, thresh: number): boolean { + const bgc: BgColor = bg === 'white' ? { r: 255, g: 255, b: 255 } : { r: 0, g: 0, b: 0 }; + return isBgPixelColor(r, g, b, bgc, thresh); +} + +function isContent(r: number, g: number, b: number, bg: Bg, thresh: number): boolean { + return !isBgPixel(r, g, b, bg, thresh); +} + +export function makeTransparentColor( + pixels: Buffer, + width: number, + height: number, + bg: BgColor, + thresh: number = 40 +): Buffer { + const out = Buffer.from(pixels); + for (let i = 0; i < width * height * 4; i += 4) { + if (isBgPixelColor(out[i]!, out[i + 1]!, out[i + 2]!, bg, thresh)) { + out[i] = 0; + out[i + 1] = 0; + out[i + 2] = 0; + out[i + 3] = 0; + } + } + return out; +} + +interface Bounds { + x1: number; + y1: number; + x2: number; + y2: number; +} + +export function findBounds( + pixels: Buffer, + width: number, + height: number, + bg: Bg, + thresh: number = 20 +): Bounds { + let x1 = width; + let y1 = height; + let x2 = 0; + let y2 = 0; + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const i = (y * width + x) * 4; + if (isContent(pixels[i]!, pixels[i + 1]!, pixels[i + 2]!, bg, thresh)) { + x1 = Math.min(x1, x); + y1 = Math.min(y1, y); + x2 = Math.max(x2, x); + y2 = Math.max(y2, y); + } + } + } + return { x1, y1, x2: x2 + 1, y2: y2 + 1 }; +} + +export function cropPixels( + pixels: Buffer, + srcW: number, + x1: number, + y1: number, + x2: number, + y2: number +): ImageData { + const w = x2 - x1; + const h = y2 - y1; + const out = Buffer.alloc(w * h * 4); + for (let y = 0; y < h; y++) { + const srcOff = ((y1 + y) * srcW + x1) * 4; + const dstOff = y * w * 4; + pixels.copy(out, dstOff, srcOff, srcOff + w * 4); + } + return { width: w, height: h, pixels: out }; +} + +export function makeSquare(pixels: Buffer, width: number, height: number): ImageData { + const side = Math.max(width, height); + if (width === side && height === side) return { width, height, pixels }; + const out = Buffer.alloc(side * side * 4); + const ox = Math.floor((side - width) / 2); + const oy = Math.floor((side - height) / 2); + for (let y = 0; y < height; y++) { + const srcOff = y * width * 4; + const dstOff = ((oy + y) * side + ox) * 4; + pixels.copy(out, dstOff, srcOff, srcOff + width * 4); + } + return { width: side, height: side, pixels: out }; +} + +/** + * 2D blob detection using flood-fill connected-component analysis. + * Finds distinct sprite regions regardless of layout (horizontal, vertical, grid). + */ +export function findSpriteBlobs( + pixels: Buffer, + width: number, + height: number, + bg: Bg, + thresh: number = 20, + minBlobArea: number = 100 +): Bounds[] { + // Downscale for performance — work at 1/4 resolution + const scale = 4; + const sw = Math.ceil(width / scale); + const sh = Math.ceil(height / scale); + const visited = new Uint8Array(sw * sh); + + function isFg(sx: number, sy: number): boolean { + const ox = Math.min(sx * scale, width - 1); + const oy = Math.min(sy * scale, height - 1); + const i = (oy * width + ox) * 4; + return isContent(pixels[i]!, pixels[i + 1]!, pixels[i + 2]!, bg, thresh); + } + + const blobs: Bounds[] = []; + + for (let sy = 0; sy < sh; sy++) { + for (let sx = 0; sx < sw; sx++) { + const idx = sy * sw + sx; + if (visited[idx] || !isFg(sx, sy)) continue; + + // BFS flood-fill + let bx1 = sx, + by1 = sy, + bx2 = sx, + by2 = sy; + let area = 0; + const queue: number[] = [sx, sy]; + visited[idx] = 1; + + while (queue.length > 0) { + const cy = queue.pop()!; + const cx = queue.pop()!; + area++; + bx1 = Math.min(bx1, cx); + by1 = Math.min(by1, cy); + bx2 = Math.max(bx2, cx); + by2 = Math.max(by2, cy); + + // 4-connected neighbors + for (const [dx, dy] of [ + [1, 0], + [-1, 0], + [0, 1], + [0, -1], + ]) { + const nx = cx + dx!; + const ny = cy + dy!; + if (nx < 0 || nx >= sw || ny < 0 || ny >= sh) continue; + const ni = ny * sw + nx; + if (visited[ni] || !isFg(nx, ny)) continue; + visited[ni] = 1; + queue.push(nx, ny); + } + } + + // Filter tiny noise blobs + const scaledArea = area * scale * scale; + if (scaledArea >= minBlobArea) { + blobs.push({ + x1: bx1 * scale, + y1: by1 * scale, + x2: Math.min((bx2 + 1) * scale, width), + y2: Math.min((by2 + 1) * scale, height), + }); + } + } + } + + // Sort blobs left-to-right, then top-to-bottom + blobs.sort((a, b) => { + const ax = (a.x1 + a.x2) / 2; + const bx = (b.x1 + b.x2) / 2; + const ay = (a.y1 + a.y2) / 2; + const by = (b.y1 + b.y2) / 2; + // If centers are within 20% of image height, consider same row + const rowThresh = height * 0.2; + if (Math.abs(ay - by) < rowThresh) return ax - bx; + return ay - by; + }); + + return blobs; +} + +/** + * Merge overlapping or very close blobs into single bounds. + */ +function mergeCloseBlobs(blobs: Bounds[], gapThresh: number): Bounds[] { + if (blobs.length <= 1) return blobs; + + const merged: Bounds[] = [{ ...blobs[0]! }]; + + for (let i = 1; i < blobs.length; i++) { + const b = blobs[i]!; + const last = merged[merged.length - 1]!; + + // Check if blobs overlap or are very close + const xOverlap = b.x1 <= last.x2 + gapThresh && b.x2 >= last.x1 - gapThresh; + const yOverlap = b.y1 <= last.y2 + gapThresh && b.y2 >= last.y1 - gapThresh; + + if (xOverlap && yOverlap) { + last.x1 = Math.min(last.x1, b.x1); + last.y1 = Math.min(last.y1, b.y1); + last.x2 = Math.max(last.x2, b.x2); + last.y2 = Math.max(last.y2, b.y2); + } else { + merged.push({ ...b }); + } + } + + return merged; +} + +/** + * Equidistant split fallback — divide full content area into N equal slices. + * Uses content bounds to avoid splitting empty margins. + */ +function equidistantSplit(img: ImageData, count: number, bg: Bg, thresh: number = 20): Bounds[] { + // Find overall content bounds first + const bounds = findBounds(img.pixels, img.width, img.height, bg, thresh); + const contentW = bounds.x2 - bounds.x1; + const contentH = bounds.y2 - bounds.y1; + + // Decide split direction based on content shape + const isHorizontal = contentW > contentH; + + const results: Bounds[] = []; + if (isHorizontal) { + const sliceW = Math.floor(contentW / count); + for (let i = 0; i < count; i++) { + const x1 = bounds.x1 + i * sliceW; + const x2 = i === count - 1 ? bounds.x2 : x1 + sliceW; + results.push({ x1, y1: bounds.y1, x2, y2: bounds.y2 }); + } + } else { + const sliceH = Math.floor(contentH / count); + for (let i = 0; i < count; i++) { + const y1 = bounds.y1 + i * sliceH; + const y2 = i === count - 1 ? bounds.y2 : y1 + sliceH; + results.push({ x1: bounds.x1, y1, x2: bounds.x2, y2 }); + } + } + return results; +} + +/** + * Area-averaging downscale — produces clean pixel art from high-res "pixel art style" images. + * Each output pixel is the average of all source pixels that map to it. + * This turns fake pixel art (where each visible "pixel" is 10-20 real pixels) into real pixel art. + */ +export function pixelateDownscale( + pixels: Buffer, + srcW: number, + srcH: number, + dstW: number, + dstH: number +): ImageData { + const out = Buffer.alloc(dstW * dstH * 4); + const scaleX = srcW / dstW; + const scaleY = srcH / dstH; + + for (let dy = 0; dy < dstH; dy++) { + for (let dx = 0; dx < dstW; dx++) { + const sx1 = Math.floor(dx * scaleX); + const sy1 = Math.floor(dy * scaleY); + const sx2 = Math.min(Math.floor((dx + 1) * scaleX), srcW); + const sy2 = Math.min(Math.floor((dy + 1) * scaleY), srcH); + + let r = 0, + g = 0, + b = 0, + a = 0, + count = 0; + for (let sy = sy1; sy < sy2; sy++) { + for (let sx = sx1; sx < sx2; sx++) { + const si = (sy * srcW + sx) * 4; + const alpha = pixels[si + 3]!; + if (alpha > 0) { + r += pixels[si]!; + g += pixels[si + 1]!; + b += pixels[si + 2]!; + a += alpha; + count++; + } + } + } + + const di = (dy * dstW + dx) * 4; + const totalPixels = (sx2 - sx1) * (sy2 - sy1); + if (count > 0 && count >= totalPixels * 0.3) { + // Enough opaque pixels — this is content + out[di] = Math.round(r / count); + out[di + 1] = Math.round(g / count); + out[di + 2] = Math.round(b / count); + out[di + 3] = Math.round(a / count); + } + // else: stays transparent (0,0,0,0) + } + } + return { width: dstW, height: dstH, pixels: out }; +} + +export function processSpriteColor( + img: ImageData, + bgColor: BgColor, + opts: { + threshold?: number; + padding?: number; + square?: boolean; + skipCrop?: boolean; + skipTransparent?: boolean; + size?: number; + } +): ImageData { + const thresh = opts.threshold ?? 40; + const pad = opts.padding ?? 2; + let { width, height, pixels } = img; + + if (!opts.skipTransparent) { + pixels = makeTransparentColor(pixels, width, height, bgColor, thresh); + } + + if (!opts.skipCrop) { + // Find non-transparent bounds + let x1 = width, + y1 = height, + x2 = 0, + y2 = 0; + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const i = (y * width + x) * 4; + if (pixels[i + 3]! > 0) { + x1 = Math.min(x1, x); + y1 = Math.min(y1, y); + x2 = Math.max(x2, x); + y2 = Math.max(y2, y); + } + } + } + if (x2 >= x1 && y2 >= y1) { + const cx1 = Math.max(0, x1 - pad); + const cy1 = Math.max(0, y1 - pad); + const cx2 = Math.min(width, x2 + 1 + pad); + const cy2 = Math.min(height, y2 + 1 + pad); + const cropped = cropPixels(pixels, width, cx1, cy1, cx2, cy2); + width = cropped.width; + height = cropped.height; + pixels = cropped.pixels; + } + } + + if (opts.square) { + const sq = makeSquare(pixels, width, height); + width = sq.width; + height = sq.height; + pixels = sq.pixels; + } + + // Pixelate downscale to target size — turns fake pixel art into real pixel art + const targetSize = opts.size ?? 64; + if (targetSize > 0 && (width > targetSize || height > targetSize)) { + const scale = targetSize / Math.max(width, height); + const nw = Math.max(1, Math.round(width * scale)); + const nh = Math.max(1, Math.round(height * scale)); + const downscaled = pixelateDownscale(pixels, width, height, nw, nh); + width = downscaled.width; + height = downscaled.height; + pixels = downscaled.pixels; + } + + return { width, height, pixels }; +} + +export function processSprite( + img: ImageData, + opts: { + threshold?: number; + padding?: number; + square?: boolean; + skipCrop?: boolean; + skipTransparent?: boolean; + maxSize?: number; + } +): ImageData { + const bgColor = detectBgColor(img.pixels, img.width, img.height); + return processSpriteColor(img, bgColor, opts); +} + +export function splitAndProcess( + img: ImageData, + opts: { + threshold?: number; + padding?: number; + square?: boolean; + expectedFrames?: number; + maxSize?: number; + } +): ImageData[] { + const pad = opts.padding ?? 4; + const expected = opts.expectedFrames; + const maxSize = opts.maxSize ?? 128; + + // Use actual color-based bg detection + const bgColor = detectBgColor(img.pixels, img.width, img.height); + const bg = (bgColor.r + bgColor.g + bgColor.b) / 3 > 128 ? ('white' as Bg) : ('black' as Bg); + const thresh = opts.threshold ?? 40; + + // Step 1: 2D blob detection — finds distinct sprite regions + const minBlobArea = Math.floor(img.width * img.height * 0.005); + let blobs = findSpriteBlobs(img.pixels, img.width, img.height, bg, thresh, minBlobArea); + + // Step 2: Merge blobs that are very close (parts of same sprite) + const mergeDist = Math.floor(Math.min(img.width, img.height) * 0.03); + blobs = mergeCloseBlobs(blobs, mergeDist); + + let bounds: Bounds[]; + + if (expected && blobs.length === expected) { + bounds = blobs; + } else if (expected && blobs.length > expected) { + const bigMerge = Math.floor(Math.min(img.width, img.height) * 0.08); + const merged = mergeCloseBlobs(blobs, bigMerge); + bounds = merged.length === expected ? merged : equidistantSplit(img, expected, bg, thresh); + } else if (expected) { + bounds = equidistantSplit(img, expected, bg, thresh); + } else { + bounds = blobs.length > 0 ? blobs : [{ x1: 0, y1: 0, x2: img.width, y2: img.height }]; + } + + const results: ImageData[] = []; + + for (const b of bounds) { + const cx1 = Math.max(0, b.x1 - pad); + const cy1 = Math.max(0, b.y1 - pad); + const cx2 = Math.min(img.width, b.x2 + pad); + const cy2 = Math.min(img.height, b.y2 + pad); + + let sprite = cropPixels(img.pixels, img.width, cx1, cy1, cx2, cy2); + // Use color-based transparency for accurate bg removal + sprite.pixels = makeTransparentColor( + sprite.pixels, + sprite.width, + sprite.height, + bgColor, + thresh + ); + + // Tight crop to non-transparent pixels + let x1t = sprite.width, + y1t = sprite.height, + x2t = 0, + y2t = 0; + for (let y = 0; y < sprite.height; y++) { + for (let x = 0; x < sprite.width; x++) { + if (sprite.pixels[(y * sprite.width + x) * 4 + 3]! > 0) { + x1t = Math.min(x1t, x); + y1t = Math.min(y1t, y); + x2t = Math.max(x2t, x); + y2t = Math.max(y2t, y); + } + } + } + if (x2t >= x1t && y2t >= y1t) { + sprite = cropPixels( + sprite.pixels, + sprite.width, + Math.max(0, x1t - 2), + Math.max(0, y1t - 2), + Math.min(sprite.width, x2t + 3), + Math.min(sprite.height, y2t + 3) + ); + } + + if (opts.square) { + sprite = makeSquare(sprite.pixels, sprite.width, sprite.height); + } + + // Pixelate downscale to target game size + if (maxSize > 0 && (sprite.width > maxSize || sprite.height > maxSize)) { + const scale = maxSize / Math.max(sprite.width, sprite.height); + const nw = Math.max(1, Math.round(sprite.width * scale)); + const nh = Math.max(1, Math.round(sprite.height * scale)); + sprite = pixelateDownscale(sprite.pixels, sprite.width, sprite.height, nw, nh); + } + + results.push(sprite); + } + + return results; +} diff --git a/src/pipeline/png.ts b/src/pipeline/png.ts new file mode 100644 index 0000000..9dbc8fc --- /dev/null +++ b/src/pipeline/png.ts @@ -0,0 +1,175 @@ +import { inflateSync, deflateSync } from 'zlib'; +import jpeg from 'jpeg-js'; +import type { ImageData } from '../types/common.js'; + +const PNG_SIG = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]); +const JPEG_SIG = Buffer.from([0xff, 0xd8, 0xff]); +const WEBP_SIG = Buffer.from('RIFF', 'ascii'); + +export type ImageFormat = 'png' | 'jpeg' | 'webp' | 'unknown'; + +export function detectFormat(buf: Buffer): ImageFormat { + if (buf.length >= 8 && buf.subarray(0, 8).equals(PNG_SIG)) return 'png'; + if (buf.length >= 3 && buf.subarray(0, 3).equals(JPEG_SIG)) return 'jpeg'; + if (buf.length >= 4 && buf.subarray(0, 4).equals(WEBP_SIG)) return 'webp'; + return 'unknown'; +} + +function paeth(a: number, b: number, c: number): number { + const p = a + b - c; + const pa = Math.abs(p - a); + const pb = Math.abs(p - b); + const pc = Math.abs(p - c); + if (pa <= pb && pa <= pc) return a; + if (pb <= pc) return b; + return c; +} + +export function decodePNG(buf: Buffer): ImageData { + if (!buf.subarray(0, 8).equals(PNG_SIG)) throw new Error('Not a PNG file'); + + let pos = 8; + let width = 0; + let height = 0; + let bitDepth = 0; + let colorType = 0; + const idatChunks: Buffer[] = []; + + while (pos < buf.length) { + const len = buf.readUInt32BE(pos); + const type = buf.toString('ascii', pos + 4, pos + 8); + const data = buf.subarray(pos + 8, pos + 8 + len); + + if (type === 'IHDR') { + width = data.readUInt32BE(0); + height = data.readUInt32BE(4); + bitDepth = data[8]!; + colorType = data[9]!; + } else if (type === 'IDAT') { + idatChunks.push(data); + } else if (type === 'IEND') { + break; + } + + pos += 12 + len; + } + + if (bitDepth !== 8) throw new Error(`Unsupported bit depth: ${bitDepth}`); + const srcChannels = colorType === 6 ? 4 : colorType === 2 ? 3 : 0; + if (!srcChannels) throw new Error(`Unsupported color type: ${colorType}`); + + const raw = inflateSync(Buffer.concat(idatChunks)); + const srcStride = width * srcChannels; + const bpp = srcChannels; + + const pixels = Buffer.alloc(width * height * 4); + let rawPos = 0; + + const curLine = Buffer.alloc(srcStride); + const prevLine = Buffer.alloc(srcStride); + + for (let y = 0; y < height; y++) { + const filterType = raw[rawPos++]!; + + for (let x = 0; x < srcStride; x++) { + const rawByte = raw[rawPos + x]!; + const a = x >= bpp ? curLine[x - bpp]! : 0; + const b = prevLine[x]!; + const c = x >= bpp ? prevLine[x - bpp]! : 0; + + let val = rawByte; + switch (filterType) { + case 0: + break; + case 1: + val += a; + break; + case 2: + val += b; + break; + case 3: + val += Math.floor((a + b) / 2); + break; + case 4: + val += paeth(a, b, c); + break; + } + curLine[x] = val & 0xff; + } + rawPos += srcStride; + + for (let x = 0; x < width; x++) { + const si = x * srcChannels; + const di = (y * width + x) * 4; + pixels[di] = curLine[si]!; + pixels[di + 1] = curLine[si + 1]!; + pixels[di + 2] = curLine[si + 2]!; + pixels[di + 3] = srcChannels === 4 ? curLine[si + 3]! : 255; + } + + curLine.copy(prevLine); + curLine.fill(0); + } + + return { width, height, pixels }; +} + +export function decodeJPEG(buf: Buffer): ImageData { + const decoded = jpeg.decode(buf, { useTArray: true, formatAsRGBA: true }); + return { width: decoded.width, height: decoded.height, pixels: Buffer.from(decoded.data) }; +} + +export function decodeImage(buf: Buffer): ImageData { + const format = detectFormat(buf); + if (format === 'png') return decodePNG(buf); + if (format === 'jpeg') return decodeJPEG(buf); + throw new Error(`Unsupported image format: ${format}`); +} + +const crcTable = (() => { + const t = new Uint32Array(256); + for (let n = 0; n < 256; n++) { + let c = n; + for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; + t[n] = c; + } + return t; +})(); + +function crc32(buf: Buffer): number { + let c = 0xffffffff; + for (let i = 0; i < buf.length; i++) c = crcTable[(c ^ buf[i]!) & 0xff]! ^ (c >>> 8); + return (c ^ 0xffffffff) >>> 0; +} + +export function encodePNG(width: number, height: number, pixels: Buffer): Buffer { + const chunks: Buffer[] = []; + + function addChunk(type: string, data: Buffer): void { + const typeBuf = Buffer.from(type, 'ascii'); + const lenBuf = Buffer.alloc(4); + lenBuf.writeUInt32BE(data.length); + const crcBuf = Buffer.alloc(4); + crcBuf.writeUInt32BE(crc32(Buffer.concat([typeBuf, data]))); + chunks.push(lenBuf, typeBuf, data, crcBuf); + } + + const ihdr = Buffer.alloc(13); + ihdr.writeUInt32BE(width, 0); + ihdr.writeUInt32BE(height, 4); + ihdr[8] = 8; + ihdr[9] = 6; + addChunk('IHDR', ihdr); + + const stride = width * 4; + const rawBuf = Buffer.alloc(height * (1 + stride)); + for (let y = 0; y < height; y++) { + rawBuf[y * (1 + stride)] = 0; + pixels.copy(rawBuf, y * (1 + stride) + 1, y * stride, y * stride + stride); + } + addChunk('IDAT', deflateSync(rawBuf)); + + addChunk('IEND', Buffer.alloc(0)); + + return Buffer.concat([PNG_SIG, ...chunks]); +} diff --git a/src/pipeline/prompt-builder.ts b/src/pipeline/prompt-builder.ts new file mode 100644 index 0000000..84b6b71 --- /dev/null +++ b/src/pipeline/prompt-builder.ts @@ -0,0 +1,155 @@ +import type { Style } from '../types/common.js'; + +const STYLE_PRESETS: Record = { + neon: 'neon glowing, vibrant saturated colors, dark outline, subtle glow effect, emissive highlights', + retro: '8-bit retro, limited color palette, chunky pixels, classic arcade feel, dithered shading', + gameboy: '4-color green monochrome palette, Game Boy style, dithered shading, no anti-aliasing', + snes: '16-bit SNES style, rich colors, detailed shading, clean outlines, smooth gradients', + clean: 'clean pixel art, solid colors, dark outline, no anti-aliasing, cel-shaded', +}; + +export const VALID_STYLES = Object.keys(STYLE_PRESETS) as Style[]; + +// Core rules that dramatically improve pixel art generation quality. +// These are the lessons learned from hundreds of generations. +const PIXEL_ART_CORE = [ + 'crisp sharp pixels', + 'no blur', + 'no gradients unless specified', + 'no anti-aliasing on edges', + 'visible individual pixels', + 'limited color palette', +]; + +const SPRITE_RULES = [ + 'single isolated object centered in frame', + 'consistent lighting from top-left', + 'dark pixel outline around the entire shape', + 'no shadow on ground', + 'no perspective distortion', +]; + +const ANIMATION_RULES = [ + 'consistent character size across all frames', + 'identical color palette across all frames', + 'identical outline thickness across all frames', + 'smooth motion transition between frames', +]; + +const NEGATIVE_ALWAYS = + 'No text, no labels, no watermark, no UI elements, no numbers, no letters, no words, no signature, no border, no frame.'; + +/** + * Detail hints based on target pixel art size. + * Smaller sprites need simpler designs; larger ones can have more detail. + * Mirrors PixelLab's detail parameter approach. + */ +function detailHintForSize(size: number): string { + if (size <= 0) return ''; + if (size <= 24) + return 'Very simple iconic shape, minimal detail, 3-5 colors maximum, bold readable silhouette, chunky features.'; + if (size <= 32) return 'Simple clean design, limited detail, 5-8 colors, clear silhouette.'; + if (size <= 48) return 'Medium detail, clean readable shapes, 8-12 colors.'; + if (size <= 64) return 'Moderate detail with clear features, up to 16 colors.'; + return 'Detailed pixel art, fine features allowed, rich color palette.'; +} + +export function buildSpritePrompt( + description: string, + style: Style = 'clean', + bg: string = 'black', + size: number = 48 +): string { + const parts = [ + `Pixel art sprite: ${description}.`, + STYLE_PRESETS[style] + '.', + PIXEL_ART_CORE.join(', ') + '.', + SPRITE_RULES.join(', ') + '.', + ]; + const hint = detailHintForSize(size); + if (hint) parts.push(hint); + parts.push(`Pure ${bg} background, completely flat solid ${bg} with no variation.`); + parts.push(NEGATIVE_ALWAYS); + return parts.join(' '); +} + +export function buildAnimationPrompt( + description: string, + frames: number, + action: string, + frameDescriptions: string[] | undefined, + style: Style = 'clean', + bg: string = 'black' +): string { + const ORDINALS = ['First', 'Second', 'Third', 'Fourth', 'Fifth', 'Sixth', 'Seventh', 'Eighth']; + const frameWord = + frames === 2 + ? 'two' + : frames === 3 + ? 'three' + : frames === 4 + ? 'four' + : frames === 5 + ? 'five' + : frames === 6 + ? 'six' + : frames === 7 + ? 'seven' + : frames === 8 + ? 'eight' + : String(frames); + + let frameInstructions: string; + if (frameDescriptions?.length) { + frameInstructions = + frameDescriptions.map((d, i) => `${ORDINALS[i] ?? 'Next'} pose shows ${d}`).join('. ') + '.'; + } else { + frameInstructions = `Animation shows: ${action}.`; + } + + return [ + `Pixel art sprite sheet: exactly ${frameWord} frames of ${description} arranged in a single horizontal row, evenly spaced.`, + frameInstructions, + STYLE_PRESETS[style] + '.', + PIXEL_ART_CORE.join(', ') + '.', + ANIMATION_RULES.join(', ') + '.', + `Pure ${bg} background, completely flat solid ${bg} with no variation between and around sprites.`, + `Each frame must be the same size and clearly separated by ${bg} space.`, + NEGATIVE_ALWAYS, + ].join(' '); +} + +export function buildBackgroundPrompt( + description: string, + style: Style = 'clean', + aspect?: string +): string { + const parts = [ + `Pixel art game background: ${description}.`, + STYLE_PRESETS[style] + '.', + 'Visible individual pixels, retro game aesthetic, tileable-friendly composition.', + 'Fill the entire canvas edge to edge, no borders, no empty space.', + ]; + if (aspect) { + const orientations: Record = { + '3:4': 'portrait orientation (taller than wide)', + '4:3': 'landscape orientation (wider than tall)', + '9:16': 'tall portrait orientation', + '16:9': 'wide landscape orientation', + '1:1': 'square format', + }; + parts.push(`Image should be ${orientations[aspect] ?? aspect}.`); + } + parts.push('No text, no watermark, no UI elements, no characters.'); + return parts.join(' '); +} + +export function buildThumbnailPrompt(description: string, style: Style = 'clean'): string { + return [ + `Pixel art game screenshot: ${description}.`, + STYLE_PRESETS[style] + '.', + 'Dynamic action composition, vibrant colors, game-in-action feel.', + 'Visible individual pixels, retro arcade aesthetic.', + NEGATIVE_ALWAYS, + ].join(' '); +} diff --git a/src/prompts/pixel-art-guide.ts b/src/prompts/pixel-art-guide.ts new file mode 100644 index 0000000..4114425 --- /dev/null +++ b/src/prompts/pixel-art-guide.ts @@ -0,0 +1,83 @@ +export const pixelArtGuideName = 'pixel_art_guide'; + +export const pixelArtGuideDescription = + 'Comprehensive pixel art generation guidelines — prompting rules, style tips, and best practices for creating game assets with AI.'; + +export function getPixelArtGuide(): { + messages: Array<{ role: 'user'; content: { type: 'text'; text: string } }>; +} { + return { + messages: [ + { + role: 'user', + content: { + type: 'text', + text: PIXEL_ART_GUIDE, + }, + }, + ], + }; +} + +const PIXEL_ART_GUIDE = `# Pixel Art Generation Guide + +## Prompting Rules (Critical) + +1. **Always specify background**: "pure black background" or "pure white background" + - Black works best for neon/dark-themed games + - White works best for light/casual games + - Solid color backgrounds enable clean extraction + +2. **Always add "No text, no labels, no watermark, no other elements"** + - AI models love adding random text — this prevents it + +3. **Specify proportions**: "square shape", "7 times wider than tall", "~2.5:1 aspect ratio" + +4. **Be specific about visual style**: + - "pixel art" (always include this) + - "dark outline" (gives sprites clean edges) + - Add style keywords: "8-bit", "16-bit", "retro arcade", "neon glowing" + +## Style Presets + +| Style | Best For | Keywords | +|----------|--------------------------------|-----------------------------------------------------| +| neon | Arcade, space, cyberpunk | neon glowing, vibrant colors, dark outline, glow | +| retro | Classic arcade, NES style | 8-bit, limited palette, chunky pixels | +| gameboy | Monochrome, minimal | 4-color green palette, dithered shading | +| snes | RPG, detailed games | 16-bit, rich colors, detailed shading | +| clean | Generic, versatile | solid colors, dark outline, no anti-aliasing | + +## Aspect Ratio Selection + +| Content | Aspect | Why | +|------------------|--------|----------------------------------------| +| Single sprite | 1:1 | Square works for most game objects | +| Wide asset | 16:9 | Paddles, platforms, wide items | +| Sprite sheet | 4:3 | Horizontal row of frames needs width | +| Portrait game bg | 3:4 | Mobile/portrait game backgrounds | +| Landscape bg | 16:9 | Desktop/landscape game backgrounds | + +## Animation Sprite Sheets + +When generating animation frames: +- Always request "in horizontal row, evenly spaced" +- Specify exact frame count and describe each frame +- Use 4:3 aspect for sheets (gives room for horizontal layout) +- Example: "three frames: first pose wings up, second pose wings middle, third pose wings down" + +## Reference Images + +- Use reference images when you need style consistency across multiple assets +- Most useful for thumbnails that should match actual game sprites +- Gemini will attempt to match the visual style of reference images + +## Post-Processing + +All forge_sprite and forge_animation outputs are automatically: +- Cropped to content bounding box +- Background removed (made transparent) +- Optionally padded to square + +For backgrounds and thumbnails, no processing is applied — they go straight to the game. +`; diff --git a/src/tools/forge-animation.ts b/src/tools/forge-animation.ts new file mode 100644 index 0000000..2e0e400 --- /dev/null +++ b/src/tools/forge-animation.ts @@ -0,0 +1,156 @@ +import { writeFile, mkdir } from 'fs/promises'; +import { dirname, resolve } from 'path'; +import { generate } from '../engine/gemini.js'; +import { buildAnimationPrompt } from '../pipeline/prompt-builder.js'; +import { decodeImage, encodePNG, detectFormat } from '../pipeline/png.js'; +import { splitAndProcess, snapToPixelArtSize } from '../pipeline/image-ops.js'; +import { forgeResponse, errorResponse } from '../utils/response-helpers.js'; +import { log } from '../utils/logger.js'; +import { MODEL_ALIASES, DEFAULT_MODEL } from '../engine/models.js'; +import { VALID_STYLES } from '../pipeline/prompt-builder.js'; +import type { McpToolResponse, Style, ForgeResult } from '../types/common.js'; + +export const forgeAnimationTool = { + name: 'forge_animation', + description: + 'Generate pixel art animation frames. Produces a sprite sheet via Gemini, then automatically splits it into individual frame PNGs with background removal and cropping. Returns an array of ready-to-use frame files.', + inputSchema: { + type: 'object', + properties: { + description: { + type: 'string', + description: 'What is being animated (e.g. "green slime enemy", "player knight")', + }, + action: { + type: 'string', + description: + 'What the animation shows (e.g. "bouncing up and down", "walking cycle", "idle breathing")', + }, + frames: { + type: 'number', + description: 'Number of animation frames (default: 3)', + }, + frameDescriptions: { + type: 'array', + items: { type: 'string' }, + description: + 'Optional per-frame descriptions (e.g. ["compressed flat", "stretching up", "at peak"])', + }, + outputPrefix: { + type: 'string', + description: + 'Output path prefix — frames saved as {prefix}-{name}.png (e.g. "public/assets/games/rpg/slime")', + }, + names: { + type: 'array', + items: { type: 'string' }, + description: 'Frame names (default: frame-0, frame-1, ...)', + }, + style: { + type: 'string', + enum: VALID_STYLES, + description: 'Visual style preset (default: clean)', + }, + background: { + type: 'string', + enum: ['black', 'white'], + description: 'Generation background color (default: black)', + }, + size: { + type: 'number', + description: + 'Target frame size in pixels (default: 48). Snaps to nearest standard size. Recommended: 16-24 for particle effects/small FX, 32 for item animations, 48 for character animations (PixelLab default), 64 for detailed characters, 96-128 for bosses. Set to 0 to skip downscale.', + }, + square: { + type: 'boolean', + description: 'Pad each frame to square (default: true)', + }, + model: { + type: 'string', + description: `Model alias or full ID. Aliases: ${MODEL_ALIASES.join(', ')} (default: ${DEFAULT_MODEL})`, + }, + references: { + type: 'array', + items: { type: 'string' }, + description: + 'Paths to existing PNG/JPEG assets to match visual style. The animation frames will be generated in a similar art style, color palette, and detail level as the reference images.', + }, + }, + required: ['description', 'action', 'outputPrefix'], + }, +}; + +export async function handleForgeAnimation(input: unknown): Promise { + try { + const args = input as Record; + const description = args.description as string; + const action = args.action as string; + const frameCount = (args.frames as number) ?? 3; + const frameDescriptions = args.frameDescriptions as string[] | undefined; + const outputPrefix = args.outputPrefix as string; + const names = args.names as string[] | undefined; + const style = (args.style as Style) ?? 'clean'; + const bg = (args.background as 'black' | 'white') ?? 'black'; + const size = args.size as number | undefined; + const square = (args.square as boolean) ?? true; + const model = args.model as string | undefined; + const references = args.references as string[] | undefined; + + const prompt = buildAnimationPrompt( + description, + frameCount, + action, + frameDescriptions, + style, + bg + ); + log(`Prompt: ${prompt}`); + + const images = await generate({ prompt, model, aspect: '4:3', references }); + const imgBuf = Buffer.from(images[0]!.b64, 'base64'); + const format = detectFormat(imgBuf); + const decoded = decodeImage(imgBuf); + const threshold = format === 'jpeg' ? 60 : 25; + + log(`Raw sheet: ${decoded.width}x${decoded.height} (${format}, threshold: ${threshold})`); + + const targetSize = snapToPixelArtSize(size ?? 48); + const frameDatas = splitAndProcess(decoded, { + square, + expectedFrames: frameCount, + threshold, + maxSize: targetSize, + }); + + log(`Split into ${frameDatas.length} frames`); + + const results: ForgeResult[] = []; + for (let i = 0; i < frameDatas.length; i++) { + const frame = frameDatas[i]!; + const name = names?.[i] ?? `frame-${i}`; + const framePath = `${outputPrefix}-${name}.png`; + const absPath = resolve(framePath); + + await mkdir(dirname(absPath), { recursive: true }); + const pngBuf = encodePNG(frame.width, frame.height, frame.pixels); + await writeFile(absPath, pngBuf); + + results.push({ + path: framePath, + width: frame.width, + height: frame.height, + size: pngBuf.length, + }); + + log(`Frame: ${framePath} (${frame.width}x${frame.height})`); + } + + return forgeResponse(results, { + prompt, + model: model ?? DEFAULT_MODEL, + frameCount: frameDatas.length, + }); + } catch (err) { + return errorResponse(err instanceof Error ? err : new Error(String(err))); + } +} diff --git a/src/tools/forge-background.ts b/src/tools/forge-background.ts new file mode 100644 index 0000000..b4c4a59 --- /dev/null +++ b/src/tools/forge-background.ts @@ -0,0 +1,87 @@ +import { writeFile, mkdir } from 'fs/promises'; +import { dirname, resolve } from 'path'; +import { generate } from '../engine/gemini.js'; +import { buildBackgroundPrompt } from '../pipeline/prompt-builder.js'; +import { forgeResponse, errorResponse } from '../utils/response-helpers.js'; +import { log } from '../utils/logger.js'; +import { MODEL_ALIASES, DEFAULT_MODEL } from '../engine/models.js'; +import { VALID_STYLES } from '../pipeline/prompt-builder.js'; +import type { McpToolResponse, Style, ForgeResult } from '../types/common.js'; + +export const forgeBackgroundTool = { + name: 'forge_background', + description: + 'Generate a pixel art game background. No post-processing — outputs the full image as-is, ready for use as a game scene background. Use appropriate aspect ratio for your game resolution.', + inputSchema: { + type: 'object', + properties: { + description: { + type: 'string', + description: + 'Background scene description (e.g. "deep space with stars and nebula", "forest clearing at night")', + }, + outputPath: { + type: 'string', + description: 'Output file path (e.g. "public/assets/games/invaders/bg.png")', + }, + aspect: { + type: 'string', + enum: ['1:1', '16:9', '9:16', '4:3', '3:4'], + description: + 'Aspect ratio matching your game resolution (e.g. 3:4 for 480x640 portrait game)', + }, + style: { + type: 'string', + enum: VALID_STYLES, + description: 'Visual style preset (default: clean)', + }, + model: { + type: 'string', + description: `Model alias or full ID. Aliases: ${MODEL_ALIASES.join(', ')} (default: ${DEFAULT_MODEL})`, + }, + }, + required: ['description', 'outputPath', 'aspect'], + }, +}; + +export async function handleForgeBackground(input: unknown): Promise { + try { + const args = input as Record; + const description = args.description as string; + const outputPath = args.outputPath as string; + const aspect = args.aspect as string; + const style = (args.style as Style) ?? 'clean'; + const model = args.model as string | undefined; + + const prompt = buildBackgroundPrompt(description, style, aspect); + log(`Prompt: ${prompt}`); + + const images = await generate({ prompt, model, aspect }); + const imgBuf = Buffer.from(images[0]!.b64, 'base64'); + + const absPath = resolve(outputPath); + await mkdir(dirname(absPath), { recursive: true }); + await writeFile(absPath, imgBuf); + + const result: ForgeResult = { + path: outputPath, + width: 0, + height: 0, + size: imgBuf.length, + }; + + try { + const { decodeImage } = await import('../pipeline/png.js'); + const decoded = decodeImage(imgBuf); + result.width = decoded.width; + result.height = decoded.height; + } catch { + // dimensions unknown, not critical + } + + log(`Saved: ${outputPath} (${result.width}x${result.height})`); + return forgeResponse([result], { prompt, model: model ?? DEFAULT_MODEL }); + } catch (err) { + return errorResponse(err instanceof Error ? err : new Error(String(err))); + } +} diff --git a/src/tools/forge-sprite.ts b/src/tools/forge-sprite.ts new file mode 100644 index 0000000..6a0d368 --- /dev/null +++ b/src/tools/forge-sprite.ts @@ -0,0 +1,113 @@ +import { writeFile, mkdir } from 'fs/promises'; +import { dirname, resolve } from 'path'; +import { generate } from '../engine/gemini.js'; +import { buildSpritePrompt } from '../pipeline/prompt-builder.js'; +import { decodeImage, encodePNG, detectFormat } from '../pipeline/png.js'; +import { detectBgColor, processSpriteColor, snapToPixelArtSize } from '../pipeline/image-ops.js'; +import { forgeResponse, errorResponse } from '../utils/response-helpers.js'; +import { log } from '../utils/logger.js'; +import { MODEL_ALIASES, DEFAULT_MODEL } from '../engine/models.js'; +import { VALID_STYLES } from '../pipeline/prompt-builder.js'; +import type { McpToolResponse, Style, ForgeResult } from '../types/common.js'; + +export const forgeSpriteTool = { + name: 'forge_sprite', + description: + 'Generate a single pixel art sprite. Handles prompt engineering, generation via Gemini, and post-processing (crop, background removal, pixelation downscale, square padding) automatically. Returns a clean, transparent PNG at proper pixel art resolution. Output size snaps to standard pixel art sizes (16, 24, 32, 48, 64, 96, 128).', + inputSchema: { + type: 'object', + properties: { + description: { + type: 'string', + description: + 'What the sprite is (e.g. "green slime enemy with horns", "cyan spaceship", "golden coin")', + }, + outputPath: { + type: 'string', + description: 'Output file path (e.g. "public/assets/games/rpg/slime.png")', + }, + size: { + type: 'number', + description: + 'Target sprite size in pixels (default: 48). Snaps to nearest standard size. Recommended: 16-24 for icons/projectiles/powerups, 32 for tiles/small sprites, 48 for game sprites/characters (PixelLab default), 64 for detailed characters, 96-128 for bosses/large objects. Set to 0 to skip downscale.', + }, + style: { + type: 'string', + enum: VALID_STYLES, + description: 'Visual style preset (default: clean)', + }, + background: { + type: 'string', + enum: ['black', 'white'], + description: 'Generation background color for extraction (default: black)', + }, + aspect: { + type: 'string', + enum: ['1:1', '16:9', '9:16', '4:3', '3:4'], + description: 'Aspect ratio (default: 1:1)', + }, + square: { + type: 'boolean', + description: 'Pad output to square (default: true)', + }, + model: { + type: 'string', + description: `Model alias or full ID. Aliases: ${MODEL_ALIASES.join(', ')} (default: ${DEFAULT_MODEL})`, + }, + references: { + type: 'array', + items: { type: 'string' }, + description: 'Paths to existing PNG/JPEG assets to match visual style.', + }, + }, + required: ['description', 'outputPath'], + }, +}; + +export async function handleForgeSprite(input: unknown): Promise { + try { + const args = input as Record; + const description = args.description as string; + const outputPath = args.outputPath as string; + const size = args.size as number | undefined; + const style = (args.style as Style) ?? 'clean'; + const bg = (args.background as 'black' | 'white') ?? 'black'; + const aspect = (args.aspect as string) ?? '1:1'; + const square = (args.square as boolean) ?? true; + const model = args.model as string | undefined; + const references = args.references as string[] | undefined; + + const targetSize = snapToPixelArtSize(size ?? 48); + const prompt = buildSpritePrompt(description, style, bg, targetSize); + log(`Prompt: ${prompt}`); + + const images = await generate({ prompt, model, aspect, references }); + const imgBuf = Buffer.from(images[0]!.b64, 'base64'); + const format = detectFormat(imgBuf); + const decoded = decodeImage(imgBuf); + const threshold = format === 'jpeg' ? 60 : 40; + + const bgColor = detectBgColor(decoded.pixels, decoded.width, decoded.height); + log( + `Raw image: ${decoded.width}x${decoded.height} (${format}, bg: rgb(${bgColor.r},${bgColor.g},${bgColor.b}), threshold: ${threshold})` + ); + const processed = processSpriteColor(decoded, bgColor, { square, threshold, size: targetSize }); + const pngBuf = encodePNG(processed.width, processed.height, processed.pixels); + + const absPath = resolve(outputPath); + await mkdir(dirname(absPath), { recursive: true }); + await writeFile(absPath, pngBuf); + + const result: ForgeResult = { + path: outputPath, + width: processed.width, + height: processed.height, + size: pngBuf.length, + }; + + log(`Saved: ${outputPath} (${result.width}x${result.height}, target: ${targetSize}px)`); + return forgeResponse([result], { prompt, model: model ?? DEFAULT_MODEL }); + } catch (err) { + return errorResponse(err instanceof Error ? err : new Error(String(err))); + } +} diff --git a/src/tools/forge-thumbnail.ts b/src/tools/forge-thumbnail.ts new file mode 100644 index 0000000..b00044a --- /dev/null +++ b/src/tools/forge-thumbnail.ts @@ -0,0 +1,93 @@ +import { writeFile, mkdir } from 'fs/promises'; +import { dirname, resolve } from 'path'; +import { generate } from '../engine/gemini.js'; +import { buildThumbnailPrompt } from '../pipeline/prompt-builder.js'; +import { decodeImage } from '../pipeline/png.js'; +import { forgeResponse, errorResponse } from '../utils/response-helpers.js'; +import { log } from '../utils/logger.js'; +import { MODEL_ALIASES, DEFAULT_MODEL } from '../engine/models.js'; +import { VALID_STYLES } from '../pipeline/prompt-builder.js'; +import type { McpToolResponse, Style, ForgeResult } from '../types/common.js'; + +export const forgeThumbnailTool = { + name: 'forge_thumbnail', + description: + 'Generate a pixel art game thumbnail/screenshot. Use reference images to match the visual style of actual in-game sprites. Best for store listings, game cards, and preview images.', + inputSchema: { + type: 'object', + properties: { + description: { + type: 'string', + description: + 'Scene description (e.g. "space shooter, cyan spaceship vs rows of colorful aliens, dark space")', + }, + outputPath: { + type: 'string', + description: 'Output file path (e.g. "public/assets/games/invaders/thumbnail.png")', + }, + references: { + type: 'array', + items: { type: 'string' }, + description: + 'Paths to actual game sprite PNGs — STRONGLY RECOMMENDED. The thumbnail will visually match these assets so the preview looks like the real game. Pass the main character, enemies, and key objects.', + }, + aspect: { + type: 'string', + enum: ['1:1', '16:9', '9:16', '4:3', '3:4'], + description: 'Aspect ratio (default: 4:3)', + }, + style: { + type: 'string', + enum: VALID_STYLES, + description: 'Visual style preset (default: clean)', + }, + model: { + type: 'string', + description: `Model alias or full ID. Aliases: ${MODEL_ALIASES.join(', ')} (default: ${DEFAULT_MODEL})`, + }, + }, + required: ['description', 'outputPath'], + }, +}; + +export async function handleForgeThumbnail(input: unknown): Promise { + try { + const args = input as Record; + const description = args.description as string; + const outputPath = args.outputPath as string; + const references = args.references as string[] | undefined; + const aspect = (args.aspect as string) ?? '4:3'; + const style = (args.style as Style) ?? 'clean'; + const model = args.model as string | undefined; + + const prompt = buildThumbnailPrompt(description, style); + log(`Prompt: ${prompt}`); + + const images = await generate({ prompt, model, aspect, references }); + const imgBuf = Buffer.from(images[0]!.b64, 'base64'); + + const absPath = resolve(outputPath); + await mkdir(dirname(absPath), { recursive: true }); + await writeFile(absPath, imgBuf); + + const result: ForgeResult = { + path: outputPath, + width: 0, + height: 0, + size: imgBuf.length, + }; + + try { + const decoded = decodeImage(imgBuf); + result.width = decoded.width; + result.height = decoded.height; + } catch { + // dimensions unknown + } + + log(`Saved: ${outputPath} (${result.width}x${result.height})`); + return forgeResponse([result], { prompt, model: model ?? DEFAULT_MODEL }); + } catch (err) { + return errorResponse(err instanceof Error ? err : new Error(String(err))); + } +} diff --git a/src/tools/index.ts b/src/tools/index.ts new file mode 100644 index 0000000..65f087a --- /dev/null +++ b/src/tools/index.ts @@ -0,0 +1,11 @@ +export { forgeSpriteTool, handleForgeSprite } from './forge-sprite.js'; + +export { forgeAnimationTool, handleForgeAnimation } from './forge-animation.js'; + +export { forgeBackgroundTool, handleForgeBackground } from './forge-background.js'; + +export { forgeThumbnailTool, handleForgeThumbnail } from './forge-thumbnail.js'; + +export { processSpriteTool, handleProcessSprite } from './process-sprite.js'; + +export { optimizeSpriteTool, handleOptimizeSprite } from './optimize-sprite.js'; diff --git a/src/tools/optimize-sprite.ts b/src/tools/optimize-sprite.ts new file mode 100644 index 0000000..fc495ae --- /dev/null +++ b/src/tools/optimize-sprite.ts @@ -0,0 +1,108 @@ +import { readFile, writeFile, mkdir } from 'fs/promises'; +import { dirname, resolve } from 'path'; +import { decodeImage, encodePNG } from '../pipeline/png.js'; +import { + detectBgColor, + processSpriteColor, + pixelateDownscale, + snapToPixelArtSize, +} from '../pipeline/image-ops.js'; +import { forgeResponse, errorResponse } from '../utils/response-helpers.js'; +import { log } from '../utils/logger.js'; +import type { McpToolResponse, ForgeResult } from '../types/common.js'; + +export const optimizeSpriteTool = { + name: 'optimize_sprite', + description: + 'Optimize an existing sprite PNG for pixel art games. Downscales large AI-generated sprites to proper pixel art resolution using area-averaging (not blurry bilinear). Also optionally removes background and crops. Use this to convert oversized "pixel art style" images into true pixel art at game-ready sizes.', + inputSchema: { + type: 'object', + properties: { + inputPath: { + type: 'string', + description: 'Path to the input PNG file', + }, + outputPath: { + type: 'string', + description: 'Output path (default: overwrites input)', + }, + size: { + type: 'number', + description: + 'Target size in pixels for the longest side. Snaps to nearest standard size (16, 24, 32, 48, 64, 96, 128). Recommended: 16-24 for icons/particles, 32 for tiles, 48 for sprites, 64 for characters, 96-128 for bosses.', + }, + removeBackground: { + type: 'boolean', + description: 'Auto-detect and remove background color (default: true)', + }, + square: { + type: 'boolean', + description: 'Pad output to square (default: false)', + }, + }, + required: ['inputPath', 'size'], + }, +}; + +export async function handleOptimizeSprite(input: unknown): Promise { + try { + const args = input as Record; + const inputPath = args.inputPath as string; + const outputPath = (args.outputPath as string) ?? inputPath; + const size = args.size as number; + const removeBg = (args.removeBackground as boolean) ?? true; + const square = (args.square as boolean) ?? false; + + const snappedSize = snapToPixelArtSize(size); + log(`Optimizing: ${inputPath} → ${snappedSize}px (requested: ${size})`); + + const buf = await readFile(resolve(inputPath)); + const img = decodeImage(buf); + log(`Input: ${img.width}x${img.height} (${buf.length} bytes)`); + + let { width, height, pixels } = img; + + if (removeBg) { + const bgColor = detectBgColor(pixels, width, height); + log(`Detected bg: rgb(${bgColor.r},${bgColor.g},${bgColor.b})`); + const processed = processSpriteColor(img, bgColor, { + threshold: 40, + square, + size: snappedSize, + }); + width = processed.width; + height = processed.height; + pixels = processed.pixels; + } else { + // Just downscale, no bg removal + if (snappedSize > 0 && (width > snappedSize || height > snappedSize)) { + const scale = snappedSize / Math.max(width, height); + const nw = Math.max(1, Math.round(width * scale)); + const nh = Math.max(1, Math.round(height * scale)); + const downscaled = pixelateDownscale(pixels, width, height, nw, nh); + width = downscaled.width; + height = downscaled.height; + pixels = downscaled.pixels; + } + } + + const pngBuf = encodePNG(width, height, pixels); + + const absPath = resolve(outputPath); + await mkdir(dirname(absPath), { recursive: true }); + await writeFile(absPath, pngBuf); + + const result: ForgeResult = { + path: outputPath, + width, + height, + size: pngBuf.length, + }; + + const savings = Math.round((1 - pngBuf.length / buf.length) * 100); + log(`Saved: ${outputPath} (${width}x${height}, ${pngBuf.length} bytes, ${savings}% smaller)`); + return forgeResponse([result], { originalSize: buf.length, savings: `${savings}%` }); + } catch (err) { + return errorResponse(err instanceof Error ? err : new Error(String(err))); + } +} diff --git a/src/tools/process-sprite.ts b/src/tools/process-sprite.ts new file mode 100644 index 0000000..b5e7097 --- /dev/null +++ b/src/tools/process-sprite.ts @@ -0,0 +1,132 @@ +import { readFile, writeFile, mkdir } from 'fs/promises'; +import { dirname, resolve } from 'path'; +import { decodeImage, encodePNG } from '../pipeline/png.js'; +import { processSprite, splitAndProcess } from '../pipeline/image-ops.js'; +import { forgeResponse, errorResponse } from '../utils/response-helpers.js'; +import { log } from '../utils/logger.js'; +import type { McpToolResponse, ForgeResult } from '../types/common.js'; + +export const processSpriteTool = { + name: 'process_sprite', + description: + 'Post-process an existing PNG image into a clean sprite. Handles background removal, auto-crop, square padding, and sprite sheet splitting. Use this for images from external sources that need processing.', + inputSchema: { + type: 'object', + properties: { + inputPath: { + type: 'string', + description: 'Path to the input PNG file', + }, + outputPath: { + type: 'string', + description: + 'Output path. For split mode: prefix for frame files (e.g. "assets/snake" → "assets/snake-head.png")', + }, + threshold: { + type: 'number', + description: 'Color detection threshold 0-255 (default: 20)', + }, + square: { + type: 'boolean', + description: 'Pad output to square (default: false)', + }, + padding: { + type: 'number', + description: 'Padding around content in pixels (default: 2)', + }, + split: { + type: 'boolean', + description: 'Split sprite sheet into individual sprites by detecting content boundaries', + }, + names: { + type: 'array', + items: { type: 'string' }, + description: 'Names for split sprites (default: 0, 1, 2, ...)', + }, + skipCrop: { + type: 'boolean', + description: 'Skip auto-crop step', + }, + skipTransparent: { + type: 'boolean', + description: 'Skip background removal step', + }, + }, + required: ['inputPath'], + }, +}; + +export async function handleProcessSprite(input: unknown): Promise { + try { + const args = input as Record; + const inputPath = args.inputPath as string; + const outputPath = args.outputPath as string | undefined; + const threshold = args.threshold as number | undefined; + const square = (args.square as boolean) ?? false; + const padding = args.padding as number | undefined; + const split = (args.split as boolean) ?? false; + const names = args.names as string[] | undefined; + const skipCrop = (args.skipCrop as boolean) ?? false; + const skipTransparent = (args.skipTransparent as boolean) ?? false; + + log(`Processing: ${inputPath}`); + + const buf = await readFile(resolve(inputPath)); + const img = decodeImage(buf); + log(`Input: ${img.width}x${img.height}`); + + if (split) { + const frames = splitAndProcess(img, { threshold, padding, square }); + log(`Split into ${frames.length} sprites`); + + const outBase = outputPath ?? inputPath.replace(/\.png$/, ''); + const results: ForgeResult[] = []; + + for (let i = 0; i < frames.length; i++) { + const frame = frames[i]!; + const name = names?.[i] ?? String(i); + const framePath = `${outBase}-${name}.png`; + const absPath = resolve(framePath); + + await mkdir(dirname(absPath), { recursive: true }); + const pngBuf = encodePNG(frame.width, frame.height, frame.pixels); + await writeFile(absPath, pngBuf); + + results.push({ + path: framePath, + width: frame.width, + height: frame.height, + size: pngBuf.length, + }); + } + + return forgeResponse(results); + } else { + const processed = processSprite(img, { + threshold, + padding, + square, + skipCrop, + skipTransparent, + }); + + const finalPath = outputPath ?? inputPath; + const absPath = resolve(finalPath); + await mkdir(dirname(absPath), { recursive: true }); + const pngBuf = encodePNG(processed.width, processed.height, processed.pixels); + await writeFile(absPath, pngBuf); + + const result: ForgeResult = { + path: finalPath, + width: processed.width, + height: processed.height, + size: pngBuf.length, + }; + + log(`Saved: ${finalPath} (${processed.width}x${processed.height})`); + return forgeResponse([result]); + } + } catch (err) { + return errorResponse(err instanceof Error ? err : new Error(String(err))); + } +} diff --git a/src/types/common.ts b/src/types/common.ts new file mode 100644 index 0000000..798c675 --- /dev/null +++ b/src/types/common.ts @@ -0,0 +1,35 @@ +export type McpContentItem = + | { type: 'text'; text: string; [key: string]: unknown } + | { type: 'image'; data: string; mimeType: string; [key: string]: unknown }; + +export interface McpToolResponse { + [key: string]: unknown; + content: McpContentItem[]; + isError?: boolean; +} + +export type Style = 'neon' | 'retro' | 'gameboy' | 'snes' | 'clean'; + +export interface ModelDef { + id: string; + engine: 'gemini'; + description: string; +} + +export interface GeneratedImage { + b64: string; + ext: string; +} + +export interface ImageData { + width: number; + height: number; + pixels: Buffer; +} + +export interface ForgeResult { + path: string; + width: number; + height: number; + size: number; +} diff --git a/src/utils/logger.ts b/src/utils/logger.ts new file mode 100644 index 0000000..ac1ed65 --- /dev/null +++ b/src/utils/logger.ts @@ -0,0 +1,11 @@ +export function log(message: string, ...args: unknown[]): void { + console.error(`[pixelforge] ${message}`, ...args); +} + +export function logError(message: string, error?: unknown): void { + if (error instanceof Error) { + console.error(`[pixelforge] ERROR: ${message}`, error.message); + } else { + console.error(`[pixelforge] ERROR: ${message}`, error); + } +} diff --git a/src/utils/response-helpers.ts b/src/utils/response-helpers.ts new file mode 100644 index 0000000..0e6dee9 --- /dev/null +++ b/src/utils/response-helpers.ts @@ -0,0 +1,22 @@ +import type { McpToolResponse, ForgeResult } from '../types/common.js'; + +export function errorResponse(error: Error | string): McpToolResponse { + const message = error instanceof Error ? error.message : error; + return { + content: [{ type: 'text', text: `Error: ${message}` }], + isError: true, + }; +} + +export function forgeResponse( + files: ForgeResult[], + meta?: Record +): McpToolResponse { + const result = { + files, + ...(meta ?? {}), + }; + return { + content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], + }; +} diff --git a/tests/unit/image-ops.test.ts b/tests/unit/image-ops.test.ts new file mode 100644 index 0000000..5bfb794 --- /dev/null +++ b/tests/unit/image-ops.test.ts @@ -0,0 +1,98 @@ +import { describe, it, expect } from 'vitest'; +import { + snapToPixelArtSize, + detectBgColor, + findSpriteBlobs, +} from '../../src/pipeline/image-ops.js'; + +describe('snapToPixelArtSize', () => { + it('snaps to nearest standard size', () => { + expect(snapToPixelArtSize(30)).toBe(32); + expect(snapToPixelArtSize(50)).toBe(48); + expect(snapToPixelArtSize(60)).toBe(64); + expect(snapToPixelArtSize(100)).toBe(96); + expect(snapToPixelArtSize(120)).toBe(128); + }); + + it('returns exact match for standard sizes', () => { + for (const size of [16, 24, 32, 48, 64, 96, 128]) { + expect(snapToPixelArtSize(size)).toBe(size); + } + }); + + it('returns 0 for zero or negative', () => { + expect(snapToPixelArtSize(0)).toBe(0); + expect(snapToPixelArtSize(-10)).toBe(0); + }); + + it('snaps small values to 16', () => { + expect(snapToPixelArtSize(1)).toBe(16); + expect(snapToPixelArtSize(10)).toBe(16); + }); +}); + +describe('detectBgColor', () => { + function makeImage(w: number, h: number, r: number, g: number, b: number): Buffer { + const buf = Buffer.alloc(w * h * 4); + for (let i = 0; i < w * h; i++) { + buf[i * 4] = r; + buf[i * 4 + 1] = g; + buf[i * 4 + 2] = b; + buf[i * 4 + 3] = 255; + } + return buf; + } + + it('detects black background', () => { + const bg = detectBgColor(makeImage(32, 32, 0, 0, 0), 32, 32); + expect(bg.r).toBe(0); + expect(bg.g).toBe(0); + expect(bg.b).toBe(0); + }); + + it('detects white background', () => { + const bg = detectBgColor(makeImage(32, 32, 255, 255, 255), 32, 32); + expect(bg.r).toBe(255); + expect(bg.g).toBe(255); + expect(bg.b).toBe(255); + }); + + it('detects colored background', () => { + const bg = detectBgColor(makeImage(32, 32, 100, 50, 200), 32, 32); + expect(bg.r).toBe(100); + expect(bg.g).toBe(50); + expect(bg.b).toBe(200); + }); +}); + +describe('findSpriteBlobs', () => { + it('finds a single blob on black bg', () => { + const w = 32; + const h = 32; + const pixels = Buffer.alloc(w * h * 4, 0); // all black + + // Draw a white 8x8 square in the center + for (let y = 12; y < 20; y++) { + for (let x = 12; x < 20; x++) { + const i = (y * w + x) * 4; + pixels[i] = 255; + pixels[i + 1] = 255; + pixels[i + 2] = 255; + pixels[i + 3] = 255; + } + } + + const blobs = findSpriteBlobs(pixels, w, h, 'black', 20, 4); + expect(blobs.length).toBe(1); + expect(blobs[0]!.x1).toBeGreaterThanOrEqual(12); + expect(blobs[0]!.x2).toBeLessThanOrEqual(20); + }); + + it('returns empty for uniform image', () => { + const w = 16; + const h = 16; + const pixels = Buffer.alloc(w * h * 4, 0); + const blobs = findSpriteBlobs(pixels, w, h, 'black', 20, 4); + expect(blobs.length).toBe(0); + }); +}); diff --git a/tests/unit/models.test.ts b/tests/unit/models.test.ts new file mode 100644 index 0000000..de0f860 --- /dev/null +++ b/tests/unit/models.test.ts @@ -0,0 +1,29 @@ +import { describe, it, expect } from 'vitest'; +import { resolveModel, DEFAULT_MODEL, MODELS, MODEL_ALIASES } from '../../src/engine/models.js'; + +describe('resolveModel', () => { + it('resolves known aliases', () => { + expect(resolveModel('nano-banana').id).toBe('nano-banana-pro-preview'); + expect(resolveModel('banana').id).toBe('nano-banana-pro-preview'); + expect(resolveModel('flash').id).toBe('gemini-3.1-flash-image-preview'); + expect(resolveModel('pro').id).toBe('gemini-3-pro-image-preview'); + expect(resolveModel('25').id).toBe('gemini-2.5-flash-image'); + }); + + it('falls back to default for unknown model', () => { + const result = resolveModel('nonexistent-model'); + expect(result.id).toBe(MODELS[DEFAULT_MODEL]!.id); + }); + + it('DEFAULT_MODEL is a valid key', () => { + expect(MODELS[DEFAULT_MODEL]).toBeDefined(); + }); + + it('all aliases resolve to valid models', () => { + for (const alias of MODEL_ALIASES) { + const model = resolveModel(alias); + expect(model.id).toBeTruthy(); + expect(model.engine).toBe('gemini'); + } + }); +}); diff --git a/tests/unit/png.test.ts b/tests/unit/png.test.ts new file mode 100644 index 0000000..d082aac --- /dev/null +++ b/tests/unit/png.test.ts @@ -0,0 +1,78 @@ +import { describe, it, expect } from 'vitest'; +import { encodePNG, decodePNG, detectFormat } from '../../src/pipeline/png.js'; + +describe('PNG encode/decode roundtrip', () => { + it('encodes and decodes a small image', () => { + const w = 4; + const h = 4; + const pixels = Buffer.alloc(w * h * 4); + + // Red pixel at (0,0) + pixels[0] = 255; + pixels[3] = 255; + // Green pixel at (1,0) + pixels[5] = 255; + pixels[7] = 255; + // Blue pixel at (0,1) + pixels[w * 4 + 2] = 255; + pixels[w * 4 + 3] = 255; + + const encoded = encodePNG(w, h, pixels); + expect(encoded).toBeInstanceOf(Buffer); + expect(encoded.length).toBeGreaterThan(0); + + const decoded = decodePNG(encoded); + expect(decoded.width).toBe(w); + expect(decoded.height).toBe(h); + expect(decoded.pixels.length).toBe(w * h * 4); + + // Verify red pixel + expect(decoded.pixels[0]).toBe(255); + expect(decoded.pixels[1]).toBe(0); + expect(decoded.pixels[2]).toBe(0); + expect(decoded.pixels[3]).toBe(255); + }); + + it('preserves transparency', () => { + const w = 2; + const h = 2; + const pixels = Buffer.alloc(w * h * 4); + // Transparent pixel + pixels[0] = 0; + pixels[1] = 0; + pixels[2] = 0; + pixels[3] = 0; + // Opaque white pixel + pixels[4] = 255; + pixels[5] = 255; + pixels[6] = 255; + pixels[7] = 255; + + const encoded = encodePNG(w, h, pixels); + const decoded = decodePNG(encoded); + expect(decoded.pixels[3]).toBe(0); + expect(decoded.pixels[7]).toBe(255); + }); +}); + +describe('detectFormat', () => { + it('detects PNG', () => { + const png = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10, 0, 0]); + expect(detectFormat(png)).toBe('png'); + }); + + it('detects JPEG', () => { + const jpeg = Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0x00]); + expect(detectFormat(jpeg)).toBe('jpeg'); + }); + + it('detects WebP', () => { + const webp = Buffer.from('RIFF____WEBP', 'ascii'); + expect(detectFormat(webp)).toBe('webp'); + }); + + it('returns unknown for random data', () => { + const random = Buffer.from([1, 2, 3, 4, 5, 6, 7, 8]); + expect(detectFormat(random)).toBe('unknown'); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..9b36d9f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "lib": ["ES2022"], + "moduleResolution": "node", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "./dist", + "rootDir": "./src", + "removeComments": true, + "noEmitOnError": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/tsup.config.ts b/tsup.config.ts new file mode 100644 index 0000000..524096b --- /dev/null +++ b/tsup.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: ['src/index.ts'], + outDir: 'dist', + format: ['esm'], + target: 'node20', + bundle: true, + minify: false, + sourcemap: false, + clean: true, + dts: false, + platform: 'node', + splitting: false, + noExternal: ['@modelcontextprotocol/sdk', 'zod', 'jpeg-js'], +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..fea9e0f --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + include: ['tests/**/*.test.ts'], + coverage: { + provider: 'v8', + reporter: ['text', 'json', 'html'], + include: ['src/**/*.ts'], + exclude: ['src/index.ts'], + }, + }, +});