commit 7c510c737c4ae6524aca4f53a17cc3f367d83bb0 Author: sunlei Date: Wed May 20 19:14:51 2026 +0800 Initial commit diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..85075a6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules +dist +*.log +.env +.DS_Store diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6d0846e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 sunlei + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ed7d823 --- /dev/null +++ b/README.md @@ -0,0 +1,155 @@ +# Figma Local Context MCP + +一个本地 `.fig` 文件 MCP Server。它结合了当前项目里的 `.fig` 解码、节点 SVG/PNG 导出能力,以及 `GLips/Figma-Context-MCP` 的 MCP 工具注册方式。 + +这个项目不依赖 Figma REST API,也不需要 `FIGMA_API_KEY`。MCP 客户端只需要传入本机 `.fig` 或 `.fig.json` 路径即可。 + +## 工具 + +- `get_design_context`:官方 Figma MCP 风格的主入口,返回节点树、样式、tokens 和代码提示。 +- `get_code_context`:返回更适合代码生成的布局、样式和导出资源提示。 +- `export_assets`:批量导出多个节点为 SVG 或 PNG。 +- `list_fig_nodes`:按名称、类型或 node-id 搜索节点。 +- `get_design_tokens`:从全文件或指定节点子树中推导颜色、渐变、阴影、描边 token。 +- `inspect_fig_file`:读取本地 `.fig`,返回节点数量、类型统计和节点概览。 +- `get_fig_node`:按节点名、`2625:12945`、`2625-12945`、`node-id=2625-12945` 或完整 Figma 链接查找节点,并返回简化上下文。 +- `export_fig_node`:把指定节点导出为 SVG 或 PNG,PNG 支持倍率。 + +## 示例参数 + +获取官方风格设计上下文: + +```json +{ + "filePath": "D:\\MyFiles\\Download\\兴泰安全生产预计平台-朱梓鑫.fig", + "nodeQuery": "node-id=2625-12945", + "depth": 2, + "includeTokens": true, + "includeCodeHints": true +} +``` + +获取代码生成上下文: + +```json +{ + "filePath": "D:\\MyFiles\\Download\\兴泰安全生产预计平台-朱梓鑫.fig", + "nodeQuery": "2625-12945", + "depth": 2 +} +``` + +搜索节点: + +```json +{ + "filePath": "D:\\MyFiles\\Download\\兴泰安全生产预计平台-朱梓鑫.fig", + "query": "Group 1321315187", + "limit": 20 +} +``` + +批量导出资源: + +```json +{ + "filePath": "D:\\MyFiles\\Download\\兴泰安全生产预计平台-朱梓鑫.fig", + "nodeQueries": ["2625-12945", "Group 1321315187"], + "outputDir": "D:\\MyFiles\\Download\\fig-export-debug\\assets", + "format": "png", + "scale": 2 +} +``` + +获取设计 token: + +```json +{ + "filePath": "D:\\MyFiles\\Download\\兴泰安全生产预计平台-朱梓鑫.fig", + "nodeQuery": "2625-12945" +} +``` + +底层文件概览: + +```json +{ + "filePath": "D:\\MyFiles\\Download\\兴泰安全生产预计平台-朱梓鑫.fig", + "maxNodes": 20 +} +``` + +底层单节点查询: + +```json +{ + "filePath": "D:\\MyFiles\\Download\\兴泰安全生产预计平台-朱梓鑫.fig", + "nodeQuery": "node-id=2625-12945", + "depth": 1 +} +``` + +底层单节点导出: + +```json +{ + "filePath": "D:\\MyFiles\\Download\\兴泰安全生产预计平台-朱梓鑫.fig", + "nodeQuery": "2625-12945", + "outputPath": "D:\\MyFiles\\Download\\fig-export-debug\\mcp-export-node.png", + "format": "png", + "scale": 2 +} +``` + +## 开发 + +```bash +pnpm install +pnpm typecheck +pnpm build +``` + +本地调试: + +```bash +pnpm dev +``` + +MCP 客户端配置示例: + +```json +{ + "mcpServers": { + "Figma Local Context": { + "command": "cmd", + "args": ["/c", "pnpm", "--dir", "D:\\MyFiles\\Codes\\Node\\figma-local-context-mcp", "dev"] + } + } +} +``` + +构建后也可以使用: + +```json +{ + "mcpServers": { + "Figma Local Context": { + "command": "cmd", + "args": ["/c", "node", "D:\\MyFiles\\Codes\\Node\\figma-local-context-mcp\\dist\\bin.js", "--stdio"] + } + } +} +``` + +## 引用与致谢 + +本项目在实现过程中参考并复用了以下开源项目的思路或能力: + +- [GLips/Figma-Context-MCP](https://github.com/GLips/Figma-Context-MCP):参考它的 MCP Server 组织方式、工具注册结构和官方 Figma MCP 风格的上下文输出思路。该仓库根目录提供 MIT License,`package.json` 也声明为 MIT。 +- [yagudaev/figma-to-json](https://github.com/yagudaev/figma-to-json):参考它对 `.fig` 文件读写和解码的实现方向,并在本项目中继续扩展本地节点检索、SVG/PNG 导出和 MCP 上下文能力。该仓库根 `package.json` 声明 `license: MIT`;当前主分支没有单独的根 LICENSE 文件。 + +## 许可证兼容性 + +上面两个引用仓库均声明为 MIT 许可证。MIT 许可证允许使用、复制、修改、合并、发布、分发、再许可和销售软件副本,因此支持本项目的私有使用、商业使用、二次开发和发布。需要遵守的核心条件是:分发包含这些项目的代码或其重要部分时,应保留对应的版权声明和许可声明。 + +本项目同样使用 MIT License,见 [LICENSE](./LICENSE)。 diff --git a/package.json b/package.json new file mode 100644 index 0000000..ea4a26a --- /dev/null +++ b/package.json @@ -0,0 +1,59 @@ +{ + "name": "figma-local-context-mcp", + "version": "0.1.0", + "description": "Local MCP server for reading .fig files, inspecting nodes, and exporting SVG/PNG assets without the Figma API.", + "type": "module", + "main": "dist/index.js", + "bin": { + "figma-local-context-mcp": "dist/bin.js" + }, + "files": [ + "dist", + "README.md", + "server.json" + ], + "scripts": { + "build": "tsup --dts", + "typecheck": "tsc --noEmit", + "start": "node dist/bin.js --stdio", + "dev": "tsx src/bin.ts --stdio", + "inspect": "pnpx @modelcontextprotocol/inspector" + }, + "engines": { + "node": ">=20.20.0" + }, + "packageManager": "pnpm@10.33.0", + "keywords": [ + "figma", + "mcp", + "fig", + "typescript" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/KwiTsukasa/figma-local-context-mcp.git" + }, + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "1.29.0", + "@resvg/resvg-js": "^2.6.2", + "cleye": "^2.2.1", + "fzstd": "^0.1.1", + "kiwi-schema": "^0.5.0", + "uzip": "^0.20201231.0", + "zod": "^3.25.76" + }, + "devDependencies": { + "@types/node": "^25.3.3", + "@types/uzip": "^0.20201231.0", + "tsup": "^8.5.1", + "tsx": "^4.22.3", + "typescript": "^5.7.3" + }, + "pnpm": { + "onlyBuiltDependencies": [ + "@resvg/resvg-js", + "esbuild" + ] + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..cef318f --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,2143 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@modelcontextprotocol/sdk': + specifier: 1.29.0 + version: 1.29.0(zod@3.25.76) + '@resvg/resvg-js': + specifier: ^2.6.2 + version: 2.6.2 + cleye: + specifier: ^2.2.1 + version: 2.6.0 + fzstd: + specifier: ^0.1.1 + version: 0.1.1 + kiwi-schema: + specifier: ^0.5.0 + version: 0.5.0 + uzip: + specifier: ^0.20201231.0 + version: 0.20201231.0 + zod: + specifier: ^3.25.76 + version: 3.25.76 + devDependencies: + '@types/node': + specifier: ^25.3.3 + version: 25.9.1 + '@types/uzip': + specifier: ^0.20201231.0 + version: 0.20201231.2 + tsup: + specifier: ^8.5.1 + version: 8.5.1(tsx@4.22.3)(typescript@5.9.3) + tsx: + specifier: ^4.22.3 + version: 4.22.3 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + +packages: + + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.28.0': + resolution: {integrity: sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.28.0': + resolution: {integrity: sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.28.0': + resolution: {integrity: sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.28.0': + resolution: {integrity: sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.28.0': + resolution: {integrity: sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.0': + resolution: {integrity: sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.28.0': + resolution: {integrity: sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.0': + resolution: {integrity: sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.28.0': + resolution: {integrity: sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.28.0': + resolution: {integrity: sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.28.0': + resolution: {integrity: sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.28.0': + resolution: {integrity: sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.28.0': + resolution: {integrity: sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.28.0': + resolution: {integrity: sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.0': + resolution: {integrity: sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.28.0': + resolution: {integrity: sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.28.0': + resolution: {integrity: sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-arm64@0.28.0': + resolution: {integrity: sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.0': + resolution: {integrity: sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-arm64@0.28.0': + resolution: {integrity: sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.0': + resolution: {integrity: sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/openharmony-arm64@0.28.0': + resolution: {integrity: sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.28.0': + resolution: {integrity: sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.28.0': + resolution: {integrity: sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.28.0': + resolution: {integrity: sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.28.0': + resolution: {integrity: sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@hono/node-server@1.19.14': + resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@modelcontextprotocol/sdk@1.29.0': + resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + + '@resvg/resvg-js-android-arm-eabi@2.6.2': + resolution: {integrity: sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [android] + + '@resvg/resvg-js-android-arm64@2.6.2': + resolution: {integrity: sha512-VcOKezEhm2VqzXpcIJoITuvUS/fcjIw5NA/w3tjzWyzmvoCdd+QXIqy3FBGulWdClvp4g+IfUemigrkLThSjAQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@resvg/resvg-js-darwin-arm64@2.6.2': + resolution: {integrity: sha512-nmok2LnAd6nLUKI16aEB9ydMC6Lidiiq2m1nEBDR1LaaP7FGs4AJ90qDraxX+CWlVuRlvNjyYJTNv8qFjtL9+A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@resvg/resvg-js-darwin-x64@2.6.2': + resolution: {integrity: sha512-GInyZLjgWDfsVT6+SHxQVRwNzV0AuA1uqGsOAW+0th56J7Nh6bHHKXHBWzUrihxMetcFDmQMAX1tZ1fZDYSRsw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@resvg/resvg-js-linux-arm-gnueabihf@2.6.2': + resolution: {integrity: sha512-YIV3u/R9zJbpqTTNwTZM5/ocWetDKGsro0SWp70eGEM9eV2MerWyBRZnQIgzU3YBnSBQ1RcxRZvY/UxwESfZIw==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@resvg/resvg-js-linux-arm64-gnu@2.6.2': + resolution: {integrity: sha512-zc2BlJSim7YR4FZDQ8OUoJg5holYzdiYMeobb9pJuGDidGL9KZUv7SbiD4E8oZogtYY42UZEap7dqkkYuA91pg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@resvg/resvg-js-linux-arm64-musl@2.6.2': + resolution: {integrity: sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@resvg/resvg-js-linux-x64-gnu@2.6.2': + resolution: {integrity: sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@resvg/resvg-js-linux-x64-musl@2.6.2': + resolution: {integrity: sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@resvg/resvg-js-win32-arm64-msvc@2.6.2': + resolution: {integrity: sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@resvg/resvg-js-win32-ia32-msvc@2.6.2': + resolution: {integrity: sha512-har4aPAlvjnLcil40AC77YDIk6loMawuJwFINEM7n0pZviwMkMvjb2W5ZirsNOZY4aDbo5tLx0wNMREp5Brk+w==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@resvg/resvg-js-win32-x64-msvc@2.6.2': + resolution: {integrity: sha512-ZXtYhtUr5SSaBrUDq7DiyjOFJqBVL/dOBN7N/qmi/pO0IgiWW/f/ue3nbvu9joWE5aAKDoIzy/CxsY0suwGosQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@resvg/resvg-js@2.6.2': + resolution: {integrity: sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q==} + engines: {node: '>= 10'} + + '@rollup/rollup-android-arm-eabi@4.60.4': + resolution: {integrity: sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.60.4': + resolution: {integrity: sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.60.4': + resolution: {integrity: sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.60.4': + resolution: {integrity: sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.60.4': + resolution: {integrity: sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.60.4': + resolution: {integrity: sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.60.4': + resolution: {integrity: sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.60.4': + resolution: {integrity: sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.60.4': + resolution: {integrity: sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.60.4': + resolution: {integrity: sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.60.4': + resolution: {integrity: sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.60.4': + resolution: {integrity: sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.60.4': + resolution: {integrity: sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.60.4': + resolution: {integrity: sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.60.4': + resolution: {integrity: sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.60.4': + resolution: {integrity: sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.60.4': + resolution: {integrity: sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.60.4': + resolution: {integrity: sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.60.4': + resolution: {integrity: sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.60.4': + resolution: {integrity: sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.60.4': + resolution: {integrity: sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.60.4': + resolution: {integrity: sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.60.4': + resolution: {integrity: sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.60.4': + resolution: {integrity: sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.60.4': + resolution: {integrity: sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==} + cpu: [x64] + os: [win32] + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/node@25.9.1': + resolution: {integrity: sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==} + + '@types/uzip@0.20201231.2': + resolution: {integrity: sha512-l8n/uaJDoxv2hMMWKCaq6l/umbCZjq6CMr8z2wuteB7lKoIwHrIlMZ3AwAuc+olmOK1AobfvK1PPfr5wl7lkWQ==} + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + + bundle-require@5.1.0: + resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.18' + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + cleye@2.6.0: + resolution: {integrity: sha512-u0SQCsega/ox+2GSuUlG6wvA9c2FtH8sPmv9G9Q3JRTs7FK6+LtaziRAQgx7lrJ1J7bOd3palhwgZKMg8R6JbQ==} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + content-disposition@1.1.0: + resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + content-type@2.0.0: + resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} + engines: {node: '>=18'} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} + hasBin: true + + esbuild@0.28.0: + resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==} + engines: {node: '>=18'} + hasBin: true + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + eventsource-parser@3.0.8: + resolution: {integrity: sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + + express-rate-limit@8.5.2: + resolution: {integrity: sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-uri@3.1.2: + resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + + fix-dts-default-cjs-exports@1.0.1: + resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + fzstd@0.1.1: + resolution: {integrity: sha512-dkuVSOKKwh3eas5VkJy1AW1vFpet8TA/fGmVA5krThl8YcOVE/8ZIoEA1+U1vEn5ckxxhLirSdY837azmbaNHA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + hasown@2.0.3: + resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} + engines: {node: '>= 0.4'} + + hono@4.12.21: + resolution: {integrity: sha512-uV63apnb0kyPtAUwoWgaGh9HyIFcv8lgmzPZSiTBQAFOFGIzka5EZ1dZocmGnn0XdX0+XTqJ6Tqv7selMuGLRQ==} + engines: {node: '>=16.9.0'} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ip-address@10.2.0: + resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==} + engines: {node: '>= 12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jose@6.2.3: + resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} + + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + + kiwi-schema@0.5.0: + resolution: {integrity: sha512-X+FpfU0yTEtc6aTHS7VwbOpvQwRt70+pXXWRI5fd6CvWhe7pSVC854TVo4Zo0x5/wwcWj+/9KUlXpdcP0dY9AA==} + hasBin: true + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + mlly@1.8.2: + resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-to-regexp@8.4.2: + resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + 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 + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + qs@6.15.2: + resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} + engines: {node: '>=0.6'} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + rollup@4.60.4: + resolution: {integrity: sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + terminal-columns@2.0.0: + resolution: {integrity: sha512-6IByuUjyNZJXUtwDNm+OIe62zgwwaRbH+WMNTcx05O2G5V9WhvluAAHJY8OvUdwmzMPpqAD/7EUpGdI6ae1aiQ==} + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + tsup@8.5.1: + resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} + engines: {node: '>=18'} + hasBin: true + 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 + + tsx@4.22.3: + resolution: {integrity: sha512-mdoNxBC/cSQObGGVQ5Bpn5i+yv7j68gk3Nfm3wFjcJg3Z0Mix9jzAFfP12prmm5eVGmDKtp0yyArrs0Q+8gZHg==} + engines: {node: '>=18.0.0'} + hasBin: true + + type-flag@4.3.0: + resolution: {integrity: sha512-XwZWK+SOWleva2EjynfJT6tXHUkS+/TZAIc5fTJXVMArNf/5jBEt7Y/yBtGcxLFThtQCQ9v6nxXon0QZ86ZJhA==} + + type-is@2.1.0: + resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} + engines: {node: '>= 18'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.4: + resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} + + undici-types@7.24.6: + resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + uzip@0.20201231.0: + resolution: {integrity: sha512-OZeJfZP+R0z9D6TmBgLq2LHzSSptGMGDGigGiEe0pr8UBe/7fdflgHlHBNDASTXB5jnFuxHpNaJywSg8YFeGng==} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + zod-to-json-schema@3.25.2: + resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} + peerDependencies: + zod: ^3.25.28 || ^4 + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + +snapshots: + + '@esbuild/aix-ppc64@0.27.7': + optional: true + + '@esbuild/aix-ppc64@0.28.0': + optional: true + + '@esbuild/android-arm64@0.27.7': + optional: true + + '@esbuild/android-arm64@0.28.0': + optional: true + + '@esbuild/android-arm@0.27.7': + optional: true + + '@esbuild/android-arm@0.28.0': + optional: true + + '@esbuild/android-x64@0.27.7': + optional: true + + '@esbuild/android-x64@0.28.0': + optional: true + + '@esbuild/darwin-arm64@0.27.7': + optional: true + + '@esbuild/darwin-arm64@0.28.0': + optional: true + + '@esbuild/darwin-x64@0.27.7': + optional: true + + '@esbuild/darwin-x64@0.28.0': + optional: true + + '@esbuild/freebsd-arm64@0.27.7': + optional: true + + '@esbuild/freebsd-arm64@0.28.0': + optional: true + + '@esbuild/freebsd-x64@0.27.7': + optional: true + + '@esbuild/freebsd-x64@0.28.0': + optional: true + + '@esbuild/linux-arm64@0.27.7': + optional: true + + '@esbuild/linux-arm64@0.28.0': + optional: true + + '@esbuild/linux-arm@0.27.7': + optional: true + + '@esbuild/linux-arm@0.28.0': + optional: true + + '@esbuild/linux-ia32@0.27.7': + optional: true + + '@esbuild/linux-ia32@0.28.0': + optional: true + + '@esbuild/linux-loong64@0.27.7': + optional: true + + '@esbuild/linux-loong64@0.28.0': + optional: true + + '@esbuild/linux-mips64el@0.27.7': + optional: true + + '@esbuild/linux-mips64el@0.28.0': + optional: true + + '@esbuild/linux-ppc64@0.27.7': + optional: true + + '@esbuild/linux-ppc64@0.28.0': + optional: true + + '@esbuild/linux-riscv64@0.27.7': + optional: true + + '@esbuild/linux-riscv64@0.28.0': + optional: true + + '@esbuild/linux-s390x@0.27.7': + optional: true + + '@esbuild/linux-s390x@0.28.0': + optional: true + + '@esbuild/linux-x64@0.27.7': + optional: true + + '@esbuild/linux-x64@0.28.0': + optional: true + + '@esbuild/netbsd-arm64@0.27.7': + optional: true + + '@esbuild/netbsd-arm64@0.28.0': + optional: true + + '@esbuild/netbsd-x64@0.27.7': + optional: true + + '@esbuild/netbsd-x64@0.28.0': + optional: true + + '@esbuild/openbsd-arm64@0.27.7': + optional: true + + '@esbuild/openbsd-arm64@0.28.0': + optional: true + + '@esbuild/openbsd-x64@0.27.7': + optional: true + + '@esbuild/openbsd-x64@0.28.0': + optional: true + + '@esbuild/openharmony-arm64@0.27.7': + optional: true + + '@esbuild/openharmony-arm64@0.28.0': + optional: true + + '@esbuild/sunos-x64@0.27.7': + optional: true + + '@esbuild/sunos-x64@0.28.0': + optional: true + + '@esbuild/win32-arm64@0.27.7': + optional: true + + '@esbuild/win32-arm64@0.28.0': + optional: true + + '@esbuild/win32-ia32@0.27.7': + optional: true + + '@esbuild/win32-ia32@0.28.0': + optional: true + + '@esbuild/win32-x64@0.27.7': + optional: true + + '@esbuild/win32-x64@0.28.0': + optional: true + + '@hono/node-server@1.19.14(hono@4.12.21)': + dependencies: + hono: 4.12.21 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@modelcontextprotocol/sdk@1.29.0(zod@3.25.76)': + dependencies: + '@hono/node-server': 1.19.14(hono@4.12.21) + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.8 + express: 5.2.1 + express-rate-limit: 8.5.2(express@5.2.1) + hono: 4.12.21 + jose: 6.2.3 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 3.25.76 + zod-to-json-schema: 3.25.2(zod@3.25.76) + transitivePeerDependencies: + - supports-color + + '@resvg/resvg-js-android-arm-eabi@2.6.2': + optional: true + + '@resvg/resvg-js-android-arm64@2.6.2': + optional: true + + '@resvg/resvg-js-darwin-arm64@2.6.2': + optional: true + + '@resvg/resvg-js-darwin-x64@2.6.2': + optional: true + + '@resvg/resvg-js-linux-arm-gnueabihf@2.6.2': + optional: true + + '@resvg/resvg-js-linux-arm64-gnu@2.6.2': + optional: true + + '@resvg/resvg-js-linux-arm64-musl@2.6.2': + optional: true + + '@resvg/resvg-js-linux-x64-gnu@2.6.2': + optional: true + + '@resvg/resvg-js-linux-x64-musl@2.6.2': + optional: true + + '@resvg/resvg-js-win32-arm64-msvc@2.6.2': + optional: true + + '@resvg/resvg-js-win32-ia32-msvc@2.6.2': + optional: true + + '@resvg/resvg-js-win32-x64-msvc@2.6.2': + optional: true + + '@resvg/resvg-js@2.6.2': + optionalDependencies: + '@resvg/resvg-js-android-arm-eabi': 2.6.2 + '@resvg/resvg-js-android-arm64': 2.6.2 + '@resvg/resvg-js-darwin-arm64': 2.6.2 + '@resvg/resvg-js-darwin-x64': 2.6.2 + '@resvg/resvg-js-linux-arm-gnueabihf': 2.6.2 + '@resvg/resvg-js-linux-arm64-gnu': 2.6.2 + '@resvg/resvg-js-linux-arm64-musl': 2.6.2 + '@resvg/resvg-js-linux-x64-gnu': 2.6.2 + '@resvg/resvg-js-linux-x64-musl': 2.6.2 + '@resvg/resvg-js-win32-arm64-msvc': 2.6.2 + '@resvg/resvg-js-win32-ia32-msvc': 2.6.2 + '@resvg/resvg-js-win32-x64-msvc': 2.6.2 + + '@rollup/rollup-android-arm-eabi@4.60.4': + optional: true + + '@rollup/rollup-android-arm64@4.60.4': + optional: true + + '@rollup/rollup-darwin-arm64@4.60.4': + optional: true + + '@rollup/rollup-darwin-x64@4.60.4': + optional: true + + '@rollup/rollup-freebsd-arm64@4.60.4': + optional: true + + '@rollup/rollup-freebsd-x64@4.60.4': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.60.4': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.60.4': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-x64-musl@4.60.4': + optional: true + + '@rollup/rollup-openbsd-x64@4.60.4': + optional: true + + '@rollup/rollup-openharmony-arm64@4.60.4': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.60.4': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.60.4': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.60.4': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.60.4': + optional: true + + '@types/estree@1.0.8': {} + + '@types/node@25.9.1': + dependencies: + undici-types: 7.24.6 + + '@types/uzip@0.20201231.2': {} + + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + + acorn@8.16.0: {} + + ajv-formats@3.0.1(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.2 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + any-promise@1.3.0: {} + + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.15.2 + raw-body: 3.0.2 + type-is: 2.1.0 + transitivePeerDependencies: + - supports-color + + bundle-require@5.1.0(esbuild@0.27.7): + dependencies: + esbuild: 0.27.7 + load-tsconfig: 0.2.5 + + bytes@3.1.2: {} + + cac@6.7.14: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + cleye@2.6.0: + dependencies: + terminal-columns: 2.0.0 + type-flag: 4.3.0 + + commander@4.1.1: {} + + confbox@0.1.8: {} + + consola@3.4.2: {} + + content-disposition@1.1.0: {} + + content-type@1.0.5: {} + + content-type@2.0.0: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + depd@2.0.0: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ee-first@1.1.1: {} + + encodeurl@2.0.0: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + esbuild@0.27.7: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 + + esbuild@0.28.0: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.0 + '@esbuild/android-arm': 0.28.0 + '@esbuild/android-arm64': 0.28.0 + '@esbuild/android-x64': 0.28.0 + '@esbuild/darwin-arm64': 0.28.0 + '@esbuild/darwin-x64': 0.28.0 + '@esbuild/freebsd-arm64': 0.28.0 + '@esbuild/freebsd-x64': 0.28.0 + '@esbuild/linux-arm': 0.28.0 + '@esbuild/linux-arm64': 0.28.0 + '@esbuild/linux-ia32': 0.28.0 + '@esbuild/linux-loong64': 0.28.0 + '@esbuild/linux-mips64el': 0.28.0 + '@esbuild/linux-ppc64': 0.28.0 + '@esbuild/linux-riscv64': 0.28.0 + '@esbuild/linux-s390x': 0.28.0 + '@esbuild/linux-x64': 0.28.0 + '@esbuild/netbsd-arm64': 0.28.0 + '@esbuild/netbsd-x64': 0.28.0 + '@esbuild/openbsd-arm64': 0.28.0 + '@esbuild/openbsd-x64': 0.28.0 + '@esbuild/openharmony-arm64': 0.28.0 + '@esbuild/sunos-x64': 0.28.0 + '@esbuild/win32-arm64': 0.28.0 + '@esbuild/win32-ia32': 0.28.0 + '@esbuild/win32-x64': 0.28.0 + + escape-html@1.0.3: {} + + etag@1.8.1: {} + + eventsource-parser@3.0.8: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.8 + + express-rate-limit@8.5.2(express@5.2.1): + dependencies: + express: 5.2.1 + ip-address: 10.2.0 + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.1.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.2 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.1.0 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + fast-deep-equal@3.1.3: {} + + fast-uri@3.1.2: {} + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + fix-dts-default-cjs-exports@1.0.1: + dependencies: + magic-string: 0.30.21 + mlly: 1.8.2 + rollup: 4.60.4 + + forwarded@0.2.0: {} + + fresh@2.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + fzstd@0.1.1: {} + + get-intrinsic@1.3.0: + 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.3 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + gopd@1.2.0: {} + + has-symbols@1.1.0: {} + + hasown@2.0.3: + dependencies: + function-bind: 1.1.2 + + hono@4.12.21: {} + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + + inherits@2.0.4: {} + + ip-address@10.2.0: {} + + ipaddr.js@1.9.1: {} + + is-promise@4.0.0: {} + + isexe@2.0.0: {} + + jose@6.2.3: {} + + joycon@3.1.1: {} + + json-schema-traverse@1.0.0: {} + + json-schema-typed@8.0.2: {} + + kiwi-schema@0.5.0: {} + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + load-tsconfig@0.2.5: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + math-intrinsics@1.1.0: {} + + media-typer@1.1.0: {} + + merge-descriptors@2.0.0: {} + + mime-db@1.54.0: {} + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + mlly@1.8.2: + dependencies: + acorn: 8.16.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.4 + + ms@2.1.3: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + negotiator@1.0.0: {} + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + parseurl@1.3.3: {} + + path-key@3.1.1: {} + + path-to-regexp@8.4.2: {} + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@4.0.4: {} + + pirates@4.0.7: {} + + pkce-challenge@5.0.1: {} + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.2 + pathe: 2.0.3 + + postcss-load-config@6.0.1(tsx@4.22.3): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + tsx: 4.22.3 + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + qs@6.15.2: + dependencies: + side-channel: 1.1.0 + + range-parser@1.2.1: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + + readdirp@4.1.2: {} + + require-from-string@2.0.2: {} + + resolve-from@5.0.0: {} + + rollup@4.60.4: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.60.4 + '@rollup/rollup-android-arm64': 4.60.4 + '@rollup/rollup-darwin-arm64': 4.60.4 + '@rollup/rollup-darwin-x64': 4.60.4 + '@rollup/rollup-freebsd-arm64': 4.60.4 + '@rollup/rollup-freebsd-x64': 4.60.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.4 + '@rollup/rollup-linux-arm-musleabihf': 4.60.4 + '@rollup/rollup-linux-arm64-gnu': 4.60.4 + '@rollup/rollup-linux-arm64-musl': 4.60.4 + '@rollup/rollup-linux-loong64-gnu': 4.60.4 + '@rollup/rollup-linux-loong64-musl': 4.60.4 + '@rollup/rollup-linux-ppc64-gnu': 4.60.4 + '@rollup/rollup-linux-ppc64-musl': 4.60.4 + '@rollup/rollup-linux-riscv64-gnu': 4.60.4 + '@rollup/rollup-linux-riscv64-musl': 4.60.4 + '@rollup/rollup-linux-s390x-gnu': 4.60.4 + '@rollup/rollup-linux-x64-gnu': 4.60.4 + '@rollup/rollup-linux-x64-musl': 4.60.4 + '@rollup/rollup-openbsd-x64': 4.60.4 + '@rollup/rollup-openharmony-arm64': 4.60.4 + '@rollup/rollup-win32-arm64-msvc': 4.60.4 + '@rollup/rollup-win32-ia32-msvc': 4.60.4 + '@rollup/rollup-win32-x64-gnu': 4.60.4 + '@rollup/rollup-win32-x64-msvc': 4.60.4 + fsevents: 2.3.3 + + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.4.2 + transitivePeerDependencies: + - supports-color + + safer-buffer@2.1.2: {} + + send@1.2.1: + 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 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + source-map@0.7.6: {} + + statuses@2.0.2: {} + + sucrase@3.35.1: + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + tinyglobby: 0.2.16 + ts-interface-checker: 0.1.13 + + terminal-columns@2.0.0: {} + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + tinyexec@0.3.2: {} + + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + toidentifier@1.0.1: {} + + tree-kill@1.2.2: {} + + ts-interface-checker@0.1.13: {} + + tsup@8.5.1(tsx@4.22.3)(typescript@5.9.3): + dependencies: + bundle-require: 5.1.0(esbuild@0.27.7) + cac: 6.7.14 + chokidar: 4.0.3 + consola: 3.4.2 + debug: 4.4.3 + esbuild: 0.27.7 + fix-dts-default-cjs-exports: 1.0.1 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1(tsx@4.22.3) + resolve-from: 5.0.0 + rollup: 4.60.4 + source-map: 0.7.6 + sucrase: 3.35.1 + tinyexec: 0.3.2 + tinyglobby: 0.2.16 + tree-kill: 1.2.2 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + + tsx@4.22.3: + dependencies: + esbuild: 0.28.0 + optionalDependencies: + fsevents: 2.3.3 + + type-flag@4.3.0: {} + + type-is@2.1.0: + dependencies: + content-type: 2.0.0 + media-typer: 1.1.0 + mime-types: 3.0.2 + + typescript@5.9.3: {} + + ufo@1.6.4: {} + + undici-types@7.24.6: {} + + unpipe@1.0.0: {} + + uzip@0.20201231.0: {} + + vary@1.1.2: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + wrappy@1.0.2: {} + + zod-to-json-schema@3.25.2(zod@3.25.76): + dependencies: + zod: 3.25.76 + + zod@3.25.76: {} diff --git a/server.json b/server.json new file mode 100644 index 0000000..d1f7bea --- /dev/null +++ b/server.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-16/server.schema.json", + "name": "figma-local-context-mcp", + "description": "Read local .fig files, inspect node data, and export selected nodes to SVG/PNG.", + "status": "active", + "repository": { + "url": "file://.", + "source": "local" + }, + "packages": [ + { + "registry_type": "npm", + "identifier": "figma-local-context-mcp", + "transport": { + "type": "stdio" + } + } + ] +} diff --git a/src/bin.ts b/src/bin.ts new file mode 100644 index 0000000..57557ca --- /dev/null +++ b/src/bin.ts @@ -0,0 +1,28 @@ +#!/usr/bin/env node + +import { cli } from "cleye" +import { startStdioServer } from "./server.js" + +const argv = cli({ + name: "figma-local-context-mcp", + version: process.env.NPM_PACKAGE_VERSION ?? "0.1.0", + flags: { + stdio: { + type: Boolean, + description: "Run in stdio transport mode for MCP clients" + } + } +}) + +async function main(): Promise { + if (!argv.flags.stdio) { + process.stderr.write("当前版本只支持 stdio,请传入 --stdio。\n") + } + await startStdioServer() +} + +main().catch((error) => { + const message = error instanceof Error ? error.message : String(error) + process.stderr.write(`启动失败:${message}\n`) + process.exit(1) +}) diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..1d1bdf9 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,12 @@ +export { createServer } from "./mcp/index.js" +export { startStdioServer } from "./server.js" +export { figToJson } from "./services/fig2json.js" +export { inspectFigFile, getFigNodeContext, loadFigFile } from "./services/fig-file.js" +export { exportFigNode } from "./services/export-node.js" +export { + exportAssets, + getCodeContext, + getDesignContext, + getDesignTokens, + listFigNodes +} from "./services/design-context.js" diff --git a/src/mcp/index.ts b/src/mcp/index.ts new file mode 100644 index 0000000..73531c6 --- /dev/null +++ b/src/mcp/index.ts @@ -0,0 +1,247 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js" +import { z } from "zod" +import { exportFigNode } from "../services/export-node.js" +import { + exportAssets, + getCodeContext, + getDesignContext, + getDesignTokens, + listFigNodes +} from "../services/design-context.js" +import { getFigNodeContext, inspectFigFile } from "../services/fig-file.js" + +const serverInfo = { + name: "Figma Local Context MCP", + version: process.env.NPM_PACKAGE_VERSION ?? "0.1.0", + description: "Read local .fig files, inspect node context, and export selected nodes without Figma API." +} + +const inspectParams = z.object({ + filePath: z.string().min(1).describe("本地 .fig 或 .fig.json 文件路径。"), + maxNodes: z.number().int().positive().max(2000).default(200).describe("最多返回多少个节点摘要。") +}) + +const getNodeParams = z.object({ + filePath: z.string().min(1).describe("本地 .fig 或 .fig.json 文件路径。"), + nodeQuery: z + .string() + .min(1) + .describe("节点名称、2625:12945、2625-12945、node-id=2625-12945 或完整 Figma 链接。"), + depth: z.number().int().min(0).max(10).default(2).describe("返回节点子树的深度。") +}) + +const exportNodeParams = z.object({ + filePath: z.string().min(1).describe("本地 .fig 或 .fig.json 文件路径。"), + nodeQuery: z + .string() + .min(1) + .describe("节点名称、2625:12945、2625-12945、node-id=2625-12945 或完整 Figma 链接。"), + outputPath: z.string().min(1).optional().describe("输出文件路径;不传时写到当前工作目录。"), + format: z.enum(["svg", "png"]).default("png").describe("导出格式。"), + scale: z.number().positive().max(10).default(2).describe("导出倍率,PNG 常用 1/2/3/4。"), + background: z.string().optional().describe("可选背景色,例如 #ffffff。") +}) + +const listNodesParams = z.object({ + filePath: z.string().min(1).describe("本地 .fig 或 .fig.json 文件路径。"), + query: z.string().optional().describe("按节点名、类型或 node-id 片段搜索。"), + type: z.string().optional().describe("按 Figma 节点类型过滤,例如 FRAME、TEXT、VECTOR。"), + limit: z.number().int().positive().max(5000).default(200).describe("最多返回多少个节点。"), + includeHidden: z.boolean().default(false).describe("是否包含 visible=false 的节点。") +}) + +const designContextParams = z.object({ + filePath: z.string().min(1).describe("本地 .fig 或 .fig.json 文件路径。"), + nodeQuery: z + .string() + .optional() + .describe("节点名称、2625:12945、2625-12945、node-id=2625-12945 或完整 Figma 链接;不传则返回文档根节点上下文。"), + depth: z.number().int().min(0).max(8).default(2).describe("返回节点子树的深度。"), + includeTokens: z.boolean().default(true).describe("是否附带从当前子树推导出的设计 token。"), + includeCodeHints: z.boolean().default(true).describe("是否附带代码实现提示。") +}) + +const codeContextParams = z.object({ + filePath: z.string().min(1).describe("本地 .fig 或 .fig.json 文件路径。"), + nodeQuery: z + .string() + .min(1) + .describe("节点名称、2625:12945、2625-12945、node-id=2625-12945 或完整 Figma 链接。"), + depth: z.number().int().min(0).max(8).default(2).describe("返回代码提示子树的深度。") +}) + +const exportAssetsParams = z.object({ + filePath: z.string().min(1).describe("本地 .fig 或 .fig.json 文件路径。"), + nodeQueries: z + .array(z.string().min(1)) + .min(1) + .max(100) + .describe("要导出的节点名或 node-id 列表。"), + outputDir: z.string().min(1).describe("资源输出目录。"), + format: z.enum(["svg", "png"]).default("png").describe("导出格式。"), + scale: z.number().positive().max(10).default(2).describe("导出倍率,PNG 常用 1/2/3/4。"), + background: z.string().optional().describe("可选背景色,例如 #ffffff。") +}) + +const designTokensParams = z.object({ + filePath: z.string().min(1).describe("本地 .fig 或 .fig.json 文件路径。"), + nodeQuery: z.string().optional().describe("可选节点范围;不传则从文档根节点提取。") +}) + +type InspectParams = z.infer +type GetNodeParams = z.infer +type ExportNodeParams = z.infer +type ListNodesParams = z.infer +type DesignContextParams = z.infer +type CodeContextParams = z.infer +type ExportAssetsParams = z.infer +type DesignTokensParams = z.infer + +export function createServer(): McpServer { + const server = new McpServer(serverInfo) + + server.registerTool( + "list_fig_nodes", + { + title: "List local Figma nodes", + description: "按名称、类型或 node-id 搜索本地 .fig/.fig.json 里的节点。", + inputSchema: listNodesParams, + annotations: { readOnlyHint: true } + }, + async (params: ListNodesParams) => + jsonResponse( + listFigNodes(params.filePath, { + query: params.query, + type: params.type, + limit: params.limit, + includeHidden: params.includeHidden + }) + ) + ) + + server.registerTool( + "get_design_context", + { + title: "Get design context", + description: "官方 Figma MCP 风格的设计上下文工具:返回节点树、样式、tokens 和代码提示。", + inputSchema: designContextParams, + annotations: { readOnlyHint: true } + }, + async (params: DesignContextParams) => + jsonResponse( + getDesignContext({ + filePath: params.filePath, + nodeQuery: params.nodeQuery, + depth: params.depth, + includeTokens: params.includeTokens, + includeCodeHints: params.includeCodeHints + }) + ) + ) + + server.registerTool( + "get_code_context", + { + title: "Get code context", + description: "返回适合代码生成的布局、样式、导出资源提示和子节点实现线索。", + inputSchema: codeContextParams, + annotations: { readOnlyHint: true } + }, + async (params: CodeContextParams) => + jsonResponse( + getCodeContext({ + filePath: params.filePath, + nodeQuery: params.nodeQuery, + depth: params.depth + }) + ) + ) + + server.registerTool( + "export_assets", + { + title: "Export design assets", + description: "批量导出本地 .fig/.fig.json 中的多个节点为 SVG 或 PNG。", + inputSchema: exportAssetsParams, + annotations: { readOnlyHint: false, openWorldHint: true } + }, + async (params: ExportAssetsParams) => + jsonResponse( + exportAssets({ + filePath: params.filePath, + nodeQueries: params.nodeQueries, + outputDir: params.outputDir, + format: params.format, + scale: params.scale, + background: params.background + }) + ) + ) + + server.registerTool( + "get_design_tokens", + { + title: "Get design tokens", + description: "从本地 .fig/.fig.json 的全文件或指定节点子树中推导颜色、渐变、阴影和描边 token。", + inputSchema: designTokensParams, + annotations: { readOnlyHint: true } + }, + async (params: DesignTokensParams) => jsonResponse(getDesignTokens(params.filePath, params.nodeQuery)) + ) + + server.registerTool( + "inspect_fig_file", + { + title: "Inspect local .fig file", + description: "读取本地 .fig/.fig.json,返回节点数量、类型统计和节点摘要。", + inputSchema: inspectParams, + annotations: { readOnlyHint: true } + }, + async (params: InspectParams) => jsonResponse(inspectFigFile(params.filePath, params.maxNodes)) + ) + + server.registerTool( + "get_fig_node", + { + title: "Get local Figma node", + description: "按节点名或 node-id 从本地 .fig/.fig.json 中查找节点,并返回简化上下文。", + inputSchema: getNodeParams, + annotations: { readOnlyHint: true } + }, + async (params: GetNodeParams) => jsonResponse(getFigNodeContext(params.filePath, params.nodeQuery, params.depth)) + ) + + server.registerTool( + "export_fig_node", + { + title: "Export local Figma node", + description: "把本地 .fig/.fig.json 中的指定节点导出为 SVG 或 PNG。", + inputSchema: exportNodeParams, + annotations: { readOnlyHint: false, openWorldHint: true } + }, + async (params: ExportNodeParams) => + jsonResponse( + exportFigNode({ + filePath: params.filePath, + nodeQuery: params.nodeQuery, + outputPath: params.outputPath, + format: params.format, + scale: params.scale, + background: params.background + }) + ) + ) + + return server +} + +function jsonResponse(value: unknown) { + return { + content: [ + { + type: "text" as const, + text: JSON.stringify(value, null, 2) + } + ] + } +} diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 0000000..e9b1d9a --- /dev/null +++ b/src/server.ts @@ -0,0 +1,17 @@ +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" +import { createServer } from "./mcp/index.js" + +export async function startStdioServer(): Promise { + const server = createServer() + const transport = new StdioServerTransport() + await server.connect(transport) + + process.on("SIGINT", async () => { + await server.close() + process.exit(0) + }) + process.on("SIGTERM", async () => { + await server.close() + process.exit(0) + }) +} diff --git a/src/services/design-context.ts b/src/services/design-context.ts new file mode 100644 index 0000000..5351209 --- /dev/null +++ b/src/services/design-context.ts @@ -0,0 +1,276 @@ +import path from "node:path" +import type { FigColor, FigJson, FigNode, FigPaint } from "./fig-types.js" +import { exportFigNode, type ExportNodeResult } from "./export-node.js" +import { getChildrenByParent, findTargetNode } from "./fig-node-svg.js" +import { getRootNode, loadFigFile, serializeNode, summarizeNode } from "./fig-file.js" +import { keyForGuid, sanitizeFilePart } from "../utils/node-id.js" + +export type NodeListOptions = { + query?: string + type?: string + limit: number + includeHidden: boolean +} + +export type DesignContextOptions = { + filePath: string + nodeQuery?: string + depth: number + includeTokens: boolean + includeCodeHints: boolean +} + +export type CodeContextOptions = { + filePath: string + nodeQuery: string + depth: number +} + +export type ExportAssetsOptions = { + filePath: string + nodeQueries: string[] + outputDir: string + format: "svg" | "png" + scale: number + background?: string +} + +export function listFigNodes(filePath: string, options: NodeListOptions) { + const figJson = loadFigFile(filePath) + const childrenByParent = getChildrenByParent(figJson) + const normalizedQuery = options.query?.trim().toLowerCase() + const normalizedType = options.type?.trim().toUpperCase() + const nodes = (figJson.nodeChanges ?? []) + .filter((node) => options.includeHidden || node.visible !== false) + .filter((node) => !normalizedType || node.type?.toUpperCase() === normalizedType) + .filter((node) => { + if (!normalizedQuery) return true + const haystack = `${node.name ?? ""} ${node.type ?? ""} ${keyForGuid(node.guid)}`.toLowerCase() + return haystack.includes(normalizedQuery) + }) + .slice(0, options.limit) + + return { + filePath: path.resolve(filePath), + query: options.query, + type: options.type, + count: nodes.length, + nodes: nodes.map((node) => summarizeNode(node, childrenByParent)) + } +} + +export function getDesignContext(options: DesignContextOptions) { + const figJson = loadFigFile(options.filePath) + const childrenByParent = getChildrenByParent(figJson) + const target = options.nodeQuery ? findTargetNode(figJson, { nodeQuery: options.nodeQuery }) : getRootNode(figJson) + if (!target) { + throw new Error("文件中没有可用节点") + } + + return { + kind: "figma-local-design-context", + filePath: path.resolve(options.filePath), + query: options.nodeQuery, + node: serializeNode(target, childrenByParent, options.depth), + tokens: options.includeTokens ? collectDesignTokens(figJson, target, childrenByParent) : undefined, + codeHints: options.includeCodeHints ? buildCodeHints(target, childrenByParent, options.depth) : undefined + } +} + +export function getCodeContext(options: CodeContextOptions) { + const figJson = loadFigFile(options.filePath) + const childrenByParent = getChildrenByParent(figJson) + const target = findTargetNode(figJson, { nodeQuery: options.nodeQuery }) + + return { + kind: "figma-local-code-context", + filePath: path.resolve(options.filePath), + query: options.nodeQuery, + node: summarizeNode(target, childrenByParent), + implementationHints: buildCodeHints(target, childrenByParent, options.depth) + } +} + +export function exportAssets(options: ExportAssetsOptions) { + const results: ExportNodeResult[] = [] + for (const nodeQuery of options.nodeQueries) { + const fileName = `${sanitizeFilePart(nodeQuery)}${options.format === "png" ? `@${options.scale}x` : ""}.${options.format}` + results.push( + exportFigNode({ + filePath: options.filePath, + nodeQuery, + outputPath: path.join(options.outputDir, fileName), + format: options.format, + scale: options.scale, + background: options.background + }) + ) + } + + return { + filePath: path.resolve(options.filePath), + outputDir: path.resolve(options.outputDir), + count: results.length, + assets: results + } +} + +export function getDesignTokens(filePath: string, nodeQuery?: string) { + const figJson = loadFigFile(filePath) + const childrenByParent = getChildrenByParent(figJson) + const target = nodeQuery ? findTargetNode(figJson, { nodeQuery }) : getRootNode(figJson) + if (!target) { + throw new Error("文件中没有可用节点") + } + + return { + filePath: path.resolve(filePath), + query: nodeQuery, + tokens: collectDesignTokens(figJson, target, childrenByParent) + } +} + +function buildCodeHints(node: FigNode, childrenByParent: Map, depth: number): unknown { + const children = childrenByParent.get(keyForGuid(node.guid)) ?? [] + const hints = { + id: keyForGuid(node.guid), + name: node.name, + type: node.type, + box: { + width: node.size?.x, + height: node.size?.y, + x: node.transform?.m02, + y: node.transform?.m12 + }, + css: { + opacity: node.opacity, + fills: simplifyPaintsForCode(node.fillPaints), + strokes: simplifyPaintsForCode(node.strokePaints), + strokeWidth: node.strokeWeight, + shadows: node.effects + ?.filter((effect) => effect.visible !== false && effect.type === "DROP_SHADOW") + .map((effect) => ({ + x: effect.offset?.x ?? 0, + y: effect.offset?.y ?? 0, + blur: effect.radius ?? 0, + spread: effect.spread ?? 0, + color: colorToCss(effect.color) + })) + }, + exportHint: + node.type === "VECTOR" || node.type === "ELLIPSE" || node.type === "FRAME" + ? `Use export_fig_node or export_assets with nodeQuery "${keyForGuid(node.guid)}" for exact SVG/PNG.` + : undefined + } + + if (depth <= 0) return hints + + return { + ...hints, + children: children + .sort((a, b) => (a.parentIndex?.position ?? "").localeCompare(b.parentIndex?.position ?? "")) + .map((child) => buildCodeHints(child, childrenByParent, depth - 1)) + } +} + +function collectDesignTokens(figJson: FigJson, root: FigNode, childrenByParent: Map) { + const colors = new Map() + const gradients = new Map() + const shadows = new Map() + const strokeWidths = new Map() + + // Tokens are inferred from the selected subtree instead of global styles, + // because local .fig decoding does not yet resolve Figma's variable/style registry. + for (const node of walkSubtree(root, childrenByParent)) { + const example = `${node.name ?? "未命名"} (${keyForGuid(node.guid)})` + for (const paint of [...(node.fillPaints ?? []), ...(node.strokePaints ?? [])]) { + if (paint.visible === false) continue + if (paint.type === "SOLID") { + addToken(colors, colorToCss(paint.color), colorToCss(paint.color), example) + } else if (paint.type === "GRADIENT_LINEAR") { + const stops = paint.stops?.map((stop) => `${formatPercent(stop.position)} ${colorToCss(stop.color)}`) ?? [] + addToken(gradients, stops.join(" | "), stops, example) + } + } + + for (const effect of node.effects ?? []) { + if (effect.visible === false || effect.type !== "DROP_SHADOW") continue + const value = { + x: effect.offset?.x ?? 0, + y: effect.offset?.y ?? 0, + blur: effect.radius ?? 0, + spread: effect.spread ?? 0, + color: colorToCss(effect.color) + } + addToken(shadows, JSON.stringify(value), value, example) + } + + if (node.strokeWeight != null) { + addToken(strokeWidths, String(node.strokeWeight), node.strokeWeight, example) + } + } + + return { + nodeCount: figJson.nodeChanges?.length ?? 0, + colors: [...colors.values()], + gradients: [...gradients.values()], + shadows: [...shadows.values()], + strokeWidths: [...strokeWidths.values()] + } +} + +function* walkSubtree(root: FigNode, childrenByParent: Map): Generator { + yield root + for (const child of childrenByParent.get(keyForGuid(root.guid)) ?? []) { + yield* walkSubtree(child, childrenByParent) + } +} + +function addToken( + map: Map, + key: string, + value: T, + example: string +) { + const current = map.get(key) + if (current) { + current.count += 1 + if (current.examples.length < 5) current.examples.push(example) + return + } + + map.set(key, { value, count: 1, examples: [example] }) +} + +function simplifyPaintsForCode(paints: FigPaint[] | undefined) { + if (!paints?.length) return undefined + + return paints + .filter((paint) => paint.visible !== false) + .map((paint) => { + if (paint.type === "SOLID") return colorToCss(paint.color) + if (paint.type === "GRADIENT_LINEAR") { + const stops = paint.stops?.map((stop) => `${colorToCss(stop.color)} ${formatPercent(stop.position)}`) ?? [] + return `linear-gradient(${stops.join(", ")})` + } + return paint.type + }) +} + +function colorToCss(color?: FigColor): string { + if (!color) return "rgb(0 0 0 / 1)" + + return `rgb(${toByte(color.r)} ${toByte(color.g)} ${toByte(color.b)} / ${trimNumber(color.a)})` +} + +function toByte(value: number): number { + return Math.round(Math.max(0, Math.min(1, value)) * 255) +} + +function formatPercent(value: number): string { + return `${trimNumber(value * 100)}%` +} + +function trimNumber(value: number): string { + return Number.isInteger(value) ? String(value) : Number(value.toFixed(4)).toString() +} diff --git a/src/services/export-node.ts b/src/services/export-node.ts new file mode 100644 index 0000000..078b9ef --- /dev/null +++ b/src/services/export-node.ts @@ -0,0 +1,67 @@ +import fs from "node:fs" +import path from "node:path" +import { Resvg } from "@resvg/resvg-js" +import { loadFigFile } from "./fig-file.js" +import { renderNodeToSvg } from "./fig-node-svg.js" +import { keyForGuid, sanitizeFilePart } from "../utils/node-id.js" + +export type ExportNodeOptions = { + filePath: string + nodeQuery: string + outputPath?: string + format: "svg" | "png" + scale: number + background?: string +} + +export type ExportNodeResult = { + filePath: string + outputPath: string + format: "svg" | "png" + scale: number + width: number + height: number + node: { + id: string + name?: string + type?: string + } +} + +export function exportFigNode(options: ExportNodeOptions): ExportNodeResult { + const figJson = loadFigFile(options.filePath) + const rendered = renderNodeToSvg(figJson, { + nodeQuery: options.nodeQuery, + scale: options.scale, + background: options.background + }) + const outputPath = path.resolve(options.outputPath ?? defaultOutputPath(options)) + + fs.mkdirSync(path.dirname(outputPath), { recursive: true }) + if (options.format === "svg") { + fs.writeFileSync(outputPath, rendered.svg) + } else { + fs.writeFileSync(outputPath, new Resvg(rendered.svg).render().asPng()) + } + + return { + filePath: path.resolve(options.filePath), + outputPath, + format: options.format, + scale: options.scale, + width: rendered.width, + height: rendered.height, + node: { + id: keyForGuid(rendered.node.guid), + name: rendered.node.name, + type: rendered.node.type + } + } +} + +function defaultOutputPath(options: ExportNodeOptions): string { + const input = path.parse(options.filePath) + const nodePart = sanitizeFilePart(options.nodeQuery) + const scalePart = options.format === "png" ? `@${options.scale}x` : "" + return path.join(process.cwd(), `${input.name}-${nodePart}${scalePart}.${options.format}`) +} diff --git a/src/services/fig-file.ts b/src/services/fig-file.ts new file mode 100644 index 0000000..f8b562b --- /dev/null +++ b/src/services/fig-file.ts @@ -0,0 +1,138 @@ +import fs from "node:fs" +import path from "node:path" +import type { FigJson, FigNode } from "./fig-types.js" +import { figToJson } from "./fig2json.js" +import { findTargetNode, getChildrenByParent } from "./fig-node-svg.js" +import { keyForGuid, normalizeNodeId } from "../utils/node-id.js" + +export type NodeSummary = { + id: string + name?: string + type?: string + parentId?: string + visible?: boolean + opacity?: number + size?: { x: number; y: number } + childCount: number + hasFill: boolean + hasStroke: boolean + hasEffects: boolean +} + +export type FileSummary = { + filePath: string + nodeCount: number + blobCount: number + topTypes: Record + nodes: NodeSummary[] +} + +export function loadFigFile(filePath: string): FigJson { + const absolutePath = path.resolve(filePath) + if (!fs.existsSync(absolutePath)) { + throw new Error(`文件不存在:${absolutePath}`) + } + + if (absolutePath.toLowerCase().endsWith(".json")) { + return JSON.parse(fs.readFileSync(absolutePath, "utf8")) as FigJson + } + + return figToJson(fs.readFileSync(absolutePath)) +} + +export function inspectFigFile(filePath: string, maxNodes: number): FileSummary { + const figJson = loadFigFile(filePath) + const childrenByParent = getChildrenByParent(figJson) + const nodes = figJson.nodeChanges ?? [] + const topTypes: Record = {} + + for (const node of nodes) { + const type = node.type ?? "UNKNOWN" + topTypes[type] = (topTypes[type] ?? 0) + 1 + } + + return { + filePath: path.resolve(filePath), + nodeCount: nodes.length, + blobCount: figJson.blobs?.length ?? 0, + topTypes: Object.fromEntries(Object.entries(topTypes).sort((a, b) => b[1] - a[1])), + nodes: nodes.slice(0, maxNodes).map((node) => summarizeNode(node, childrenByParent)) + } +} + +export function getFigNodeContext(filePath: string, nodeQuery: string, depth: number): unknown { + const figJson = loadFigFile(filePath) + const target = findTargetNode(figJson, { nodeQuery }) + const childrenByParent = getChildrenByParent(figJson) + + return { + filePath: path.resolve(filePath), + query: nodeQuery, + normalizedNodeId: normalizeNodeId(nodeQuery), + node: serializeNode(target, childrenByParent, depth) + } +} + +export function getRootNode(figJson: FigJson): FigNode | undefined { + const nodes = figJson.nodeChanges ?? [] + return ( + nodes.find((node) => node.type === "DOCUMENT") ?? + nodes.find((node) => !node.parentIndex?.guid) ?? + nodes[0] + ) +} + +export function summarizeNode(node: FigNode, childrenByParent: Map): NodeSummary { + return { + id: keyForGuid(node.guid), + name: node.name, + type: node.type, + parentId: keyForGuid(node.parentIndex?.guid) || undefined, + visible: node.visible, + opacity: node.opacity, + size: node.size, + childCount: childrenByParent.get(keyForGuid(node.guid))?.length ?? 0, + hasFill: Boolean(node.fillPaints?.length), + hasStroke: Boolean(node.strokePaints?.length), + hasEffects: Boolean(node.effects?.length) + } +} + +export function serializeNode(node: FigNode, childrenByParent: Map, depth: number): unknown { + const children = childrenByParent.get(keyForGuid(node.guid)) ?? [] + const summary = { + ...summarizeNode(node, childrenByParent), + transform: node.transform, + strokeWeight: node.strokeWeight, + strokeAlign: node.strokeAlign, + arcData: node.arcData, + fills: simplifyPaints(node.fillPaints), + strokes: simplifyPaints(node.strokePaints), + effects: node.effects, + geometry: { + fillCount: node.fillGeometry?.length ?? 0, + strokeCount: node.strokeGeometry?.length ?? 0 + } + } + + if (depth <= 0) return summary + + return { + ...summary, + children: children + .sort((a, b) => (a.parentIndex?.position ?? "").localeCompare(b.parentIndex?.position ?? "")) + .map((child) => serializeNode(child, childrenByParent, depth - 1)) + } +} + +function simplifyPaints(paints: FigNode["fillPaints"]): unknown[] | undefined { + if (!paints?.length) return undefined + + return paints.map((paint) => ({ + type: paint.type, + visible: paint.visible, + opacity: paint.opacity, + color: paint.color, + stops: paint.stops + })) +} diff --git a/src/services/fig-node-svg.ts b/src/services/fig-node-svg.ts new file mode 100644 index 0000000..fd2d447 --- /dev/null +++ b/src/services/fig-node-svg.ts @@ -0,0 +1,541 @@ +import type { + Bounds, + FigArcData, + FigColor, + FigEffect, + FigGeometry, + FigJson, + FigNode, + FigPaint, + FigmaMatrix +} from "./fig-types.js" +import { keyForGuid, normalizeNodeId } from "../utils/node-id.js" + +type SvgMatrix = [number, number, number, number, number, number] + +type ParsedPath = { + d: string + bounds: Bounds +} + +type RenderContext = { + figJson: FigJson + childrenByParent: Map + defs: string[] + bounds: Bounds | null + idSeed: number +} + +export type RenderOptions = { + nodeName?: string + nodeId?: string + nodeQuery?: string + scale?: number + background?: string +} + +export type RenderedSvg = { + svg: string + width: number + height: number + viewBox: Bounds + node: FigNode +} + +const IDENTITY: SvgMatrix = [1, 0, 0, 1, 0, 0] +const COMMAND_MOVE = 1 +const COMMAND_LINE = 2 +const COMMAND_CUBIC = 4 +const COMMAND_CLOSE = 0 + +export function renderNodeToSvg(figJson: FigJson, options: RenderOptions): RenderedSvg { + const target = findTargetNode(figJson, options) + const childrenByParent = getChildrenByParent(figJson) + const context: RenderContext = { + figJson, + childrenByParent, + defs: [], + bounds: null, + idSeed: 0 + } + + const body = renderNodeSubtree(context, target, IDENTITY, true) + const bounds = context.bounds ?? { minX: 0, minY: 0, maxX: target.size?.x ?? 0, maxY: target.size?.y ?? 0 } + const width = bounds.maxX - bounds.minX + const height = bounds.maxY - bounds.minY + const scale = options.scale ?? 2 + const background = options.background + ? `` + : "" + const svg = [ + ``, + context.defs.length ? `${context.defs.join("")}` : "", + background, + body, + "" + ].join("") + + return { svg, width: width * scale, height: height * scale, viewBox: bounds, node: target } +} + +export function findTargetNode(figJson: FigJson, options: RenderOptions): FigNode { + const nodes = figJson.nodeChanges ?? [] + const rawQuery = options.nodeId ?? options.nodeQuery + const normalizedNodeId = normalizeNodeId(rawQuery) + const nodeName = options.nodeName ?? (normalizedNodeId ? undefined : options.nodeQuery) + const target = normalizedNodeId + ? nodes.find((node) => keyForGuid(node.guid) === normalizedNodeId) + : nodes.find((node) => node.name === nodeName) + + if (!target) { + throw new Error(`找不到节点:${options.nodeId ?? options.nodeName ?? options.nodeQuery}`) + } + + return target +} + +export function getChildrenByParent(figJson: FigJson): Map { + const childrenByParent = new Map() + for (const node of figJson.nodeChanges ?? []) { + if (!node.parentIndex?.guid) continue + + const parentKey = keyForGuid(node.parentIndex.guid) + const children = childrenByParent.get(parentKey) ?? [] + children.push(node) + childrenByParent.set(parentKey, children) + } + + return childrenByParent +} + +function renderNodeSubtree( + context: RenderContext, + node: FigNode, + parentMatrix: SvgMatrix, + isRoot = false +): string { + if (node.visible === false) return "" + + const localMatrix = isRoot ? IDENTITY : toSvgMatrix(node.transform) + const matrix = multiply(parentMatrix, localMatrix) + const nodeContent = [ + ...renderGeometry(context, node, node.fillGeometry, node.fillPaints, matrix), + ...renderStrokeGeometry(context, node, matrix), + ...getSortedChildren(context, node).map((child) => renderNodeSubtree(context, child, matrix)) + ].join("") + + if (!nodeContent) return "" + + const transform = matrixToAttribute(localMatrix) + const opacity = node.opacity != null && node.opacity !== 1 ? ` opacity="${format(node.opacity)}"` : "" + const filterId = createNodeEffectFilter(context, node, matrix) + const filter = filterId ? ` filter="url(#${filterId})"` : "" + + return `${nodeContent}` +} + +function renderStrokeGeometry(context: RenderContext, node: FigNode, matrix: SvgMatrix): string[] { + const outsideEllipseStroke = renderOutsideEllipseStroke(context, node, matrix) + if (outsideEllipseStroke) return outsideEllipseStroke + + return renderGeometry(context, node, node.strokeGeometry, node.strokePaints, matrix) +} + +function renderOutsideEllipseStroke(context: RenderContext, node: FigNode, matrix: SvgMatrix): string[] | null { + const strokeWeight = node.strokeWeight ?? 0 + if ( + node.type !== "ELLIPSE" || + node.strokeAlign !== "OUTSIDE" || + !node.size || + !node.strokePaints?.length || + strokeWeight <= 0 || + !isFullEllipse(node.arcData) + ) { + return null + } + + const bounds = { + minX: -strokeWeight, + minY: -strokeWeight, + maxX: node.size.x + strokeWeight, + maxY: node.size.y + strokeWeight + } + includeBounds(context, transformBounds(matrix, bounds)) + + return node.strokePaints + .filter((paint) => paint.visible !== false) + .map((paint) => { + const stroke = paintToSvgFill(context, node, bounds, paint) + const opacity = paint.opacity != null && paint.opacity !== 1 ? ` stroke-opacity="${format(paint.opacity)}"` : "" + const rx = node.size!.x / 2 + strokeWeight / 2 + const ry = node.size!.y / 2 + strokeWeight / 2 + + // Figma's strokeGeometry for OUTSIDE ellipses is an expanded filled + // outline. Using it directly paints inward and erases the ring gap, so + // complete ellipses are emitted as actual outside strokes. + return `` + }) +} + +function isFullEllipse(arcData?: FigArcData): boolean { + if (!arcData) return true + + const start = arcData.startingAngle ?? 0 + const end = arcData.endingAngle ?? Math.PI * 2 + const delta = Math.abs(end - start) + return (arcData.innerRadius ?? 0) === 0 && Math.abs(delta - Math.PI * 2) < 0.001 +} + +function renderGeometry( + context: RenderContext, + node: FigNode, + geometries: FigGeometry[] | undefined, + paints: FigPaint[] | undefined, + matrix: SvgMatrix +): string[] { + if (!geometries?.length || !paints?.length) return [] + + return geometries.flatMap((geometry) => { + const parsed = parsePathBlob(context.figJson, geometry.commandsBlob) + const transformedBounds = transformBounds(matrix, parsed.bounds) + includeBounds(context, transformedBounds) + + return paints + .filter((paint) => paint.visible !== false) + .map((paint) => { + const fill = paintToSvgFill(context, node, parsed.bounds, paint) + const opacity = paint.opacity != null && paint.opacity !== 1 ? ` fill-opacity="${format(paint.opacity)}"` : "" + + return `` + }) + }) +} + +function parsePathBlob(figJson: FigJson, blobIndex: number): ParsedPath { + const blob = figJson.blobs?.[blobIndex] + if (!blob) { + throw new Error(`缺少几何数据 blob:${blobIndex}`) + } + + const bytes = base64ToBytes(blob) + const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength) + let offset = 0 + let d = "" + const bounds = createEmptyBounds() + + const readFloat = () => { + const value = view.getFloat32(offset, true) + offset += 4 + return value + } + const readPoint = () => { + const x = readFloat() + const y = readFloat() + includePoint(bounds, x, y) + return `${format(x)} ${format(y)}` + } + + // Figma stores flattened vector commands as compact opcodes followed by + // little-endian float coordinates: 1=M, 2=L, 4=C, 0=Z. + while (offset < bytes.length) { + const command = bytes[offset] + offset += 1 + + if (command === COMMAND_MOVE) { + d += `M ${readPoint()} ` + } else if (command === COMMAND_LINE) { + d += `L ${readPoint()} ` + } else if (command === COMMAND_CUBIC) { + d += `C ${readPoint()} ${readPoint()} ${readPoint()} ` + } else if (command === COMMAND_CLOSE) { + d += "Z " + } else { + throw new Error(`几何数据 blob ${blobIndex} 中存在不支持的向量命令:${command}`) + } + } + + return { d: d.trim(), bounds } +} + +function paintToSvgFill(context: RenderContext, node: FigNode, pathBounds: Bounds, paint: FigPaint): string { + if (paint.type === "SOLID") { + return colorToCss(paint.color) + } + + if (paint.type === "GRADIENT_LINEAR") { + const id = nextId(context, "gradient") + const gradient = getLinearGradientLine(node, pathBounds, paint) + const stops = paint.stops + ?.map( + (stop) => + `` + ) + .join("") + + context.defs.push( + `${stops ?? ""}` + ) + return `url(#${id})` + } + + throw new Error(`不支持的填充类型:${paint.type}`) +} + +function getLinearGradientLine(node: FigNode, pathBounds: Bounds, paint: FigPaint) { + const width = node.size?.x || pathBounds.maxX - pathBounds.minX + const height = node.size?.y || pathBounds.maxY - pathBounds.minY + const originX = pathBounds.minX < 0 ? pathBounds.minX : 0 + const originY = pathBounds.minY < 0 ? pathBounds.minY : 0 + const inverse = invert(toSvgMatrix(paint.transform)) + + // Figma's stored gradient transform maps local object space back into the + // unit gradient space, so SVG needs the inverse projected onto the node box. + const start = applyToPoint(inverse, 0, 0) + const end = applyToPoint(inverse, 1, 0) + + return { + x1: originX + start.x * width, + y1: originY + start.y * height, + x2: originX + end.x * width, + y2: originY + end.y * height + } +} + +function createFilter(context: RenderContext, effects: FigEffect[] | undefined, bounds: Bounds): string | null { + const shadow = effects?.find((effect) => effect.visible !== false && effect.type === "DROP_SHADOW") + if (!shadow) return null + + const id = nextId(context, "shadow") + const radius = shadow.radius ?? 0 + const spread = shadow.spread ?? 0 + const offsetX = shadow.offset?.x ?? 0 + const offsetY = shadow.offset?.y ?? 0 + const x = bounds.minX + Math.min(0, offsetX) - radius - spread + const y = bounds.minY + Math.min(0, offsetY) - radius - spread + const width = bounds.maxX - bounds.minX + Math.abs(offsetX) + radius * 2 + spread * 2 + const height = bounds.maxY - bounds.minY + Math.abs(offsetY) + radius * 2 + spread * 2 + const sourceAlpha = spread + ? `` + : "" + const blurInput = spread ? "spreadAlpha" : "SourceAlpha" + const shadowResult = shadow.showShadowBehindNode === false ? "visibleShadow" : "shadow" + const hideShadowBehindSource = + shadow.showShadowBehindNode === false + ? `` + : "" + + // Figma can store showShadowBehindNode=false. SVG's feDropShadow always + // leaves the blurred shadow under the source, so we build the filter steps + // manually and subtract SourceAlpha when Figma says the shadow is outside-only. + context.defs.push( + `${sourceAlpha}${hideShadowBehindSource}` + ) + + return id +} + +function createNodeEffectFilter(context: RenderContext, node: FigNode, matrix: SvgMatrix): string | null { + const localBounds = getNodeLocalBounds(context, node) + if (!localBounds) return null + + // Effects belong to the Figma layer itself. Frames/groups can carry shadows + // even when their own fill paints are empty, so the filter must wrap the + // rendered layer content instead of only individual geometry paths. + const filterId = createFilter(context, node.effects, localBounds) + if (!filterId) return null + + includeBounds(context, transformBounds(matrix, expandBoundsForEffects(localBounds, node.effects))) + return filterId +} + +function getNodeLocalBounds(context: RenderContext, node: FigNode): Bounds | null { + const geometryBounds = getGeometryLocalBounds(context, node) + if (geometryBounds) return geometryBounds + + if (!node.size) return null + + return { + minX: 0, + minY: 0, + maxX: node.size.x, + maxY: node.size.y + } +} + +function getGeometryLocalBounds(context: RenderContext, node: FigNode): Bounds | null { + const geometries = [...(node.fillGeometry ?? []), ...(node.strokeGeometry ?? [])] + let bounds: Bounds | null = null + + for (const geometry of geometries) { + const parsed = parsePathBlob(context.figJson, geometry.commandsBlob) + bounds = bounds ? unionBounds(bounds, parsed.bounds) : { ...parsed.bounds } + } + + return bounds && Number.isFinite(bounds.minX) ? bounds : null +} + +function getSortedChildren(context: RenderContext, node: FigNode): FigNode[] { + return [...(context.childrenByParent.get(keyForGuid(node.guid)) ?? [])].sort((a, b) => + (a.parentIndex?.position ?? "").localeCompare(b.parentIndex?.position ?? "") + ) +} + +function nextId(context: RenderContext, prefix: string): string { + context.idSeed += 1 + return `${prefix}-${context.idSeed}` +} + +function toSvgMatrix(matrix?: FigmaMatrix): SvgMatrix { + if (!matrix) return IDENTITY + return [matrix.m00, matrix.m10, matrix.m01, matrix.m11, matrix.m02, matrix.m12] +} + +function matrixToAttribute(matrix: SvgMatrix): string { + if (matrix.every((value, index) => value === IDENTITY[index])) return "" + return ` transform="matrix(${matrix.map(format).join(" ")})"` +} + +function multiply(left: SvgMatrix, right: SvgMatrix): SvgMatrix { + const [a1, b1, c1, d1, e1, f1] = left + const [a2, b2, c2, d2, e2, f2] = right + + return [ + a1 * a2 + c1 * b2, + b1 * a2 + d1 * b2, + a1 * c2 + c1 * d2, + b1 * c2 + d1 * d2, + a1 * e2 + c1 * f2 + e1, + b1 * e2 + d1 * f2 + f1 + ] +} + +function invert(matrix: SvgMatrix): SvgMatrix { + const [a, b, c, d, e, f] = matrix + const determinant = a * d - b * c + if (Math.abs(determinant) < Number.EPSILON) return IDENTITY + + return [ + d / determinant, + -b / determinant, + -c / determinant, + a / determinant, + (c * f - d * e) / determinant, + (b * e - a * f) / determinant + ] +} + +function applyToPoint(matrix: SvgMatrix, x: number, y: number) { + const [a, b, c, d, e, f] = matrix + + return { + x: a * x + c * y + e, + y: b * x + d * y + f + } +} + +function transformBounds(matrix: SvgMatrix, bounds: Bounds): Bounds { + const points = [ + applyToPoint(matrix, bounds.minX, bounds.minY), + applyToPoint(matrix, bounds.maxX, bounds.minY), + applyToPoint(matrix, bounds.maxX, bounds.maxY), + applyToPoint(matrix, bounds.minX, bounds.maxY) + ] + + return points.reduce((next, point) => includePoint(next, point.x, point.y), createEmptyBounds()) +} + +function expandBoundsForEffects(bounds: Bounds, effects: FigEffect[] | undefined): Bounds { + const next = { ...bounds } + for (const effect of effects ?? []) { + if (effect.visible === false || effect.type !== "DROP_SHADOW") continue + + const radius = effect.radius ?? 0 + const spread = effect.spread ?? 0 + const offsetX = effect.offset?.x ?? 0 + const offsetY = effect.offset?.y ?? 0 + next.minX = Math.min(next.minX, bounds.minX + offsetX - radius - spread) + next.maxX = Math.max(next.maxX, bounds.maxX + offsetX + radius + spread) + next.minY = Math.min(next.minY, bounds.minY + offsetY - radius - spread) + next.maxY = Math.max(next.maxY, bounds.maxY + offsetY + radius + spread) + } + + return next +} + +function createEmptyBounds(): Bounds { + return { + minX: Number.POSITIVE_INFINITY, + minY: Number.POSITIVE_INFINITY, + maxX: Number.NEGATIVE_INFINITY, + maxY: Number.NEGATIVE_INFINITY + } +} + +function includeBounds(context: RenderContext, bounds: Bounds) { + context.bounds = context.bounds + ? { + minX: Math.min(context.bounds.minX, bounds.minX), + minY: Math.min(context.bounds.minY, bounds.minY), + maxX: Math.max(context.bounds.maxX, bounds.maxX), + maxY: Math.max(context.bounds.maxY, bounds.maxY) + } + : { ...bounds } +} + +function unionBounds(left: Bounds, right: Bounds): Bounds { + return { + minX: Math.min(left.minX, right.minX), + minY: Math.min(left.minY, right.minY), + maxX: Math.max(left.maxX, right.maxX), + maxY: Math.max(left.maxY, right.maxY) + } +} + +function includePoint(bounds: Bounds, x: number, y: number): Bounds { + bounds.minX = Math.min(bounds.minX, x) + bounds.minY = Math.min(bounds.minY, y) + bounds.maxX = Math.max(bounds.maxX, x) + bounds.maxY = Math.max(bounds.maxY, y) + return bounds +} + +function colorToCss(color?: FigColor): string { + if (!color) return "#000000" + + return `rgb(${toByte(color.r)} ${toByte(color.g)} ${toByte(color.b)})` +} + +function toByte(value: number): number { + return Math.round(Math.max(0, Math.min(1, value)) * 255) +} + +function base64ToBytes(value: string): Uint8Array { + const buffer = Buffer.from(value, "base64") + return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength) +} + +function escapeAttribute(value: string): string { + return value.replace(/"/g, """) +} + +function format(value: number): string { + return Number.isInteger(value) ? String(value) : Number(value.toFixed(4)).toString() +} diff --git a/src/services/fig-types.ts b/src/services/fig-types.ts new file mode 100644 index 0000000..175fadf --- /dev/null +++ b/src/services/fig-types.ts @@ -0,0 +1,89 @@ +export type Guid = { + sessionID: number + localID: number +} + +export type FigmaMatrix = { + m00: number + m01: number + m02: number + m10: number + m11: number + m12: number +} + +export type FigArcData = { + startingAngle?: number + endingAngle?: number + innerRadius?: number +} + +export type FigColor = { + r: number + g: number + b: number + a: number +} + +export type FigPaint = { + type: "SOLID" | "GRADIENT_LINEAR" | string + color?: FigColor + opacity?: number + visible?: boolean + stops?: Array<{ color: FigColor; position: number }> + transform?: FigmaMatrix +} + +export type FigGeometry = { + commandsBlob: number + windingRule?: string + styleID?: number +} + +export type FigEffect = { + type: string + visible?: boolean + offset?: { x: number; y: number } + radius?: number + spread?: number + showShadowBehindNode?: boolean + color?: FigColor +} + +export type FigNode = { + guid: Guid + parentIndex?: { guid: Guid; position?: string } + type?: string + name?: string + visible?: boolean + opacity?: number + size?: { x: number; y: number } + transform?: FigmaMatrix + strokeWeight?: number + strokeAlign?: "CENTER" | "INSIDE" | "OUTSIDE" + arcData?: FigArcData + fillPaints?: FigPaint[] + strokePaints?: FigPaint[] + fillGeometry?: FigGeometry[] + strokeGeometry?: FigGeometry[] + effects?: FigEffect[] +} + +export type FigJson = { + type?: string + sessionID?: number + ackID?: number + nodeChanges?: FigNode[] + blobs?: string[] + __figmaToJson?: { + schema: string + delimiter: number + } +} + +export type Bounds = { + minX: number + minY: number + maxX: number + maxY: number +} diff --git a/src/services/fig2json.ts b/src/services/fig2json.ts new file mode 100644 index 0000000..a69fc6d --- /dev/null +++ b/src/services/fig2json.ts @@ -0,0 +1,104 @@ +import { ByteBuffer, compileSchema, decodeBinarySchema } from "kiwi-schema" +import { decompress as decompressZstd } from "fzstd" +import UzipModule from "uzip" +import type { FigJson } from "./fig-types.js" + +const UZIP = (UzipModule as any).default ?? UzipModule +const FIGMA_TO_JSON_METADATA_KEY = "__figmaToJson" +const ZSTD_MAGIC = [0x28, 0xb5, 0x2f, 0xfd] +const PNG_MAGIC = [137, 80, 78, 71] + +type FigmaBinaryParts = { + delimiter: number + parts: Uint8Array[] +} + +export function figToJson(fileBuffer: Uint8Array | ArrayBuffer): FigJson { + const { delimiter, parts } = figToBinaryParts(fileBuffer) + assertFigmaParts(parts) + const [schemaByte, dataByte] = parts + + const schema = decodeBinarySchema(new ByteBuffer(schemaByte)) + const dataBB = new ByteBuffer(dataByte) + const schemaHelper = compileSchema(schema) + const json = schemaHelper.decodeMessage(dataBB) as FigJson + + return { + ...convertBlobsToBase64(json), + [FIGMA_TO_JSON_METADATA_KEY]: { + schema: bytesToBase64(schemaByte), + delimiter + } + } +} + +function figToBinaryParts(fileBuffer: Uint8Array | ArrayBuffer): FigmaBinaryParts { + let fileByte: Uint8Array = fileBuffer instanceof Uint8Array ? fileBuffer : new Uint8Array(fileBuffer) + + if (!isKiwiFile(fileByte)) { + const unzipped = UZIP.parse(toArrayBuffer(fileByte)) + const canvas = unzipped["canvas.fig"] + if (!canvas) { + throw new Error("未找到 canvas.fig,文件可能不是有效的 .fig") + } + fileByte = new Uint8Array(canvas.buffer, canvas.byteOffset, canvas.byteLength) + } + + let start = 8 + const delimiter = readUint32(fileByte, start) + start += 4 + + const parts: Uint8Array[] = [] + while (start < fileByte.length) { + const size = readUint32(fileByte, start) + start += 4 + + let part: Uint8Array = fileByte.slice(start, start + size) + if (startsWith(part, ZSTD_MAGIC)) { + part = decompressZstd(part) + } else if (!startsWith(part, PNG_MAGIC)) { + part = UZIP.inflateRaw(part) + } + + parts.push(part) + start += size + } + + return { delimiter, parts } +} + +function convertBlobsToBase64(json: FigJson): FigJson { + const blobs = (json as any).blobs + if (!Array.isArray(blobs)) return json + + return { + ...json, + blobs: blobs.map((blob: any) => bytesToBase64(blob.bytes ?? blob)) + } +} + +function assertFigmaParts(parts: Uint8Array[]): void { + if (parts.length < 2) { + throw new Error("fig 文件缺少 schema 或 data 数据段") + } +} + +function isKiwiFile(bytes: Uint8Array): boolean { + return bytes.length >= 8 && String.fromCharCode(...bytes.slice(0, 8)) === "fig-kiwi" +} + +function startsWith(bytes: Uint8Array, magic: number[]): boolean { + return magic.every((value, index) => bytes[index] === value) +} + +function readUint32(bytes: Uint8Array, offset: number): number { + return new DataView(bytes.buffer, bytes.byteOffset + offset, 4).getUint32(0, true) +} + +function bytesToBase64(bytes: Uint8Array): string { + return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("base64") +} + +function toArrayBuffer(bytes: Uint8Array): ArrayBuffer { + return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer +} diff --git a/src/utils/node-id.ts b/src/utils/node-id.ts new file mode 100644 index 0000000..478c6f7 --- /dev/null +++ b/src/utils/node-id.ts @@ -0,0 +1,30 @@ +import type { Guid } from "../services/fig-types.js" + +export function keyForGuid(guid?: Guid): string { + return guid ? `${guid.sessionID}:${guid.localID}` : "" +} + +export function normalizeNodeId(value?: string): string | null { + if (!value) return null + + const text = safeDecodeURIComponent(value.trim()) + const nodeIdParam = text.match(/(?:^|[?&#\s])node-id=([^&#\s]+)/i)?.[1] + const candidate = safeDecodeURIComponent(nodeIdParam ?? text).trim() + const match = candidate.match(/^(\d+)[:\-](\d+)$/) + + if (!match) return null + + return `${match[1]}:${match[2]}` +} + +export function sanitizeFilePart(value: string): string { + return value.replace(/[^a-z0-9\u4e00-\u9fa5_-]+/gi, "-").replace(/^-|-$/g, "") || "node" +} + +function safeDecodeURIComponent(value: string): string { + try { + return decodeURIComponent(value) + } catch { + return value + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..13a8e4f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "noEmit": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"] +} diff --git a/tsup.config.ts b/tsup.config.ts new file mode 100644 index 0000000..97f5af5 --- /dev/null +++ b/tsup.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "tsup" + +export default defineConfig({ + entry: ["src/bin.ts", "src/index.ts"], + format: ["esm"], + target: "node20", + dts: true, + sourcemap: true, + clean: true +})