1
0
Fork 0

feat(cli): add @docsearch/cli for MCP setup and search (#2911)

This commit is contained in:
Dylan Tientcheu 2026-07-14 16:46:18 +02:00 committed by GitHub
parent 40ca749cf4
commit 218eac3062
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 3495 additions and 43 deletions

View file

@ -33,6 +33,7 @@ aliases:
mkdir -p packages/docsearch-modal/dist
mkdir -p packages/docsearch-sidepanel/dist
mkdir -p packages/docsearch-sidepanel-js/dist
mkdir -p packages/docsearch-cli/dist
mkdir -p adapters/docusaurus-theme-search-algolia/lib
cp -R /tmp/workspace/packages/docsearch-react/dist packages/docsearch-react
@ -42,6 +43,7 @@ aliases:
cp -R /tmp/workspace/packages/docsearch-modal/dist packages/docsearch-modal
cp -R /tmp/workspace/packages/docsearch-sidepanel/dist packages/docsearch-sidepanel
cp -R /tmp/workspace/packages/docsearch-sidepanel-js/dist packages/docsearch-sidepanel-js
cp -R /tmp/workspace/packages/docsearch-cli/dist packages/docsearch-cli
cp -R /tmp/workspace/adapters/docusaurus-theme-search-algolia/lib adapters/docusaurus-theme-search-algolia
defaults: &defaults
@ -76,6 +78,7 @@ jobs:
name: Build and Size
command: |
bun run build
bun run --filter @docsearch/cli test:pack
- run:
name: Move dist folders to workspace
command: |
@ -88,6 +91,7 @@ jobs:
mkdir -p /tmp/workspace/packages/docsearch-modal/dist
mkdir -p /tmp/workspace/packages/docsearch-sidepanel/dist
mkdir -p /tmp/workspace/packages/docsearch-sidepanel-js/dist
mkdir -p /tmp/workspace/packages/docsearch-cli/dist
mkdir -p /tmp/workspace/adapters/docusaurus-theme-search-algolia/lib
cp -R packages/docsearch-react/dist /tmp/workspace/packages/docsearch-react
@ -97,6 +101,7 @@ jobs:
cp -R packages/docsearch-modal/dist /tmp/workspace/packages/docsearch-modal
cp -R packages/docsearch-sidepanel/dist /tmp/workspace/packages/docsearch-sidepanel
cp -R packages/docsearch-sidepanel-js/dist /tmp/workspace/packages/docsearch-sidepanel-js
cp -R packages/docsearch-cli/dist /tmp/workspace/packages/docsearch-cli
cp -R adapters/docusaurus-theme-search-algolia/lib /tmp/workspace/adapters/docusaurus-theme-search-algolia
- persist_to_workspace:
root: *workspace_root

275
bun.lock
View file

@ -108,6 +108,23 @@
"vite": "^6.0.7",
},
},
"packages/docsearch-cli": {
"name": "@docsearch/cli",
"version": "0.0.0",
"bin": {
"docsearch": "./dist/index.js",
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.29.0",
"jsonc-parser": "^3.3.1",
"picocolors": "^1.1.1",
},
"devDependencies": {
"@types/node": "^26.1.1",
"typescript": "5.7.3",
"vitest": "3.0.2",
},
},
"packages/docsearch-core": {
"name": "@docsearch/core",
"version": "4.6.0",
@ -687,6 +704,8 @@
"@discoveryjs/json-ext": ["@discoveryjs/json-ext@0.5.7", "", {}, "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw=="],
"@docsearch/cli": ["@docsearch/cli@workspace:packages/docsearch-cli"],
"@docsearch/core": ["@docsearch/core@workspace:packages/docsearch-core"],
"@docsearch/css": ["@docsearch/css@workspace:packages/docsearch-css"],
@ -847,6 +866,8 @@
"@hapi/topo": ["@hapi/topo@5.1.0", "", { "dependencies": { "@hapi/hoek": "^9.0.0" } }, "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg=="],
"@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="],
"@humanwhocodes/config-array": ["@humanwhocodes/config-array@0.13.0", "", { "dependencies": { "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" } }, "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw=="],
"@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
@ -899,6 +920,8 @@
"@mdx-js/react": ["@mdx-js/react@3.1.1", "", { "dependencies": { "@types/mdx": "^2.0.0" }, "peerDependencies": { "@types/react": ">=16", "react": ">=16" } }, "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw=="],
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="],
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
@ -1291,7 +1314,7 @@
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
"@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
"@types/node": ["@types/node@26.1.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw=="],
"@types/node-forge": ["@types/node-forge@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw=="],
@ -1405,7 +1428,7 @@
"@xtuc/long": ["@xtuc/long@4.2.2", "", {}, "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ=="],
"accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="],
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
@ -1425,7 +1448,7 @@
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
"ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="],
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
"ajv-keywords": ["ajv-keywords@3.5.2", "", { "peerDependencies": { "ajv": "^6.9.1" } }, "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="],
@ -1553,7 +1576,7 @@
"bluebird": ["bluebird@3.7.2", "", {}, "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="],
"body-parser": ["body-parser@1.20.4", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.14.0", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA=="],
"body-parser": ["body-parser@2.3.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^2.0.0", "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" } }, "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw=="],
"bonjour-service": ["bonjour-service@1.3.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "multicast-dns": "^7.2.5" } }, "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA=="],
@ -1707,7 +1730,7 @@
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
"content-disposition": ["content-disposition@0.5.2", "", {}, "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA=="],
"content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="],
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
@ -1715,7 +1738,7 @@
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
"cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="],
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
"copy-webpack-plugin": ["copy-webpack-plugin@11.0.0", "", { "dependencies": { "fast-glob": "^3.2.11", "glob-parent": "^6.0.1", "globby": "^13.1.1", "normalize-path": "^3.0.0", "schema-utils": "^4.0.0", "serialize-javascript": "^6.0.0" }, "peerDependencies": { "webpack": "^5.1.0" } }, "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ=="],
@ -1727,6 +1750,8 @@
"core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="],
"cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
"cosmiconfig": ["cosmiconfig@5.2.1", "", { "dependencies": { "import-fresh": "^2.0.0", "is-directory": "^0.3.1", "js-yaml": "^3.13.1", "parse-json": "^4.0.0" } }, "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
@ -2017,6 +2042,8 @@
"events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="],
"eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="],
"eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="],
"exec-buffer": ["exec-buffer@3.2.0", "", { "dependencies": { "execa": "^0.7.0", "p-finally": "^1.0.0", "pify": "^3.0.0", "rimraf": "^2.5.4", "tempfile": "^2.0.0" } }, "sha512-wsiD+2Tp6BWHoVv3B+5Dcx6E7u5zky+hUwOHjuH2hKSLR3dvRmX8fk8UD8uqQixHs4Wk6eDmiegVrMPjKj7wpA=="],
@ -2029,7 +2056,9 @@
"expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="],
"express": ["express@4.22.1", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.3", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g=="],
"express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
"express-rate-limit": ["express-rate-limit@8.5.2", "", { "dependencies": { "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A=="],
"ext-list": ["ext-list@2.2.2", "", { "dependencies": { "mime-db": "^1.28.0" } }, "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA=="],
@ -2083,7 +2112,7 @@
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
"finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="],
"finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
"find-cache-dir": ["find-cache-dir@4.0.0", "", { "dependencies": { "common-path-prefix": "^3.0.0", "pkg-dir": "^7.0.0" } }, "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg=="],
@ -2113,7 +2142,7 @@
"framer-motion": ["framer-motion@12.24.0", "", { "dependencies": { "motion-dom": "^12.24.0", "motion-utils": "^12.23.28", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-ggTMRkIDPc76lHmM+dRT1MmVfFV6t/y+jkWjWuzR7FG5xRvnAAl/5wFPjzSkLE8Nu5E5uIQRCNxmIXtWJVo6XQ=="],
"fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="],
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
"from": ["from@0.1.7", "", {}, "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g=="],
@ -2239,6 +2268,8 @@
"hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="],
"hono": ["hono@4.12.28", "", {}, "sha512-YwUvVpSF7m1yOblFPrU3Hbo8XhPheBoiyfGuII6z19LnOr6JpDnyyp7LFNrfV56wS8tpvtBFGRISHN02pDdLOA=="],
"hookable": ["hookable@6.0.1", "", {}, "sha512-uKGyY8BuzN/a5gvzvA+3FVWo0+wUjgtfSdnmjtrOVwQCZPHpHDH2WRO3VZSOeluYrHoDCiXFffZXs8Dj1ULWtw=="],
"hookified": ["hookified@1.15.0", "", {}, "sha512-51w+ZZGt7Zw5q7rM3nC4t3aLn/xvKDETsXqMczndvwyVQhAHfUmUuFBRFcos8Iyebtk7OAE9dL26wFNzZVVOkw=="],
@ -2339,6 +2370,8 @@
"invariant": ["invariant@2.2.4", "", { "dependencies": { "loose-envify": "^1.0.0" } }, "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA=="],
"ip-address": ["ip-address@10.2.0", "", {}, "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA=="],
"ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="],
"irregular-plurals": ["irregular-plurals@3.5.0", "", {}, "sha512-1ANGLZ+Nkv1ptFb2pa8oG8Lem4krflKuX/gINiHJHjJUKaJHk/SXk5x6K3J+39/p0h1RQ2saROclJJ+QLvETCQ=="],
@ -2427,6 +2460,8 @@
"is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="],
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
"is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="],
"is-regexp": ["is-regexp@1.0.0", "", {}, "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA=="],
@ -2481,6 +2516,8 @@
"joi": ["joi@17.13.3", "", { "dependencies": { "@hapi/hoek": "^9.3.0", "@hapi/topo": "^5.1.0", "@sideway/address": "^4.1.5", "@sideway/formula": "^3.0.1", "@sideway/pinpoint": "^2.0.0" } }, "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA=="],
"jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="],
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
@ -2501,10 +2538,14 @@
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
"json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
"json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="],
"jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="],
"jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="],
"jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="],
@ -2651,7 +2692,7 @@
"mdn-data": ["mdn-data@2.12.2", "", {}, "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA=="],
"media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="],
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
"memfs": ["memfs@4.51.1", "", { "dependencies": { "@jsonjoy.com/json-pack": "^1.11.0", "@jsonjoy.com/util": "^1.9.0", "glob-to-regex.js": "^1.0.1", "thingies": "^2.5.0", "tree-dump": "^1.0.3", "tslib": "^2.0.0" } }, "sha512-Eyt3XrufitN2ZL9c/uIRMyDwXanLI88h/L3MoWqNY747ha3dMR9dWqp8cRT5ntjZ0U1TNuq4U91ZXK0sMBjYOQ=="],
@ -2659,7 +2700,7 @@
"merge": ["merge@1.2.1", "", {}, "sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ=="],
"merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="],
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
"merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="],
@ -2791,7 +2832,7 @@
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
"negotiator": ["negotiator@0.6.4", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="],
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
"neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="],
@ -2959,6 +3000,8 @@
"pinkie-promise": ["pinkie-promise@2.0.1", "", { "dependencies": { "pinkie": "^2.0.0" } }, "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw=="],
"pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="],
"pkg-dir": ["pkg-dir@7.0.0", "", { "dependencies": { "find-up": "^6.3.0" } }, "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA=="],
"playwright": ["playwright@1.49.1", "", { "dependencies": { "playwright-core": "1.49.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA=="],
@ -3177,9 +3220,9 @@
"randombytes": ["randombytes@2.1.0", "", { "dependencies": { "safe-buffer": "^5.1.0" } }, "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ=="],
"range-parser": ["range-parser@1.2.0", "", {}, "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A=="],
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
"raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="],
"raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
"rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="],
@ -3315,6 +3358,8 @@
"rollup": ["rollup@4.55.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.55.1", "@rollup/rollup-android-arm64": "4.55.1", "@rollup/rollup-darwin-arm64": "4.55.1", "@rollup/rollup-darwin-x64": "4.55.1", "@rollup/rollup-freebsd-arm64": "4.55.1", "@rollup/rollup-freebsd-x64": "4.55.1", "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", "@rollup/rollup-linux-arm-musleabihf": "4.55.1", "@rollup/rollup-linux-arm64-gnu": "4.55.1", "@rollup/rollup-linux-arm64-musl": "4.55.1", "@rollup/rollup-linux-loong64-gnu": "4.55.1", "@rollup/rollup-linux-loong64-musl": "4.55.1", "@rollup/rollup-linux-ppc64-gnu": "4.55.1", "@rollup/rollup-linux-ppc64-musl": "4.55.1", "@rollup/rollup-linux-riscv64-gnu": "4.55.1", "@rollup/rollup-linux-riscv64-musl": "4.55.1", "@rollup/rollup-linux-s390x-gnu": "4.55.1", "@rollup/rollup-linux-x64-gnu": "4.55.1", "@rollup/rollup-linux-x64-musl": "4.55.1", "@rollup/rollup-openbsd-x64": "4.55.1", "@rollup/rollup-openharmony-arm64": "4.55.1", "@rollup/rollup-win32-arm64-msvc": "4.55.1", "@rollup/rollup-win32-ia32-msvc": "4.55.1", "@rollup/rollup-win32-x64-gnu": "4.55.1", "@rollup/rollup-win32-x64-msvc": "4.55.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A=="],
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
"rrweb-cssom": ["rrweb-cssom@0.8.0", "", {}, "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw=="],
"rtlcss": ["rtlcss@4.3.0", "", { "dependencies": { "escalade": "^3.1.1", "picocolors": "^1.0.0", "postcss": "^8.4.21", "strip-json-comments": "^3.1.1" }, "bin": { "rtlcss": "bin/rtlcss.js" } }, "sha512-FI+pHEn7Wc4NqKXMXFM+VAYKEj/mRIcW4h24YVwVtyjI+EqGrLc2Hx/Ny0lrZ21cBWU2goLy36eqMcNj3AQJig=="],
@ -3363,7 +3408,7 @@
"semver-truncate": ["semver-truncate@1.1.2", "", { "dependencies": { "semver": "^5.3.0" } }, "sha512-V1fGg9i4CL3qesB6U0L6XAm4xOJiHmt4QAacazumuasc03BvtFGIMCduv01JWQ69Nv+JST9TqhSCiJoxoY031w=="],
"send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="],
"send": ["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" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
"serialize-javascript": ["serialize-javascript@6.0.2", "", { "dependencies": { "randombytes": "^2.1.0" } }, "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g=="],
@ -3371,7 +3416,7 @@
"serve-index": ["serve-index@1.9.1", "", { "dependencies": { "accepts": "~1.3.4", "batch": "0.6.1", "debug": "2.6.9", "escape-html": "~1.0.3", "http-errors": "~1.6.2", "mime-types": "~2.1.17", "parseurl": "~1.3.2" } }, "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw=="],
"serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="],
"serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
"set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
@ -3649,7 +3694,7 @@
"type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="],
"type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="],
"type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="],
"typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="],
@ -3673,7 +3718,7 @@
"undefsafe": ["undefsafe@2.0.5", "", {}, "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA=="],
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
"unicode-canonical-property-names-ecmascript": ["unicode-canonical-property-names-ecmascript@2.0.1", "", {}, "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg=="],
@ -3841,6 +3886,8 @@
"zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="],
"zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="],
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
"@asamuzakjp/css-color/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
@ -3907,6 +3954,8 @@
"@isaacs/fs-minipass/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
"@jest/types/@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
"@keyv/bigmap/keyv": ["keyv@5.5.5", "", { "dependencies": { "@keyv/serialize": "^1.1.1" } }, "sha512-FA5LmZVF1VziNc0bIdCSA1IoSVnDCqE8HJIZZv2/W8YmoAM50+tnUgJR/gQZwEeIMleuIOnRnHA/UaZRNeV4iQ=="],
"@manypkg/find-root/@types/node": ["@types/node@12.20.55", "", {}, "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="],
@ -3919,6 +3968,8 @@
"@manypkg/get-packages/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="],
"@modelcontextprotocol/sdk/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
"@pnpm/network.ca-file/graceful-fs": ["graceful-fs@4.2.10", "", {}, "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="],
"@quansync/fs/quansync": ["quansync@1.0.0", "", {}, "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA=="],
@ -3957,6 +4008,36 @@
"@testing-library/jest-dom/dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="],
"@types/body-parser/@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
"@types/bonjour/@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
"@types/connect/@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
"@types/connect-history-api-fallback/@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
"@types/express-serve-static-core/@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
"@types/fs-extra/@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
"@types/glob/@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
"@types/http-proxy/@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
"@types/jsonfile/@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
"@types/node-forge/@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
"@types/sax/@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
"@types/send/@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
"@types/serve-static/@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
"@types/sockjs/@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
"@types/ws/@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils": ["@typescript-eslint/utils@8.20.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@typescript-eslint/scope-manager": "8.20.0", "@typescript-eslint/types": "8.20.0", "@typescript-eslint/typescript-estree": "8.20.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-dq70RUw6UK9ei7vxc4KQtBRk7qkHZv447OUZ6RPQMQl71I3NZxQJX/f32Smr+iqWrB02pHKn2yAdHBb0KNrRMA=="],
"@typescript-eslint/project-service/@typescript-eslint/types": ["@typescript-eslint/types@8.52.0", "", {}, "sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg=="],
@ -3977,7 +4058,7 @@
"@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@3.0.2", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-yBohcBw/T/p0/JRgYD+IYcjCmuHzjC3WLAKsVE4/LwiubzZkE8N49/xIQ/KGQwDRA8PaviF8IRO8JMWMngdVVQ=="],
"accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
"accepts/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
"ajv-formats/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
@ -4009,9 +4090,11 @@
"bl/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
"body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"body-parser/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],
"body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
"body-parser/iconv-lite": ["iconv-lite@0.7.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ=="],
"body-parser/qs": ["qs@6.15.3", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A=="],
"boxen/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
@ -4047,6 +4130,8 @@
"compression/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"compression/negotiator": ["negotiator@0.6.4", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="],
"configstore/write-file-atomic": ["write-file-atomic@3.0.3", "", { "dependencies": { "imurmurhash": "^0.1.4", "is-typedarray": "^1.0.0", "signal-exit": "^3.0.2", "typedarray-to-buffer": "^3.1.5" } }, "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q=="],
"copy-webpack-plugin/globby": ["globby@13.2.2", "", { "dependencies": { "dir-glob": "^3.0.1", "fast-glob": "^3.3.0", "ignore": "^5.2.4", "merge2": "^1.4.1", "slash": "^4.0.0" } }, "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w=="],
@ -4139,6 +4224,8 @@
"eslint-plugin-react/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"eval/@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
"exec-buffer/execa": ["execa@0.7.0", "", { "dependencies": { "cross-spawn": "^5.0.1", "get-stream": "^3.0.0", "is-stream": "^1.1.0", "npm-run-path": "^2.0.0", "p-finally": "^1.0.0", "signal-exit": "^3.0.0", "strip-eof": "^1.0.0" } }, "sha512-RztN09XglpYI7aBBrJCPW95jEH7YF1UEPOoX9yDhUTPdp7mK+CQvnLTuD10BNXZ3byLTu2uehZ8EcKT/4CGiFw=="],
"exec-buffer/pify": ["pify@3.0.0", "", {}, "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg=="],
@ -4149,13 +4236,7 @@
"executable/pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="],
"express/content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="],
"express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"express/path-to-regexp": ["path-to-regexp@0.1.12", "", {}, "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="],
"express/range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
"express/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
"ext-list/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
@ -4165,8 +4246,6 @@
"file-loader/schema-utils": ["schema-utils@3.3.0", "", { "dependencies": { "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", "ajv-keywords": "^3.5.2" } }, "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg=="],
"finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"from2/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
"glob/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
@ -4209,8 +4288,12 @@
"is-inside-container/is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
"jest-util/@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
"jest-util/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"jest-worker/@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
"jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
"loader-utils/json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
@ -4409,8 +4492,6 @@
"proxy-addr/ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
"raw-body/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
"rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],
"react-router/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
@ -4427,20 +4508,26 @@
"rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
"router/path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="],
"seek-bzip/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
"semver-truncate/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="],
"send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"send/range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
"send/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
"serve-handler/bytes": ["bytes@3.0.0", "", {}, "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw=="],
"serve-handler/content-disposition": ["content-disposition@0.5.2", "", {}, "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA=="],
"serve-handler/mime-types": ["mime-types@2.1.18", "", { "dependencies": { "mime-db": "~1.33.0" } }, "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ=="],
"serve-handler/path-to-regexp": ["path-to-regexp@3.3.0", "", {}, "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw=="],
"serve-handler/range-parser": ["range-parser@1.2.0", "", {}, "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A=="],
"serve-index/accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="],
"serve-index/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"serve-index/http-errors": ["http-errors@1.6.3", "", { "dependencies": { "depd": "~1.1.2", "inherits": "2.0.3", "setprototypeof": "1.1.0", "statuses": ">= 1.4.0 < 2" } }, "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A=="],
@ -4499,6 +4586,10 @@
"tsdown/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
"type-is/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],
"type-is/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
"typescript-eslint/@typescript-eslint/utils": ["@typescript-eslint/utils@8.20.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@typescript-eslint/scope-manager": "8.20.0", "@typescript-eslint/types": "8.20.0", "@typescript-eslint/typescript-estree": "8.20.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-dq70RUw6UK9ei7vxc4KQtBRk7qkHZv447OUZ6RPQMQl71I3NZxQJX/f32Smr+iqWrB02pHKn2yAdHBb0KNrRMA=="],
"unconfig-core/quansync": ["quansync@1.0.0", "", {}, "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA=="],
@ -4527,10 +4618,10 @@
"webpack-dev-middleware/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
"webpack-dev-middleware/range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
"webpack-dev-middleware/schema-utils": ["schema-utils@4.3.3", "", { "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", "ajv-formats": "^2.1.1", "ajv-keywords": "^5.1.0" } }, "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA=="],
"webpack-dev-server/express": ["express@4.22.1", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.3", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g=="],
"webpack-dev-server/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
"webpack-dev-server/schema-utils": ["schema-utils@4.3.3", "", { "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", "ajv-formats": "^2.1.1", "ajv-keywords": "^5.1.0" } }, "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA=="],
@ -4589,6 +4680,8 @@
"@eslint/eslintrc/import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
"@jest/types/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"@manypkg/find-root/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
"@manypkg/find-root/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="],
@ -4599,6 +4692,8 @@
"@manypkg/get-packages/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
"@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"@radix-ui/react-popper/@floating-ui/react-dom/@floating-ui/dom": ["@floating-ui/dom@1.7.4", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA=="],
"@svgr/core/cosmiconfig/import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
@ -4641,6 +4736,36 @@
"@testing-library/jest-dom/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"@types/body-parser/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"@types/bonjour/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"@types/connect-history-api-fallback/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"@types/connect/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"@types/express-serve-static-core/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"@types/fs-extra/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"@types/glob/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"@types/http-proxy/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"@types/jsonfile/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"@types/node-forge/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"@types/sax/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"@types/send/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"@types/serve-static/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"@types/sockjs/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"@types/ws/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
"@typescript-eslint/utils/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.52.0", "", { "dependencies": { "@typescript-eslint/types": "8.52.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-ink3/Zofus34nmBsPjow63FP5M7IGff0RKAgqR6+CFpdk22M7aLwC9gOcLGYqr7MczLPzZVERW9hRog3O4n1sQ=="],
@ -4649,6 +4774,8 @@
"@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"accepts/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
"ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"autoprefixer/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.9.11", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ=="],
@ -4659,6 +4786,8 @@
"babel-loader/schema-utils/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
"babel-loader/schema-utils/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="],
"babel-loader/schema-utils/ajv-keywords": ["ajv-keywords@5.1.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "ajv": "^8.8.2" } }, "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw=="],
"bin-build/execa/cross-spawn": ["cross-spawn@5.1.0", "", { "dependencies": { "lru-cache": "^4.0.1", "shebang-command": "^1.2.0", "which": "^1.2.9" } }, "sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A=="],
@ -4711,7 +4840,7 @@
"bl/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
"body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"body-parser/qs/side-channel": ["side-channel@1.1.1", "", { "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" } }, "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ=="],
"boxen/string-width/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
@ -4729,6 +4858,8 @@
"copy-webpack-plugin/schema-utils/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
"copy-webpack-plugin/schema-utils/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="],
"copy-webpack-plugin/schema-utils/ajv-keywords": ["ajv-keywords@5.1.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "ajv": "^8.8.2" } }, "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw=="],
"core-js-compat/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.9.11", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ=="],
@ -4743,6 +4874,8 @@
"css-minimizer-webpack-plugin/schema-utils/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
"css-minimizer-webpack-plugin/schema-utils/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="],
"css-minimizer-webpack-plugin/schema-utils/ajv-keywords": ["ajv-keywords@5.1.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "ajv": "^8.8.2" } }, "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw=="],
"css-select/domutils/dom-serializer": ["dom-serializer@1.4.1", "", { "dependencies": { "domelementtype": "^2.0.1", "domhandler": "^4.2.0", "entities": "^2.0.0" } }, "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag=="],
@ -4789,6 +4922,8 @@
"duplexify/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
"eval/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"exec-buffer/execa/cross-spawn": ["cross-spawn@5.1.0", "", { "dependencies": { "lru-cache": "^4.0.1", "shebang-command": "^1.2.0", "which": "^1.2.9" } }, "sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A=="],
"exec-buffer/execa/get-stream": ["get-stream@3.0.0", "", {}, "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ=="],
@ -4801,9 +4936,7 @@
"exec-buffer/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
"express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"express/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
"from2/readable-stream/isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="],
@ -4847,10 +4980,16 @@
"imagemin/globby/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
"jest-util/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"jest-worker/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"mdast-util-gfm-autolink-literal/micromark-util-character/micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="],
"mini-css-extract-plugin/schema-utils/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
"mini-css-extract-plugin/schema-utils/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="],
"mini-css-extract-plugin/schema-utils/ajv-keywords": ["ajv-keywords@5.1.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "ajv": "^8.8.2" } }, "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw=="],
"node-fetch/whatwg-url/tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
@ -4943,10 +5082,12 @@
"renderkid/htmlparser2/entities": ["entities@2.2.0", "", {}, "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="],
"send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"send/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
"serve-handler/mime-types/mime-db": ["mime-db@1.33.0", "", {}, "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ=="],
"serve-index/accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
"serve-index/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"serve-index/http-errors/depd": ["depd@1.1.2", "", {}, "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ=="],
@ -4985,12 +5126,18 @@
"tar-stream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
"terser-webpack-plugin/jest-worker/@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
"terser-webpack-plugin/jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
"terser-webpack-plugin/schema-utils/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
"terser-webpack-plugin/schema-utils/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="],
"terser-webpack-plugin/schema-utils/ajv-keywords": ["ajv-keywords@5.1.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "ajv": "^8.8.2" } }, "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw=="],
"type-is/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
"unrun/rolldown/@oxc-project/types": ["@oxc-project/types@0.106.0", "", {}, "sha512-QdsH3rZq480VnOHSHgPYOhjL8O8LBdcnSjM408BpPCCUc0JYYZPG9Gafl9i3OcGk/7137o+gweb4cCv3WAUykg=="],
"unrun/rolldown/@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-beta.58", "", { "os": "android", "cpu": "arm64" }, "sha512-mWj5eE4Qc8TbPdGGaaLvBb9XfDPvE1EmZkJQgiGKwchkWH4oAJcRAKMTw7ZHnb1L+t7Ah41sBkAecaIsuUgsug=="],
@ -5031,12 +5178,40 @@
"webpack-dev-middleware/schema-utils/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
"webpack-dev-middleware/schema-utils/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="],
"webpack-dev-middleware/schema-utils/ajv-keywords": ["ajv-keywords@5.1.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "ajv": "^8.8.2" } }, "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw=="],
"webpack-dev-server/express/accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="],
"webpack-dev-server/express/body-parser": ["body-parser@1.20.4", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.14.0", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA=="],
"webpack-dev-server/express/content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="],
"webpack-dev-server/express/cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="],
"webpack-dev-server/express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"webpack-dev-server/express/finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="],
"webpack-dev-server/express/fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="],
"webpack-dev-server/express/merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="],
"webpack-dev-server/express/path-to-regexp": ["path-to-regexp@0.1.12", "", {}, "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="],
"webpack-dev-server/express/send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="],
"webpack-dev-server/express/serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="],
"webpack-dev-server/express/type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="],
"webpack-dev-server/open/define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="],
"webpack-dev-server/schema-utils/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
"webpack-dev-server/schema-utils/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="],
"webpack-dev-server/schema-utils/ajv-keywords": ["ajv-keywords@5.1.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "ajv": "^8.8.2" } }, "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw=="],
"webpack/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.9.11", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ=="],
@ -5051,6 +5226,8 @@
"webpack/schema-utils/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
"webpack/schema-utils/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="],
"webpack/schema-utils/ajv-keywords": ["ajv-keywords@5.1.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "ajv": "^8.8.2" } }, "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw=="],
"widest-line/string-width/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
@ -5121,6 +5298,8 @@
"bin-wrapper/download/p-event/p-timeout": ["p-timeout@2.0.1", "", { "dependencies": { "p-finally": "^1.0.0" } }, "sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA=="],
"body-parser/qs/side-channel/side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="],
"boxen/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
"copy-webpack-plugin/schema-utils/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
@ -5165,12 +5344,24 @@
"stylelint/cosmiconfig/import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
"terser-webpack-plugin/jest-worker/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"terser-webpack-plugin/schema-utils/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"update-notifier/boxen/string-width/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
"webpack-dev-middleware/schema-utils/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"webpack-dev-server/express/accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
"webpack-dev-server/express/body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
"webpack-dev-server/express/body-parser/raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="],
"webpack-dev-server/express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"webpack-dev-server/express/type-is/media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="],
"webpack-dev-server/schema-utils/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"webpack/schema-utils/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],

View file

@ -8,7 +8,7 @@
],
"type": "module",
"scripts": {
"build": "bun run --sequential build:stage:base build:stage:react build:stage:consumers build:stage:adapter website:build",
"build": "bun run --sequential build:stage:base build:stage:react build:stage:consumers build:stage:adapter build:pkg:cli website:build",
"build:all": "bun run --sequential build website:build",
"build:pkg:core": "bun --filter @docsearch/core build",
"build:pkg:css": "bun --filter @docsearch/css build",
@ -18,6 +18,7 @@
"build:pkg:sidepanel-js": "bun --filter @docsearch/sidepanel-js build",
"build:pkg:modal": "bun --filter @docsearch/modal build",
"build:pkg:adapter": "bun --filter @docsearch/docusaurus-adapter build",
"build:pkg:cli": "bun --filter @docsearch/cli build",
"build:stage:base": "bun run --parallel build:pkg:core build:pkg:css",
"build:stage:react": "bun run build:pkg:react",
"build:stage:consumers": "bun run --parallel build:pkg:js build:pkg:sidepanel build:pkg:sidepanel-js build:pkg:modal",

View file

@ -0,0 +1,7 @@
# @docsearch/cli
## 0.0.1
### Patch Changes
- 710b410: Add the DocSearch CLI for installing and querying the hosted DocSearch MCP.

View file

@ -0,0 +1,104 @@
# DocSearch CLI
Install the hosted [DocSearch MCP](https://docsearch.algolia.com/mcp) in your AI coding agents and search public developer documentation from the terminal.
## Run without installing
```sh
npx @docsearch/cli
```
Or install the `docsearch` command globally:
```sh
npm install --global @docsearch/cli
```
Requires Node.js 20 or later.
## Set up the DocSearch MCP
Run the interactive installer:
```sh
docsearch setup
```
The installer asks whether to configure the current project or your global agent settings, then lets you select any detected agents. It supports:
- Cursor
- Claude Code
- Codex
- OpenCode
- Gemini CLI
Skip the prompts with explicit flags:
```sh
docsearch setup --project --cursor --claude --yes
docsearch setup --global --all --yes
```
Project setup locates the repository root before writing configuration. Re-running setup is idempotent and preserves unrelated configuration.
## Search documentation
Use `docs` for most questions:
```sh
docsearch docs Next.js "how do middleware matchers work"
```
For explicit docset selection, resolve a product and then query its docset:
```sh
docsearch resolve "Algolia InstantSearch React"
docsearch query repo/algolia/instantsearch "configure the React search client"
```
Limit the returned results when needed:
```sh
docsearch docs Next.js "cache revalidation" --max-results 3 --max-docsets 2
docsearch resolve Next.js --top-n 3
```
Pass `--json` to write the raw MCP result as JSON:
```sh
docsearch docs Next.js "cache revalidation" --json
```
Machine-readable results are written to stdout. Progress and errors are written to stderr.
## Options
```text
--project Install into the current repository
--global Install into user-level agent settings
--all Configure every supported agent
--cursor Configure Cursor
--claude Configure Claude Code
--codex Configure Codex
--opencode Configure OpenCode
--gemini Configure Gemini CLI
--yes, -y Run setup non-interactively
--endpoint <url> Override the hosted DocSearch MCP endpoint
--json Print raw MCP results as JSON
```
Set [`NO_COLOR`](https://no-color.org/) to disable terminal colors.
## Development
From the DocSearch monorepo root:
```sh
bun install
bun run --filter @docsearch/cli build
bun run --filter @docsearch/cli test
```
## License
[MIT](https://github.com/algolia/docsearch/blob/v5/LICENSE)

View file

@ -0,0 +1,57 @@
{
"name": "@docsearch/cli",
"description": "Command-line tools for installing and querying DocSearch MCP.",
"version": "0.0.2",
"license": "MIT",
"type": "module",
"homepage": "https://docsearch.algolia.com",
"repository": {
"type": "git",
"url": "git+https://github.com/algolia/docsearch.git",
"directory": "packages/docsearch-cli"
},
"author": {
"name": "Algolia, Inc.",
"url": "https://www.algolia.com"
},
"bugs": {
"url": "https://github.com/algolia/docsearch/issues"
},
"bin": {
"docsearch": "./dist/index.js"
},
"files": [
"dist/"
],
"keywords": [
"algolia",
"cli",
"documentation",
"docsearch",
"mcp"
],
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "bun run clean && tsc --project tsconfig.json",
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
"prepack": "bun run build",
"test": "vitest run --config vitest.config.mts",
"test:pack": "npm pack --dry-run",
"watch": "tsc --project tsconfig.json --watch"
},
"engines": {
"node": ">=20"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.29.0",
"jsonc-parser": "^3.3.1",
"picocolors": "^1.1.1"
},
"devDependencies": {
"@types/node": "^26.1.1",
"typescript": "5.7.3",
"vitest": "3.0.2"
}
}

View file

@ -0,0 +1,75 @@
// @vitest-environment node
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { runCli } from '../cli';
import { callDocSearchTool } from '../mcp/client';
vi.mock('../mcp/client.js', () => ({
callDocSearchTool: vi.fn(),
}));
interface CapturedOutput {
stderr: string;
stdout: string;
}
function captureOutput(): CapturedOutput {
const output: CapturedOutput = { stderr: '', stdout: '' };
vi.spyOn(process.stdout, 'write').mockImplementation((chunk) => {
output.stdout += String(chunk);
return true;
});
vi.spyOn(process.stderr, 'write').mockImplementation((chunk) => {
output.stderr += String(chunk);
return true;
});
return output;
}
afterEach(() => {
vi.clearAllMocks();
vi.restoreAllMocks();
});
describe('runCli', () => {
it('returns a non-zero exit for MCP tool errors while preserving JSON output', async () => {
const result: CallToolResult = {
content: [{ text: 'Invalid query', type: 'text' }],
isError: true,
};
vi.mocked(callDocSearchTool).mockResolvedValue(result);
const output = captureOutput();
const exitCode = await runCli(['resolve', 'Next.js', '--json']);
expect(exitCode).toBe(1);
expect(JSON.parse(output.stdout)).toMatchObject({ isError: true });
expect(output.stderr).toBe('');
});
it('reports MCP tool errors in human-readable mode', async () => {
const result: CallToolResult = {
content: [{ text: 'Invalid query', type: 'text' }],
isError: true,
};
vi.mocked(callDocSearchTool).mockResolvedValue(result);
const output = captureOutput();
const exitCode = await runCli(['resolve', 'Next.js']);
expect(exitCode).toBe(1);
expect(output.stderr).toContain('Invalid query');
});
it('uses exit code 2 for command usage errors', async () => {
const output = captureOutput();
const exitCode = await runCli(['resolve', 'Next.js', '--max-results', '2']);
expect(exitCode).toBe(2);
expect(output.stderr).toContain('not valid for the resolve command');
expect(callDocSearchTool).not.toHaveBeenCalled();
});
});

View file

@ -0,0 +1,99 @@
// @vitest-environment node
import { describe, expect, it } from 'vitest';
import { buildSetupRequest, buildToolRequest, parseArgs } from '../commands';
import { DEFAULT_MCP_ENDPOINT, TOOL_QUERY_DOCS, TOOL_RESOLVE_DOCSET, TOOL_SEARCH_DOCS } from '../constants';
describe('buildToolRequest', () => {
it('maps docs to the one-shot DocSearch MCP tool', () => {
const request = buildToolRequest(
'docs',
parseArgs(['Next.js', 'middleware matcher config', '--max-results', '3', '--json']),
);
expect(request).toEqual({
endpoint: DEFAULT_MCP_ENDPOINT,
json: true,
toolName: TOOL_SEARCH_DOCS,
toolArguments: {
library: 'Next.js',
query: 'middleware matcher config',
maxResults: 3,
},
});
});
it('maps resolve to the docset resolver tool', () => {
const request = buildToolRequest('resolve', parseArgs(['Algolia', 'InstantSearch', '--top-n', '4']));
expect(request).toEqual({
endpoint: DEFAULT_MCP_ENDPOINT,
json: false,
toolName: TOOL_RESOLVE_DOCSET,
toolArguments: {
query: 'Algolia InstantSearch',
topN: 4,
},
});
});
it('maps query to explicit docset lookups', () => {
const request = buildToolRequest(
'query',
parseArgs(['nextjs,react', 'server components', '--endpoint', 'https://example.com/mcp']),
);
expect(request).toEqual({
endpoint: 'https://example.com/mcp',
json: false,
toolName: TOOL_QUERY_DOCS,
toolArguments: {
docsetIds: ['nextjs', 'react'],
query: 'server components',
},
});
});
it('supports equals syntax for value options', () => {
const request = buildToolRequest('resolve', parseArgs(['Next.js', '--top-n=2']));
expect(request.toolArguments).toEqual({
query: 'Next.js',
topN: 2,
});
});
it('rejects empty docset IDs', () => {
expect(() => buildToolRequest('query', parseArgs([',', 'search query']))).toThrow(/docset ID/i);
});
it('rejects options that do not belong to the command', () => {
expect(() => buildToolRequest('resolve', parseArgs(['Next.js', '--max-results', '2']))).toThrow(
/not valid for the resolve command/,
);
});
it('rejects non-HTTP endpoints', () => {
expect(() => buildToolRequest('resolve', parseArgs(['Next.js', '--endpoint', 'file:///tmp/mcp']))).toThrow(
/HTTP or HTTPS/,
);
});
});
describe('buildSetupRequest', () => {
it('maps setup flags to selected agents and project scope', () => {
expect(buildSetupRequest(parseArgs(['--cursor', '--claude', '--project', '--yes']))).toEqual({
agents: ['claude', 'cursor'].sort(),
all: false,
endpoint: DEFAULT_MCP_ENDPOINT,
scope: 'project',
yes: true,
});
});
it('rejects positional setup arguments and unrelated flags', () => {
expect(() => buildSetupRequest(parseArgs(['cursor']))).toThrow(/Unexpected setup argument/);
expect(() => buildSetupRequest(parseArgs(['--json']))).toThrow(/not valid for the setup command/);
});
});

View file

@ -0,0 +1,180 @@
/* eslint-disable import/no-unresolved -- NodeNext source imports use runtime .js extensions. */
import {
buildSetupRequest,
buildToolRequest,
parseArgs,
type QueryCommandName,
type SetupRequest,
} from './commands.js';
import { CLI_VERSION } from './constants.js';
import { UsageError } from './errors.js';
import { callDocSearchTool } from './mcp/client.js';
import { formatToolResult, getToolResultText } from './mcp/format.js';
import type { SetupAgent, SetupScope } from './setup/agents.js';
import { PromptCancelledError, promptAgents, promptScope } from './setup/prompt.js';
import { buildAgentChoices, createPathContext, detectAgents, findProjectRoot, setupDocSearch } from './setup/setup.js';
import { renderMarkdown } from './ui/markdown.js';
import {
renderError,
renderHelp,
renderLanding,
renderNotice,
renderResultHeader,
renderSetupResults,
} from './ui/render.js';
import { startSpinner } from './ui/spinner.js';
export async function runCli(argv: string[] = process.argv.slice(2)): Promise<number> {
const [commandName, ...commandArgs] = argv;
try {
if (!commandName) {
process.stdout.write(renderLanding());
return 0;
}
if (commandName === 'help' || commandName === '--help' || commandName === '-h') {
process.stdout.write(renderHelp());
return 0;
}
if (commandName === 'version' || commandName === '--version' || commandName === '-v') {
process.stdout.write(`${CLI_VERSION}\n`);
return 0;
}
const parsedArgs = parseArgs(commandArgs);
if (parsedArgs.options['--help'] || parsedArgs.options['-h']) {
process.stdout.write(renderHelp());
return 0;
}
if (commandName === 'setup') {
return await runSetup(buildSetupRequest(parsedArgs));
}
if (isQueryCommand(commandName)) {
return await runQuery(commandName, parsedArgs);
}
throw new UsageError(`Unknown command: ${commandName}.`);
} catch (error) {
if (error instanceof PromptCancelledError) {
process.stderr.write(renderNotice('Setup cancelled. No changes were made.'));
return error.exitCode;
}
const message = error instanceof Error ? error.message : String(error);
process.stderr.write(renderError(message));
if (error instanceof UsageError) {
process.stderr.write(renderHelp());
}
return error instanceof UsageError ? 2 : 1;
}
}
async function runSetup(request: SetupRequest): Promise<number> {
const baseContext = createPathContext();
const scope = await resolveScope(request);
const cwd = scope === 'project' ? await findProjectRoot(baseContext.cwd, baseContext.homeDir) : baseContext.cwd;
const context = { ...baseContext, cwd };
const agents = await resolveSetupAgents(request, context, scope);
if (agents.length === 0) {
process.stdout.write(renderNotice('No agents selected. Re-run `docsearch setup` and pick at least one agent.'));
return 0;
}
const results = await setupDocSearch({
agents,
cwd: context.cwd,
endpoint: request.endpoint,
env: context.env,
homeDir: context.homeDir,
scope,
});
process.stdout.write(
renderSetupResults(results, {
endpoint: request.endpoint,
location: scope === 'project' ? context.cwd : undefined,
scope,
}),
);
return 0;
}
function resolveScope(request: SetupRequest): Promise<SetupScope> {
if (request.scope) {
return Promise.resolve(request.scope);
}
if (request.yes) {
return Promise.resolve('project');
}
return promptScope();
}
async function resolveSetupAgents(
request: SetupRequest,
context: ReturnType<typeof createPathContext>,
scope: SetupScope,
): Promise<SetupAgent[]> {
if (request.all) {
return buildAgentChoices(context, []).map((choice) => choice.name);
}
if (request.agents.length > 0) {
return request.agents;
}
const detected = await detectAgents(scope, context);
if (request.yes) {
if (detected.length === 0) {
throw new UsageError('No supported agents were detected. Pass an agent flag such as --cursor, or use --all.');
}
return detected;
}
return promptAgents(buildAgentChoices(context, detected));
}
async function runQuery(commandName: QueryCommandName, parsedArgs: ReturnType<typeof parseArgs>): Promise<number> {
const request = buildToolRequest(commandName, parsedArgs);
const subject = describeQuery(commandName, parsedArgs.positionals);
if (request.json) {
const result = await callDocSearchTool(request);
process.stdout.write(formatToolResult(result, true));
return result.isError === true ? 1 : 0;
}
const spinner = startSpinner(`Searching ${subject}`);
try {
const result = await callDocSearchTool(request);
if (result.isError === true) {
throw new Error(getToolResultText(result) || 'DocSearch MCP returned an error.');
}
spinner.succeed(`Results for ${subject}`);
process.stdout.write(renderResultHeader(commandName, subject));
process.stdout.write(renderMarkdown(formatToolResult(result, false)));
return 0;
} catch (error) {
spinner.fail(`Search failed for ${subject}`);
throw error;
}
}
function describeQuery(commandName: QueryCommandName, positionals: string[]): string {
if (commandName === 'docs') {
const [library] = positionals;
return library ?? 'docs';
}
return positionals.join(' ').trim() || commandName;
}
function isQueryCommand(commandName: string): commandName is QueryCommandName {
return commandName === 'docs' || commandName === 'resolve' || commandName === 'query';
}

View file

@ -0,0 +1,283 @@
/* eslint-disable import/no-unresolved -- NodeNext source imports use runtime .js extensions. */
import { DEFAULT_MCP_ENDPOINT, TOOL_QUERY_DOCS, TOOL_RESOLVE_DOCSET, TOOL_SEARCH_DOCS } from './constants.js';
import { UsageError } from './errors.js';
import type { SetupAgent, SetupScope } from './setup/agents.js';
export type QueryCommandName = 'docs' | 'query' | 'resolve';
export type CommandName = QueryCommandName | 'help' | 'setup' | 'version';
export interface ParsedArgs {
options: Record<string, boolean | string>;
positionals: string[];
}
export interface ToolRequest {
endpoint: string;
json: boolean;
toolName: string;
toolArguments: Record<string, unknown>;
}
export interface SetupRequest {
all: boolean;
agents: SetupAgent[];
endpoint: string;
scope?: SetupScope;
yes: boolean;
}
const AGENT_FLAGS: Record<string, SetupAgent> = {
'--claude': 'claude',
'--codex': 'codex',
'--cursor': 'cursor',
'--gemini': 'gemini',
'--opencode': 'opencode',
};
const BOOLEAN_OPTIONS = new Set([
'--all',
'--claude',
'--codex',
'--cursor',
'--gemini',
'--global',
'--help',
'--json',
'--opencode',
'--project',
'--yes',
'-h',
'-y',
]);
const VALUE_OPTIONS = new Set(['--endpoint', '--max-docsets', '--max-results', '--top-n']);
const HELP_OPTIONS = new Set(['--help', '-h']);
const QUERY_COMMON_OPTIONS = new Set(['--endpoint', '--help', '--json', '-h']);
const SETUP_OPTIONS = new Set([
'--all',
'--claude',
'--codex',
'--cursor',
'--endpoint',
'--gemini',
'--global',
'--help',
'--opencode',
'--project',
'--yes',
'-h',
'-y',
]);
export function parseArgs(argv: string[]): ParsedArgs {
const options: Record<string, boolean | string> = {};
const positionals: string[] = [];
for (let idx = 0; idx < argv.length; idx++) {
const token = argv[idx];
const equalsIdx = token.startsWith('--') ? token.indexOf('=') : -1;
const option = equalsIdx > 0 ? token.slice(0, equalsIdx) : token;
const inlineValue = equalsIdx > 0 ? token.slice(equalsIdx + 1) : undefined;
if (VALUE_OPTIONS.has(option)) {
const value = inlineValue ?? argv[idx + 1];
if (!value || value.startsWith('-')) {
throw new UsageError(`Missing value for ${option}.`);
}
options[option] = value;
if (inlineValue === undefined) {
idx++;
}
} else if (BOOLEAN_OPTIONS.has(option)) {
if (inlineValue !== undefined) {
throw new UsageError(`${option} does not accept a value.`);
}
options[option] = true;
} else if (option.startsWith('-')) {
throw new UsageError(`Unknown option: ${option}.`);
} else {
positionals.push(token);
}
}
return { options, positionals };
}
export function buildToolRequest(commandName: QueryCommandName, args: ParsedArgs): ToolRequest {
validateToolOptions(commandName, args.options);
const endpoint = readEndpoint(args.options);
const json = Boolean(args.options['--json']);
const maxResults = readNumberOption(args.options, '--max-results');
switch (commandName) {
case 'docs': {
const [library, ...queryParts] = args.positionals;
const query = queryParts.join(' ').trim();
if (!library || !query) {
throw new UsageError('Usage: docsearch docs <library> <query> [--json]');
}
return {
endpoint,
json,
toolName: TOOL_SEARCH_DOCS,
toolArguments: removeUndefined({
library,
query,
maxResults,
maxDocsets: readNumberOption(args.options, '--max-docsets'),
}),
};
}
case 'resolve': {
const query = args.positionals.join(' ').trim();
if (!query) {
throw new UsageError('Usage: docsearch resolve <library-or-product> [--json]');
}
return {
endpoint,
json,
toolName: TOOL_RESOLVE_DOCSET,
toolArguments: removeUndefined({
query,
topN: readNumberOption(args.options, '--top-n'),
}),
};
}
case 'query': {
const [docsetId, ...queryParts] = args.positionals;
const query = queryParts.join(' ').trim();
if (!docsetId || !query) {
throw new UsageError('Usage: docsearch query <docset-id[,docset-id...]> <query> [--json]');
}
const docsetIds = docsetId
.split(',')
.map((value) => value.trim())
.filter(Boolean);
if (docsetIds.length === 0) {
throw new UsageError('At least one non-empty docset ID is required.');
}
return {
endpoint,
json,
toolName: TOOL_QUERY_DOCS,
toolArguments: removeUndefined({
docsetIds,
query,
maxResults,
}),
};
}
default: {
const exhaustive: never = commandName;
return exhaustive;
}
}
}
export function buildSetupRequest(args: ParsedArgs): SetupRequest {
validateOptions(args.options, SETUP_OPTIONS, 'setup');
if (args.positionals.length > 0) {
throw new UsageError(`Unexpected setup argument: ${args.positionals[0]}.`);
}
const agents = Object.entries(AGENT_FLAGS)
.filter(([flag]) => Boolean(args.options[flag]))
.map(([, agent]) => agent);
if (args.options['--project'] && args.options['--global']) {
throw new UsageError('Use either --project or --global, not both.');
}
return {
agents: uniqueAgents(agents),
all: Boolean(args.options['--all']),
endpoint: readEndpoint(args.options),
scope: resolveScopeFlag(args.options),
yes: Boolean(args.options['--yes'] || args.options['-y']),
};
}
function resolveScopeFlag(options: Record<string, boolean | string>): SetupScope | undefined {
if (options['--project']) {
return 'project';
}
if (options['--global']) {
return 'global';
}
return undefined;
}
function readStringOption(options: Record<string, boolean | string>, key: string): string | undefined {
const value = options[key];
return typeof value === 'string' ? value : undefined;
}
function readEndpoint(options: Record<string, boolean | string>): string {
const value = readStringOption(options, '--endpoint') ?? DEFAULT_MCP_ENDPOINT;
let url: URL;
try {
url = new URL(value);
} catch {
throw new UsageError(`--endpoint must be a valid HTTP(S) URL.`);
}
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
throw new UsageError(`--endpoint must use HTTP or HTTPS.`);
}
return url.toString();
}
function readNumberOption(options: Record<string, boolean | string>, key: string): number | undefined {
const value = readStringOption(options, key);
if (value === undefined) {
return undefined;
}
const parsed = Number(value);
if (!Number.isInteger(parsed) || parsed < 1) {
throw new UsageError(`${key} must be a positive integer.`);
}
return parsed;
}
function removeUndefined(values: Record<string, unknown>): Record<string, unknown> {
return Object.fromEntries(Object.entries(values).filter(([, value]) => value !== undefined));
}
function uniqueAgents(agents: SetupAgent[]): SetupAgent[] {
return [...new Set(agents)];
}
function validateToolOptions(commandName: QueryCommandName, options: Record<string, boolean | string>): void {
const allowed = new Set(QUERY_COMMON_OPTIONS);
if (commandName === 'docs') {
allowed.add('--max-docsets');
allowed.add('--max-results');
} else if (commandName === 'resolve') {
allowed.add('--top-n');
} else {
allowed.add('--max-results');
}
validateOptions(options, allowed, commandName);
}
function validateOptions(
options: Record<string, boolean | string>,
allowed: ReadonlySet<string>,
commandName: string,
): void {
for (const option of Object.keys(options)) {
if (!allowed.has(option) && !HELP_OPTIONS.has(option)) {
throw new UsageError(`${option} is not valid for the ${commandName} command.`);
}
}
}

View file

@ -0,0 +1,21 @@
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
export const CLI_NAME = 'docsearch';
export const CLI_VERSION = readPackageVersion();
export const DEFAULT_MCP_ENDPOINT = 'https://mcp.algolia.com/1/docsearch/mcp';
export const DOCSEARCH_MCP_SERVER_NAME = 'algolia-docsearch';
export const TOOL_SEARCH_DOCS = 'algolia_docsearch_search_docs';
export const TOOL_RESOLVE_DOCSET = 'algolia_docsearch_resolve_docset';
export const TOOL_QUERY_DOCS = 'algolia_docsearch_query_docs';
function readPackageVersion(): string {
const currentDirectory = dirname(fileURLToPath(import.meta.url));
const packageJson = JSON.parse(readFileSync(join(currentDirectory, '..', 'package.json'), 'utf-8')) as {
version?: string;
};
return packageJson.version ?? '0.0.0';
}

View file

@ -0,0 +1,6 @@
export class UsageError extends Error {
constructor(message: string) {
super(message);
this.name = 'UsageError';
}
}

View file

@ -0,0 +1,5 @@
#!/usr/bin/env node
/* eslint-disable import/no-unresolved -- NodeNext source imports use runtime .js extensions. */
import { runCli } from './cli.js';
process.exitCode = await runCli();

View file

@ -0,0 +1,76 @@
// @vitest-environment node
/* eslint-disable max-classes-per-file -- SDK constructors are represented by minimal test doubles. */
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { callDocSearchTool } from '../client';
const mocks = vi.hoisted(() => ({
callTool: vi.fn(),
close: vi.fn(),
connect: vi.fn(),
listTools: vi.fn(),
}));
vi.mock('@modelcontextprotocol/sdk/client/index.js', () => ({
Client: class {
callTool = mocks.callTool;
close = mocks.close;
connect = mocks.connect;
listTools = mocks.listTools;
},
}));
vi.mock('@modelcontextprotocol/sdk/client/streamableHttp.js', () => ({
StreamableHTTPClientTransport: class {},
}));
beforeEach(() => {
mocks.connect.mockResolvedValue(undefined);
mocks.close.mockResolvedValue(undefined);
mocks.listTools.mockResolvedValue({
tools: [{ name: 'algolia_docsearch_resolve_docset' }],
});
mocks.callTool.mockResolvedValue({
content: [{ text: 'result', type: 'text' }],
});
});
afterEach(() => {
vi.clearAllMocks();
});
describe('callDocSearchTool', () => {
it('applies request timeouts and closes the client', async () => {
await callDocSearchTool({
endpoint: 'https://example.com/mcp',
toolArguments: { query: 'Next.js' },
toolName: 'algolia_docsearch_resolve_docset',
});
expect(mocks.listTools).toHaveBeenCalledWith(undefined, { timeout: 30_000 });
expect(mocks.callTool).toHaveBeenCalledWith(
{
arguments: { query: 'Next.js' },
name: 'algolia_docsearch_resolve_docset',
},
undefined,
{ timeout: 30_000 },
);
expect(mocks.close).toHaveBeenCalledOnce();
});
it('closes the client when connection fails', async () => {
mocks.connect.mockRejectedValue(new Error('connection failed'));
await expect(
callDocSearchTool({
endpoint: 'https://example.com/mcp',
toolArguments: {},
toolName: 'algolia_docsearch_resolve_docset',
}),
).rejects.toThrow('connection failed');
expect(mocks.close).toHaveBeenCalledOnce();
});
});

View file

@ -0,0 +1,27 @@
// @vitest-environment node
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
import { describe, expect, it } from 'vitest';
import { formatToolResult } from '../format';
describe('formatToolResult', () => {
it('prints text content for normal output', () => {
const result: CallToolResult = {
content: [
{ type: 'text', text: 'First result' },
{ type: 'text', text: 'Second result' },
],
};
expect(formatToolResult(result, false)).toBe('First result\n\nSecond result\n');
});
it('prints the raw result for JSON output', () => {
const result: CallToolResult = {
content: [{ type: 'text', text: 'Result' }],
};
expect(formatToolResult(result, true)).toBe(`${JSON.stringify(result, null, 2)}\n`);
});
});

View file

@ -0,0 +1,55 @@
/* eslint-disable import/no-unresolved -- NodeNext source imports use runtime .js extensions. */
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
import { CLI_NAME, CLI_VERSION } from '../constants.js';
const MCP_REQUEST_TIMEOUT_MS = 30_000;
export interface CallDocSearchToolOptions {
endpoint: string;
toolArguments: Record<string, unknown>;
toolName: string;
}
export async function callDocSearchTool({
endpoint,
toolArguments,
toolName,
}: CallDocSearchToolOptions): Promise<CallToolResult> {
const client = new Client({
name: CLI_NAME,
version: CLI_VERSION,
});
const transport = new StreamableHTTPClientTransport(new URL(endpoint));
try {
await client.connect(transport);
const { tools } = await client.listTools(undefined, {
timeout: MCP_REQUEST_TIMEOUT_MS,
});
if (!tools.some((tool) => tool.name === toolName)) {
throw new Error(`DocSearch MCP tool "${toolName}" is not available at ${endpoint}.`);
}
const result = await client.callTool(
{
name: toolName,
arguments: toolArguments,
},
undefined,
{
timeout: MCP_REQUEST_TIMEOUT_MS,
},
);
if (!('content' in result)) {
throw new Error(`DocSearch MCP tool "${toolName}" returned an unsupported result shape.`);
}
return result as CallToolResult;
} finally {
await client.close();
}
}

View file

@ -0,0 +1,29 @@
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
type TextContent = {
text: string;
type: 'text';
};
export function formatToolResult(result: CallToolResult, json: boolean): string {
if (json) {
return `${JSON.stringify(result, null, 2)}\n`;
}
const text = getToolResultText(result);
if (text) {
return `${text}\n`;
}
return `${JSON.stringify(result, null, 2)}\n`;
}
export function getToolResultText(result: CallToolResult): string {
const text = result.content
.filter((part): part is TextContent => part.type === 'text')
.map((part) => part.text.trim())
.filter(Boolean)
.join('\n\n');
return text;
}

View file

@ -0,0 +1,61 @@
// @vitest-environment node
import { describe, expect, it } from 'vitest';
import type { AgentChoice } from '../prompt';
import { parseAgentSelection, parseScopeSelection, PromptCancelledError } from '../prompt';
const CHOICES: AgentChoice[] = [
{ detected: true, displayName: 'Cursor', name: 'cursor' },
{ detected: false, displayName: 'Claude Code', name: 'claude' },
{ detected: false, displayName: 'Codex', name: 'codex' },
];
describe('parseAgentSelection', () => {
it('defaults blank input to detected agents only', () => {
expect(parseAgentSelection('', CHOICES)).toEqual(['cursor']);
});
it('selects every agent for "all"', () => {
expect(parseAgentSelection('all', CHOICES)).toEqual(['cursor', 'claude', 'codex']);
});
it('selects nothing for "none"', () => {
expect(parseAgentSelection('none', CHOICES)).toEqual([]);
});
it('parses a mix of numbers and names without duplicates', () => {
expect(parseAgentSelection('1, claude, 2', CHOICES)).toEqual(['cursor', 'claude']);
});
it('rejects unknown tokens', () => {
expect(() => parseAgentSelection('9', CHOICES)).toThrow(/Unknown agent/);
expect(() => parseAgentSelection('vim', CHOICES)).toThrow(/Unknown agent/);
});
});
describe('parseScopeSelection', () => {
it('defaults blank input to project', () => {
expect(parseScopeSelection('')).toBe('project');
});
it('accepts numbers, letters, and names', () => {
expect(parseScopeSelection('1')).toBe('project');
expect(parseScopeSelection('p')).toBe('project');
expect(parseScopeSelection('Project')).toBe('project');
expect(parseScopeSelection('2')).toBe('global');
expect(parseScopeSelection('g')).toBe('global');
expect(parseScopeSelection('GLOBAL')).toBe('global');
});
it('rejects unknown scope input', () => {
expect(() => parseScopeSelection('workspace')).toThrow(/Unknown scope/);
});
});
describe('PromptCancelledError', () => {
it('preserves the intended process exit code', () => {
expect(new PromptCancelledError().exitCode).toBe(0);
expect(new PromptCancelledError(130).exitCode).toBe(130);
});
});

View file

@ -0,0 +1,303 @@
// @vitest-environment node
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, describe, expect, it } from 'vitest';
import { DOCSEARCH_MCP_SERVER_NAME } from '../../constants';
import type { SetupAgent, SetupScope } from '../agents';
import { findProjectRoot, setupDocSearch, type SetupOptions } from '../setup';
const tempDirs: string[] = [];
async function createTempDir(): Promise<string> {
const path = await mkdtemp(join(tmpdir(), 'docsearch-cli-'));
tempDirs.push(path);
return path;
}
afterEach(async () => {
await Promise.all(tempDirs.splice(0).map((path) => rm(path, { force: true, recursive: true })));
});
describe('setupDocSearch', () => {
it('installs Cursor MCP config, rule, and skill in project scope', async () => {
const cwd = await createTempDir();
const homeDir = await createTempDir();
const results = await setupDocSearch({
agents: ['cursor'],
cwd,
endpoint: 'https://example.com/mcp',
env: {},
homeDir,
scope: 'project',
});
expect(results[0]).toMatchObject({
agent: 'Cursor',
mcpStatus: 'configured',
ruleStatus: 'installed',
skillStatus: 'installed',
});
const mcp = JSON.parse(await readFile(join(cwd, '.cursor', 'mcp.json'), 'utf-8')) as {
mcpServers: Record<string, { url: string }>;
};
expect(mcp.mcpServers[DOCSEARCH_MCP_SERVER_NAME]).toEqual({
url: 'https://example.com/mcp',
});
const rule = await readFile(join(cwd, '.cursor', 'rules', `${DOCSEARCH_MCP_SERVER_NAME}.mdc`), 'utf-8');
expect(rule).toContain('alwaysApply: true');
expect(rule).toContain('algolia_docsearch_search_docs');
const skill = await readFile(join(cwd, '.cursor', 'skills', DOCSEARCH_MCP_SERVER_NAME, 'SKILL.md'), 'utf-8');
expect(skill).toMatch(/^---\nname: algolia-docsearch\ndescription:/);
expect(skill).toContain('DocSearch MCP');
});
it('updates an existing Codex TOML config and shared skill idempotently', async () => {
const cwd = await createTempDir();
const homeDir = await createTempDir();
const options: SetupOptions = {
agents: ['codex'],
cwd,
endpoint: 'https://example.com/mcp',
env: {},
homeDir,
scope: 'project' as const,
};
await setupDocSearch(options);
const results = await setupDocSearch(options);
expect(results[0]).toMatchObject({
agent: 'Codex',
mcpStatus: 'updated',
ruleStatus: 'updated',
skillStatus: 'preserved',
});
const config = await readFile(join(cwd, '.codex', 'config.toml'), 'utf-8');
expect(config.match(/\[mcp_servers\.algolia-docsearch\]/g)).toHaveLength(1);
expect(config).toContain('url = "https://example.com/mcp"');
const agentsFile = await readFile(join(cwd, 'AGENTS.md'), 'utf-8');
expect(agentsFile.match(/algolia-docsearch:start/g)).toHaveLength(1);
const skill = await readFile(join(cwd, '.agents', 'skills', DOCSEARCH_MCP_SERVER_NAME, 'SKILL.md'), 'utf-8');
expect(skill).toContain('Use this skill');
});
it('preserves user edits in dedicated rule and skill files', async () => {
const cwd = await createTempDir();
const homeDir = await createTempDir();
const options: SetupOptions = {
agents: ['cursor'],
cwd,
endpoint: 'https://example.com/mcp',
env: {},
homeDir,
scope: 'project',
};
await setupDocSearch(options);
const rulePath = join(cwd, '.cursor', 'rules', `${DOCSEARCH_MCP_SERVER_NAME}.mdc`);
const skillPath = join(cwd, '.cursor', 'skills', DOCSEARCH_MCP_SERVER_NAME, 'SKILL.md');
await writeFile(rulePath, `${await readFile(rulePath, 'utf-8')}\nUser rule customization.\n`);
await writeFile(skillPath, `${await readFile(skillPath, 'utf-8')}\nUser skill customization.\n`);
const [result] = await setupDocSearch(options);
expect(result).toMatchObject({
ruleStatus: 'preserved',
skillStatus: 'preserved',
});
expect(await readFile(rulePath, 'utf-8')).toContain('User rule customization.');
expect(await readFile(skillPath, 'utf-8')).toContain('User skill customization.');
});
it('updates an existing OpenCode JSONC config in place', async () => {
const cwd = await createTempDir();
const homeDir = await createTempDir();
const configPath = join(cwd, 'opencode.jsonc');
await writeFile(
configPath,
`{
// Existing OpenCode preferences.
"theme": "system",
}
`,
);
const [result] = await setupDocSearch({
agents: ['opencode'],
cwd,
endpoint: 'https://example.com/mcp',
env: {},
homeDir,
scope: 'project',
});
expect(result.mcpPath).toBe(configPath);
expect(await readFile(configPath, 'utf-8')).toContain('// Existing OpenCode preferences.');
expect(await readFile(configPath, 'utf-8')).toContain('"algolia-docsearch"');
await expect(readFile(join(cwd, 'opencode.json'), 'utf-8')).rejects.toMatchObject({ code: 'ENOENT' });
});
});
interface AgentPathCase {
agent: SetupAgent;
mcp: string;
rule: string;
scope: SetupScope;
skill: string;
}
const AGENT_PATH_CASES: AgentPathCase[] = [
{
agent: 'claude',
mcp: '.mcp.json',
rule: join('.claude', 'rules', `${DOCSEARCH_MCP_SERVER_NAME}.md`),
scope: 'project',
skill: join('.claude', 'skills', DOCSEARCH_MCP_SERVER_NAME, 'SKILL.md'),
},
{
agent: 'cursor',
mcp: join('.cursor', 'mcp.json'),
rule: join('.cursor', 'rules', `${DOCSEARCH_MCP_SERVER_NAME}.mdc`),
scope: 'project',
skill: join('.cursor', 'skills', DOCSEARCH_MCP_SERVER_NAME, 'SKILL.md'),
},
{
agent: 'codex',
mcp: join('.codex', 'config.toml'),
rule: 'AGENTS.md',
scope: 'project',
skill: join('.agents', 'skills', DOCSEARCH_MCP_SERVER_NAME, 'SKILL.md'),
},
{
agent: 'opencode',
mcp: 'opencode.json',
rule: 'AGENTS.md',
scope: 'project',
skill: join('.agents', 'skills', DOCSEARCH_MCP_SERVER_NAME, 'SKILL.md'),
},
{
agent: 'gemini',
mcp: join('.gemini', 'settings.json'),
rule: 'GEMINI.md',
scope: 'project',
skill: join('.gemini', 'skills', DOCSEARCH_MCP_SERVER_NAME, 'SKILL.md'),
},
{
agent: 'claude',
mcp: '.claude.json',
rule: join('.claude', 'rules', `${DOCSEARCH_MCP_SERVER_NAME}.md`),
scope: 'global',
skill: join('.claude', 'skills', DOCSEARCH_MCP_SERVER_NAME, 'SKILL.md'),
},
{
agent: 'cursor',
mcp: join('.cursor', 'mcp.json'),
rule: join('.cursor', 'rules', `${DOCSEARCH_MCP_SERVER_NAME}.mdc`),
scope: 'global',
skill: join('.cursor', 'skills', DOCSEARCH_MCP_SERVER_NAME, 'SKILL.md'),
},
{
agent: 'codex',
mcp: join('.codex', 'config.toml'),
rule: join('.codex', 'AGENTS.md'),
scope: 'global',
skill: join('.agents', 'skills', DOCSEARCH_MCP_SERVER_NAME, 'SKILL.md'),
},
{
agent: 'opencode',
mcp: join('.config', 'opencode', 'opencode.json'),
rule: join('.config', 'opencode', 'AGENTS.md'),
scope: 'global',
skill: join('.agents', 'skills', DOCSEARCH_MCP_SERVER_NAME, 'SKILL.md'),
},
{
agent: 'gemini',
mcp: join('.gemini', 'settings.json'),
rule: join('.gemini', 'GEMINI.md'),
scope: 'global',
skill: join('.gemini', 'skills', DOCSEARCH_MCP_SERVER_NAME, 'SKILL.md'),
},
];
describe.each(AGENT_PATH_CASES)('$agent $scope paths', ({ agent, mcp, rule, scope, skill }) => {
it('writes every artifact to the expected location', async () => {
const cwd = await createTempDir();
const homeDir = await createTempDir();
const [result] = await setupDocSearch({
agents: [agent],
cwd,
endpoint: 'https://example.com/mcp',
env: {},
homeDir,
scope,
});
const base = scope === 'project' ? cwd : homeDir;
expect(result.mcpPath).toBe(join(base, mcp));
expect(result.rulePath).toBe(join(base, rule));
expect(result.skillPath).toBe(join(base, skill));
});
});
describe('findProjectRoot', () => {
it('walks up to the nearest VCS root', async () => {
const home = await createTempDir();
const root = join(home, 'repo');
const nested = join(root, 'packages', 'app', 'src');
await mkdir(join(root, '.git'), { recursive: true });
await mkdir(nested, { recursive: true });
expect(await findProjectRoot(nested, home)).toBe(root);
});
it('prefers a repo-root marker over a nested package.json when there is no VCS', async () => {
const home = await createTempDir();
const root = join(home, 'repo');
const pkg = join(root, 'packages', 'app');
await mkdir(pkg, { recursive: true });
await writeFile(join(root, 'pnpm-workspace.yaml'), 'packages:\n - packages/*\n');
await writeFile(join(root, 'package.json'), '{}');
await writeFile(join(pkg, 'package.json'), '{}');
expect(await findProjectRoot(pkg, home)).toBe(root);
});
it('falls back to the start directory when nothing is found', async () => {
const home = await createTempDir();
const start = join(home, 'loose', 'dir');
await mkdir(start, { recursive: true });
expect(await findProjectRoot(start, home)).toBe(start);
});
it('does not cross the home directory', async () => {
const home = await createTempDir();
const start = join(home, 'project');
await mkdir(start, { recursive: true });
// A .git in home must not be picked up.
await mkdir(join(home, '.git'), { recursive: true });
expect(await findProjectRoot(start, home)).toBe(start);
});
it('still finds a VCS root when the project is outside the home directory', async () => {
const home = await createTempDir();
const root = await createTempDir();
const nested = join(root, 'packages', 'app');
await mkdir(join(root, '.git'), { recursive: true });
await mkdir(nested, { recursive: true });
expect(await findProjectRoot(nested, home)).toBe(root);
});
});

View file

@ -0,0 +1,154 @@
// @vitest-environment node
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, describe, expect, it } from 'vitest';
import { appendTomlServer, upsertJsonServerEntry } from '../writers';
const tempDirs: string[] = [];
async function createTempDir(): Promise<string> {
const path = await mkdtemp(join(tmpdir(), 'docsearch-writers-'));
tempDirs.push(path);
return path;
}
afterEach(async () => {
await Promise.all(tempDirs.splice(0).map((path) => rm(path, { force: true, recursive: true })));
});
describe('upsertJsonServerEntry', () => {
it('preserves JSONC comments, trailing commas, and unrelated configuration', async () => {
const cwd = await createTempDir();
const path = join(cwd, 'opencode.jsonc');
await writeFile(
path,
`{
// Keep this comment.
"theme": "system",
"mcp": {
"existing": {
"type": "remote",
"url": "https://example.com/mcp",
},
},
}
`,
);
await upsertJsonServerEntry(path, 'mcp', {
enabled: true,
type: 'remote',
url: 'https://mcp.algolia.com/1/docsearch/mcp',
});
const updated = await readFile(path, 'utf-8');
expect(updated).toContain('// Keep this comment.');
expect(updated).toContain('"theme": "system"');
expect(updated).toContain('"existing"');
expect(updated).toContain('"algolia-docsearch"');
expect(updated).toContain('"url": "https://mcp.algolia.com/1/docsearch/mcp"');
});
it('preserves unknown fields on an existing DocSearch entry', async () => {
const cwd = await createTempDir();
const path = join(cwd, 'mcp.json');
await writeFile(
path,
`${JSON.stringify({
mcpServers: {
'algolia-docsearch': {
headers: { 'X-Custom': 'value' },
timeout: 15_000,
url: 'https://old.example.com/mcp',
},
},
})}\n`,
);
const result = await upsertJsonServerEntry(path, 'mcpServers', {
url: 'https://mcp.algolia.com/1/docsearch/mcp',
});
const updated = JSON.parse(await readFile(path, 'utf-8')) as {
mcpServers: Record<string, Record<string, unknown>>;
};
expect(result.alreadyExists).toBe(true);
expect(updated.mcpServers['algolia-docsearch']).toEqual({
headers: { 'X-Custom': 'value' },
timeout: 15_000,
url: 'https://mcp.algolia.com/1/docsearch/mcp',
});
});
it('rejects malformed JSONC with the file path in the error', async () => {
const cwd = await createTempDir();
const path = join(cwd, 'broken.jsonc');
await writeFile(path, '{ "mcp": {');
await expect(upsertJsonServerEntry(path, 'mcp', { url: 'https://example.com' })).rejects.toThrow(path);
});
it('rejects an existing configuration section with the wrong shape', async () => {
const cwd = await createTempDir();
const path = join(cwd, 'mcp.json');
await writeFile(path, '{ "mcpServers": [] }\n');
await expect(upsertJsonServerEntry(path, 'mcpServers', { url: 'https://example.com' })).rejects.toThrow(
'"mcpServers" must be an object',
);
});
});
describe('appendTomlServer', () => {
it('updates known values while preserving custom keys and nested tables', async () => {
const cwd = await createTempDir();
const path = join(cwd, 'config.toml');
await writeFile(
path,
`# [mcp_servers.algolia-docsearch]
[mcp_servers.algolia-docsearch]
url = "https://old.example.com/mcp" # Keep this comment.
enabled = false
[mcp_servers.algolia-docsearch.http_headers]
X-Custom = "value"
[mcp_servers.other]
url = "https://other.example.com/mcp"
`,
);
const result = await appendTomlServer(path, {
url: 'https://mcp.algolia.com/1/docsearch/mcp',
});
const updated = await readFile(path, 'utf-8');
expect(result.alreadyExists).toBe(true);
expect(updated).toContain('# [mcp_servers.algolia-docsearch]');
expect(updated).toContain('url = "https://mcp.algolia.com/1/docsearch/mcp" # Keep this comment.');
expect(updated).toContain('enabled = false');
expect(updated).toContain('[mcp_servers.algolia-docsearch.http_headers]');
expect(updated).toContain('X-Custom = "value"');
expect(updated).toContain('[mcp_servers.other]');
});
it('refuses to edit duplicate DocSearch sections', async () => {
const cwd = await createTempDir();
const path = join(cwd, 'config.toml');
await writeFile(
path,
`[mcp_servers.algolia-docsearch]
url = "https://one.example.com"
[mcp_servers.algolia-docsearch]
url = "https://two.example.com"
`,
);
await expect(appendTomlServer(path, { url: 'https://example.com' })).rejects.toThrow(/duplicate/i);
});
});

View file

@ -0,0 +1,181 @@
/* eslint-disable import/no-unresolved -- NodeNext source imports use runtime .js extensions. */
import { join } from 'node:path';
import { DOCSEARCH_MCP_SERVER_NAME } from '../constants.js';
export type SetupAgent = 'claude' | 'codex' | 'cursor' | 'gemini' | 'opencode';
export type SetupScope = 'global' | 'project';
export interface PathContext {
cwd: string;
env: NodeJS.ProcessEnv;
homeDir: string;
}
export interface AgentConfig {
detect: {
globalPaths: string[];
projectPaths: string[];
};
displayName: string;
mcp: {
configKey: string;
globalPaths: string[];
projectPaths: string[];
buildEntry: (endpoint: string) => Record<string, unknown>;
};
name: SetupAgent;
rule:
| {
dir: (scope: SetupScope) => string;
filename: string;
kind: 'file';
}
| {
file: (scope: SetupScope) => string;
kind: 'append';
};
skill: {
dir: (scope: SetupScope) => string;
};
}
export function getAgent(name: SetupAgent, context: PathContext): AgentConfig {
return agents(context)[name];
}
export function getAllAgents(context: PathContext): Record<SetupAgent, AgentConfig> {
return agents(context);
}
function agents(context: PathContext): Record<SetupAgent, AgentConfig> {
const claudeConfigDirectory = context.env.CLAUDE_CONFIG_DIR ?? join(context.homeDir, '.claude');
const claudeGlobalMcpPath = context.env.CLAUDE_CONFIG_DIR
? join(claudeConfigDirectory, '.claude.json')
: join(context.homeDir, '.claude.json');
return {
claude: {
name: 'claude',
displayName: 'Claude Code',
mcp: {
projectPaths: [join(context.cwd, '.mcp.json')],
globalPaths: [claudeGlobalMcpPath],
configKey: 'mcpServers',
buildEntry: (endpoint) => ({ type: 'http', url: endpoint }),
},
rule: {
kind: 'file',
dir: (scope) =>
scope === 'global' ? join(claudeConfigDirectory, 'rules') : join(context.cwd, '.claude', 'rules'),
filename: `${DOCSEARCH_MCP_SERVER_NAME}.md`,
},
skill: {
dir: (scope) =>
scope === 'global' ? join(claudeConfigDirectory, 'skills') : join(context.cwd, '.claude', 'skills'),
},
detect: {
projectPaths: [join(context.cwd, '.mcp.json'), join(context.cwd, '.claude')],
globalPaths: [claudeConfigDirectory, claudeGlobalMcpPath],
},
},
cursor: {
name: 'cursor',
displayName: 'Cursor',
mcp: {
projectPaths: [join(context.cwd, '.cursor', 'mcp.json')],
globalPaths: [join(context.homeDir, '.cursor', 'mcp.json')],
configKey: 'mcpServers',
buildEntry: (endpoint) => ({ url: endpoint }),
},
rule: {
kind: 'file',
dir: (scope) =>
scope === 'global' ? join(context.homeDir, '.cursor', 'rules') : join(context.cwd, '.cursor', 'rules'),
filename: `${DOCSEARCH_MCP_SERVER_NAME}.mdc`,
},
skill: {
dir: (scope) =>
scope === 'global' ? join(context.homeDir, '.cursor', 'skills') : join(context.cwd, '.cursor', 'skills'),
},
detect: {
projectPaths: [join(context.cwd, '.cursor')],
globalPaths: [join(context.homeDir, '.cursor')],
},
},
codex: {
name: 'codex',
displayName: 'Codex',
mcp: {
projectPaths: [join(context.cwd, '.codex', 'config.toml')],
globalPaths: [join(context.homeDir, '.codex', 'config.toml')],
configKey: 'mcp_servers',
buildEntry: (endpoint) => ({ url: endpoint }),
},
rule: {
kind: 'append',
file: (scope) =>
scope === 'global' ? join(context.homeDir, '.codex', 'AGENTS.md') : join(context.cwd, 'AGENTS.md'),
},
skill: {
dir: (scope) =>
scope === 'global' ? join(context.homeDir, '.agents', 'skills') : join(context.cwd, '.agents', 'skills'),
},
detect: {
projectPaths: [join(context.cwd, '.codex')],
globalPaths: [join(context.homeDir, '.codex')],
},
},
opencode: {
name: 'opencode',
displayName: 'OpenCode',
mcp: {
projectPaths: [join(context.cwd, 'opencode.json'), join(context.cwd, 'opencode.jsonc')],
globalPaths: [
join(context.homeDir, '.config', 'opencode', 'opencode.json'),
join(context.homeDir, '.config', 'opencode', 'opencode.jsonc'),
],
configKey: 'mcp',
buildEntry: (endpoint) => ({ type: 'remote', url: endpoint, enabled: true }),
},
rule: {
kind: 'append',
file: (scope) =>
scope === 'global'
? join(context.homeDir, '.config', 'opencode', 'AGENTS.md')
: join(context.cwd, 'AGENTS.md'),
},
skill: {
dir: (scope) =>
scope === 'global' ? join(context.homeDir, '.agents', 'skills') : join(context.cwd, '.agents', 'skills'),
},
detect: {
projectPaths: [join(context.cwd, 'opencode.json'), join(context.cwd, 'opencode.jsonc')],
globalPaths: [join(context.homeDir, '.config', 'opencode')],
},
},
gemini: {
name: 'gemini',
displayName: 'Gemini CLI',
mcp: {
projectPaths: [join(context.cwd, '.gemini', 'settings.json')],
globalPaths: [join(context.homeDir, '.gemini', 'settings.json')],
configKey: 'mcpServers',
buildEntry: (endpoint) => ({ httpUrl: endpoint }),
},
rule: {
kind: 'append',
file: (scope) =>
scope === 'global' ? join(context.homeDir, '.gemini', 'GEMINI.md') : join(context.cwd, 'GEMINI.md'),
},
skill: {
dir: (scope) =>
scope === 'global' ? join(context.homeDir, '.gemini', 'skills') : join(context.cwd, '.gemini', 'skills'),
},
detect: {
projectPaths: [join(context.cwd, '.gemini')],
globalPaths: [join(context.homeDir, '.gemini')],
},
},
};
}

View file

@ -0,0 +1,326 @@
/* eslint-disable import/no-unresolved -- NodeNext source imports use runtime .js extensions. */
import readline from 'node:readline';
import { UsageError } from '../errors.js';
import { color, isInteractive, symbols } from '../ui/theme.js';
import type { SetupAgent, SetupScope } from './agents.js';
export class PromptCancelledError extends Error {
constructor(readonly exitCode: number = 0) {
super('Setup cancelled.');
this.name = 'PromptCancelledError';
}
}
export interface AgentChoice {
detected: boolean;
displayName: string;
name: SetupAgent;
}
interface ScopeChoice {
hint: string;
label: string;
value: SetupScope;
}
const SCOPE_CHOICES: ScopeChoice[] = [
{ hint: 'the detected repository root', label: 'Project', value: 'project' },
{ hint: 'every project (~/.cursor, ~/.codex, …)', label: 'Global', value: 'global' },
];
export function parseScopeSelection(raw: string): SetupScope {
const normalized = raw.trim().toLowerCase();
if (normalized === '' || normalized === '1' || normalized === 'p' || normalized === 'project') {
return 'project';
}
if (normalized === '2' || normalized === 'g' || normalized === 'global') {
return 'global';
}
throw new UsageError(`Unknown scope: "${raw.trim()}". Choose "project" or "global".`);
}
export function promptScope(): Promise<SetupScope> {
if (isInteractive()) {
return promptScopeInteractive();
}
return promptScopeByLine();
}
function promptScopeInteractive(): Promise<SetupScope> {
const input = process.stdin;
const output = process.stderr;
const wasRaw = input.isRaw === true;
let cursor = 0;
let renderedLines = 0;
try {
input.setRawMode(true);
} catch {
return promptScopeByLine();
}
readline.emitKeypressEvents(input);
return new Promise<SetupScope>((resolve, reject) => {
function render(): void {
if (renderedLines > 0) {
output.write(`\u001B[${renderedLines}A`);
}
const lines = [
`${color.magenta(symbols.diamond)} ${color.bold('Where should DocSearch be installed?')} ${color.dim('(↑↓ move · enter confirm)')}`,
...SCOPE_CHOICES.map((choice, index) => {
const active = index === cursor;
const radio = active ? color.green(symbols.checkboxOn) : color.dim(symbols.checkboxOff);
const pointer = active ? color.cyan(symbols.pointer) : ' ';
const label = active ? color.cyan(choice.label) : choice.label;
return `${pointer} ${radio} ${label} ${color.dim(`${choice.hint}`)}`;
}),
];
output.write(`${lines.map((line) => `\u001B[K${line}`).join('\n')}\n`);
renderedLines = lines.length;
}
function cleanup(): void {
input.setRawMode(wasRaw);
input.removeListener('keypress', onKeypress);
output.write('\u001B[?25h');
input.pause();
}
function onKeypress(_input: string, key: readline.Key | undefined): void {
if (!key) {
return;
}
if (key.name === 'up' || key.name === 'k') {
cursor = (cursor - 1 + SCOPE_CHOICES.length) % SCOPE_CHOICES.length;
render();
} else if (key.name === 'down' || key.name === 'j') {
cursor = (cursor + 1) % SCOPE_CHOICES.length;
render();
} else if (key.name === 'return') {
cleanup();
resolve(SCOPE_CHOICES[cursor].value);
} else if (key.name === 'escape' || (key.ctrl === true && key.name === 'c')) {
cleanup();
output.write('\n');
reject(new PromptCancelledError(key.ctrl === true ? 130 : 0));
}
}
output.write('\u001B[?25l');
input.on('keypress', onKeypress);
input.resume();
render();
});
}
async function promptScopeByLine(): Promise<SetupScope> {
const output = process.stderr;
output.write(`${color.magenta(symbols.diamond)} ${color.bold('Where should DocSearch be installed?')}\n`);
for (const [index, choice] of SCOPE_CHOICES.entries()) {
output.write(` ${color.cyan(String(index + 1))}. ${choice.label} ${color.dim(`${choice.hint}`)}\n`);
}
output.write(`${color.dim('Enter "project" or "global" (blank for project):')} `);
return parseScopeSelection(await readSingleLine());
}
export function parseAgentSelection(raw: string, choices: AgentChoice[]): SetupAgent[] {
const normalized = raw.trim().toLowerCase();
if (normalized === '') {
return choices.filter((choice) => choice.detected).map((choice) => choice.name);
}
if (normalized === 'all' || normalized === '*') {
return choices.map((choice) => choice.name);
}
if (normalized === 'none') {
return [];
}
const selected = new Set<SetupAgent>();
for (const token of normalized.split(/[\s,]+/).filter(Boolean)) {
const index = Number(token);
if (Number.isInteger(index) && index >= 1 && index <= choices.length) {
selected.add(choices[index - 1].name);
} else {
const match = choices.find((choice) => choice.name === token);
if (!match) {
throw new UsageError(
`Unknown agent: "${token}". Choose a number or one of: ${choices.map((c) => c.name).join(', ')}.`,
);
}
selected.add(match.name);
}
}
return [...selected];
}
export function promptAgents(choices: AgentChoice[]): Promise<SetupAgent[]> {
if (isInteractive()) {
return promptAgentsInteractive(choices);
}
return promptAgentsByLine(choices);
}
function promptAgentsInteractive(choices: AgentChoice[]): Promise<SetupAgent[]> {
const input = process.stdin;
const output = process.stderr;
const wasRaw = input.isRaw === true;
const selected = choices.map((choice) => choice.detected);
let cursor = 0;
let renderedLines = 0;
try {
input.setRawMode(true);
} catch {
return promptAgentsByLine(choices);
}
readline.emitKeypressEvents(input);
return new Promise<SetupAgent[]>((resolve, reject) => {
function render(): void {
if (renderedLines > 0) {
output.write(`\u001B[${renderedLines}A`);
}
const lines = [
`${color.magenta(symbols.diamond)} ${color.bold('Select agents to configure')} ${color.dim('(↑↓ move · space toggle · a all · enter confirm)')}`,
...choices.map((choice, index) => {
const box = selected[index] ? color.green(symbols.checkboxOn) : color.dim(symbols.checkboxOff);
const pointer = index === cursor ? color.cyan(symbols.pointer) : ' ';
const name = index === cursor ? color.cyan(choice.displayName) : choice.displayName;
const detected = choice.detected ? color.dim(' (detected)') : '';
return `${pointer} ${box} ${name}${detected}`;
}),
];
output.write(`${lines.map((line) => `\u001B[K${line}`).join('\n')}\n`);
renderedLines = lines.length;
}
function cleanup(): void {
input.setRawMode(wasRaw);
input.removeListener('keypress', onKeypress);
output.write('\u001B[?25h');
input.pause();
}
function onKeypress(_input: string, key: readline.Key | undefined): void {
if (!key) {
return;
}
if (key.name === 'up' || key.name === 'k') {
cursor = (cursor - 1 + choices.length) % choices.length;
render();
} else if (key.name === 'down' || key.name === 'j') {
cursor = (cursor + 1) % choices.length;
render();
} else if (key.name === 'space') {
selected[cursor] = !selected[cursor];
render();
} else if (key.name === 'a') {
const allSelected = selected.every(Boolean);
selected.fill(!allSelected);
render();
} else if (key.name === 'return') {
cleanup();
resolve(choices.filter((_, index) => selected[index]).map((choice) => choice.name));
} else if (key.name === 'escape' || (key.ctrl === true && key.name === 'c')) {
cleanup();
output.write('\n');
reject(new PromptCancelledError(key.ctrl === true ? 130 : 0));
}
}
output.write('\u001B[?25l');
input.on('keypress', onKeypress);
input.resume();
render();
});
}
async function promptAgentsByLine(choices: AgentChoice[]): Promise<SetupAgent[]> {
const output = process.stderr;
output.write(`${color.magenta(symbols.diamond)} ${color.bold('Select agents to configure')}\n`);
for (const [index, choice] of choices.entries()) {
const detected = choice.detected ? color.dim(' (detected)') : '';
output.write(` ${color.cyan(String(index + 1))}. ${choice.displayName}${detected}\n`);
}
output.write(`${color.dim('Enter numbers/names (comma-separated), "all", or blank for detected:')} `);
const raw = await readSingleLine();
return parseAgentSelection(raw, choices);
}
// A single persistent line reader shared across prompts. Reading one line at a
// time with fresh listeners loses buffered input between prompts (and can hang
// at EOF), so we buffer stdin once and hand out lines on demand.
let stdinBuffer = '';
let stdinInitialized = false;
let stdinEnded = false;
const lineResolvers: Array<(line: string) => void> = [];
function pumpLines(): void {
while (lineResolvers.length > 0) {
const newline = stdinBuffer.indexOf('\n');
if (newline >= 0) {
const line = stdinBuffer.slice(0, newline).replace(/\r$/, '');
stdinBuffer = stdinBuffer.slice(newline + 1);
lineResolvers.shift()?.(line);
} else if (stdinEnded) {
const line = stdinBuffer;
stdinBuffer = '';
lineResolvers.shift()?.(line);
} else {
break;
}
}
if (lineResolvers.length === 0 && !stdinEnded) {
process.stdin.pause();
}
}
function initStdin(): void {
if (stdinInitialized) {
return;
}
stdinInitialized = true;
process.stdin.setEncoding('utf8');
process.stdin.on('data', (chunk: string) => {
stdinBuffer += chunk;
pumpLines();
});
process.stdin.on('end', () => {
stdinEnded = true;
pumpLines();
});
}
function readSingleLine(): Promise<string> {
initStdin();
return new Promise((resolve) => {
lineResolvers.push(resolve);
process.stdin.resume();
pumpLines();
});
}

View file

@ -0,0 +1,220 @@
/* eslint-disable import/no-unresolved -- NodeNext source imports use runtime .js extensions. */
import { homedir } from 'node:os';
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
import { DOCSEARCH_MCP_SERVER_NAME } from '../constants.js';
import {
getAgent,
getAllAgents,
type AgentConfig,
type PathContext,
type SetupAgent,
type SetupScope,
} from './agents.js';
import type { AgentChoice } from './prompt.js';
import { getRuleContent, getSkillContent } from './templates.js';
import {
appendRuleSection,
appendTomlServer,
pathExists,
resolveConfigPath,
upsertJsonServerEntry,
writeRuleFile,
writeSkill,
} from './writers.js';
export interface SetupOptions {
agents: SetupAgent[];
cwd?: string;
endpoint: string;
env?: NodeJS.ProcessEnv;
homeDir?: string;
scope: SetupScope;
}
export interface SetupResult {
agent: string;
mcpPath: string;
mcpStatus: string;
rulePath: string;
ruleStatus: string;
skillPath: string;
skillStatus: string;
}
export function createPathContext(overrides: Partial<PathContext> = {}): PathContext {
return {
cwd: overrides.cwd ?? process.cwd(),
env: overrides.env ?? process.env,
homeDir: overrides.homeDir ?? homedir(),
};
}
// Version control roots are the canonical project root: they match how Cursor,
// Claude Code, Codex, etc. resolve project config (from the repo/workspace top).
const VCS_MARKERS = ['.git', '.hg', '.svn'];
// Markers that only exist at the true repo root, so they beat a nested
// package.json when locating a monorepo root without VCS metadata.
const ROOT_MARKERS = [
'pnpm-workspace.yaml',
'bun.lock',
'bun.lockb',
'pnpm-lock.yaml',
'package-lock.json',
'yarn.lock',
'go.work',
'deno.json',
'deno.jsonc',
'turbo.json',
'nx.json',
'lerna.json',
];
// Walks up from `startDir` (never crossing `homeDir` or the filesystem root)
// and returns the best project root. Falls back to `startDir` when nothing is
// found, so a directory-less invocation still installs into the current folder.
export async function findProjectRoot(startDir: string, homeDir: string): Promise<string> {
const start = resolve(startDir);
const home = resolve(homeDir);
const stopBeforeHome = start !== home && isPathWithin(start, home);
let vcsRoot: string | undefined;
let rootMarkerDir: string | undefined;
let topmostPackage: string | undefined;
let dir = start;
let reachedFileSystemRoot = false;
while (!reachedFileSystemRoot) {
if (stopBeforeHome && dir === home) {
break;
}
if (vcsRoot === undefined && (await hasAnyMarker(dir, VCS_MARKERS))) {
vcsRoot = dir;
}
if (rootMarkerDir === undefined && (await hasAnyMarker(dir, ROOT_MARKERS))) {
rootMarkerDir = dir;
}
if (await pathExists(join(dir, 'package.json'))) {
topmostPackage = dir;
}
const parent = dirname(dir);
reachedFileSystemRoot = parent === dir;
dir = parent;
}
return vcsRoot ?? rootMarkerDir ?? topmostPackage ?? start;
}
async function hasAnyMarker(dir: string, markers: string[]): Promise<boolean> {
const found = await Promise.all(markers.map((marker) => pathExists(join(dir, marker))));
return found.some(Boolean);
}
function isPathWithin(path: string, parent: string): boolean {
const pathFromParent = relative(parent, path);
return (
pathFromParent !== '..' &&
!pathFromParent.startsWith(`..${process.platform === 'win32' ? '\\' : '/'}`) &&
!isAbsolute(pathFromParent)
);
}
export async function setupDocSearch(options: SetupOptions): Promise<SetupResult[]> {
const context = createPathContext({
cwd: options.cwd,
env: options.env,
homeDir: options.homeDir,
});
const results: SetupResult[] = [];
for (const agentName of options.agents) {
results.push(
await setupAgent(getAgent(agentName, context), {
endpoint: options.endpoint,
scope: options.scope,
}),
);
}
return results;
}
export async function detectAgents(scope: SetupScope, context: PathContext): Promise<SetupAgent[]> {
const detected: SetupAgent[] = [];
const allAgents = getAllAgents(context);
for (const agent of Object.values(allAgents)) {
const paths = scope === 'global' ? agent.detect.globalPaths : agent.detect.projectPaths;
for (const path of paths) {
if (await pathExists(path)) {
detected.push(agent.name);
break;
}
}
}
return detected;
}
export function buildAgentChoices(context: PathContext, detected: SetupAgent[]): AgentChoice[] {
const detectedSet = new Set(detected);
return Object.values(getAllAgents(context)).map((agent) => ({
detected: detectedSet.has(agent.name),
displayName: agent.displayName,
name: agent.name,
}));
}
async function setupAgent(agent: AgentConfig, options: { endpoint: string; scope: SetupScope }): Promise<SetupResult> {
const mcpPath = await installMcpConfig(agent, options.endpoint, options.scope);
const rulePath = await installRule(agent, options.scope);
const skill = await writeSkill(agent.skill.dir(options.scope), getSkillContent());
return {
agent: agent.displayName,
mcpPath: mcpPath.path,
mcpStatus: mcpPath.alreadyExists ? 'updated' : 'configured',
rulePath: rulePath.path,
ruleStatus: rulePath.status,
skillPath: skill.path,
skillStatus: skill.alreadyExists ? 'preserved' : 'installed',
};
}
async function installMcpConfig(
agent: AgentConfig,
endpoint: string,
scope: SetupScope,
): Promise<{ alreadyExists: boolean; path: string }> {
const candidates = scope === 'global' ? agent.mcp.globalPaths : agent.mcp.projectPaths;
const mcpPath = await resolveConfigPath(candidates);
const entry = agent.mcp.buildEntry(endpoint);
if (mcpPath.endsWith('.toml')) {
const result = await appendTomlServer(mcpPath, entry);
return { alreadyExists: result.alreadyExists, path: mcpPath };
}
const result = await upsertJsonServerEntry(mcpPath, agent.mcp.configKey, entry);
return { alreadyExists: result.alreadyExists, path: mcpPath };
}
async function installRule(agent: AgentConfig, scope: SetupScope): Promise<{ path: string; status: string }> {
const content = getRuleContent(agent.name);
if (agent.rule.kind === 'file') {
const rulePath = join(agent.rule.dir(scope), agent.rule.filename);
const result = await writeRuleFile(rulePath, content);
return { path: rulePath, status: result.alreadyExists ? 'preserved' : 'installed' };
}
const rulePath = agent.rule.file(scope);
const existed = await pathExists(rulePath);
await appendRuleSection(rulePath, content);
return { path: rulePath, status: existed ? 'updated' : 'installed' };
}
export { DOCSEARCH_MCP_SERVER_NAME };

View file

@ -0,0 +1,57 @@
/* eslint-disable import/no-unresolved -- NodeNext source imports use runtime .js extensions. */
import { DOCSEARCH_MCP_SERVER_NAME } from '../constants.js';
import type { SetupAgent } from './agents.js';
const BASE_GUIDANCE = `Use DocSearch MCP when the user asks about public developer documentation for a library, framework, SDK, API, CLI tool, or cloud service.
Prefer the one-shot \`algolia_docsearch_search_docs\` tool for normal lookups:
1. Put the official product, library, SDK, or platform name in \`library\`.
2. Put the actual documentation question in \`query\`.
3. Answer only from returned documentation chunks and include source links when available.
Use \`algolia_docsearch_resolve_docset\` followed by \`algolia_docsearch_query_docs\` when a question spans multiple products or when the right documentation set is ambiguous.
Do not use DocSearch MCP for general programming concepts, local code review, or business logic that is not answered by public docs.`;
const CURSOR_FRONTMATTER = `---\nalwaysApply: true\n---\n\n`;
export function getRuleContent(agent: SetupAgent): string {
if (agent === 'cursor') {
return `${CURSOR_FRONTMATTER}${BASE_GUIDANCE}\n`;
}
return `${BASE_GUIDANCE}\n`;
}
export function getSkillContent(): string {
return `---
name: ${DOCSEARCH_MCP_SERVER_NAME}
description: Search current public developer documentation through DocSearch MCP. Use for library, framework, SDK, API, CLI, and cloud service documentation questions.
---
# DocSearch MCP
Use this skill to fetch current public developer documentation through DocSearch MCP before answering documentation questions.
## When to use
- The user asks about a library, framework, SDK, API, CLI tool, or cloud service.
- The answer depends on current public docs, configuration, API syntax, migration steps, or version-specific behavior.
- The user explicitly says to use DocSearch MCP.
## Steps
1. For most questions, call \`algolia_docsearch_search_docs\`.
- \`library\`: official product or vendor/library name, for example \`Next.js\`, \`Stripe\`, or \`Algolia InstantSearch\`.
- \`query\`: the user's actual documentation question.
2. If the library is ambiguous, call \`algolia_docsearch_resolve_docset\`, pick the best \`docset_id\`, then call \`algolia_docsearch_query_docs\`.
3. Use separate focused queries for unrelated topics. Combined broad queries dilute ranking.
4. Answer from retrieved chunks only. Include source URLs when present.
## Fallback
If DocSearch MCP returns no useful results, retry with the official product name and a narrower query. If it still returns nothing, say that the answer was not found in the indexed docs.
`;
}

View file

@ -0,0 +1,308 @@
/* eslint-disable import/no-unresolved -- NodeNext source imports use runtime .js extensions. */
import { access, mkdir, readFile, realpath, rename, rm, stat, writeFile } from 'node:fs/promises';
import { dirname, join } from 'node:path';
import { applyEdits, modify, parse, printParseErrorCode, type FormattingOptions, type ParseError } from 'jsonc-parser';
import { DOCSEARCH_MCP_SERVER_NAME } from '../constants.js';
import { UsageError } from '../errors.js';
const SECTION_START = '<!-- algolia-docsearch:start -->';
const SECTION_END = '<!-- algolia-docsearch:end -->';
export async function pathExists(path: string): Promise<boolean> {
try {
await access(path);
return true;
} catch (error) {
if (isMissingFileError(error)) {
return false;
}
throw error;
}
}
export async function resolveConfigPath(candidates: string[]): Promise<string> {
for (const candidate of candidates) {
if (await pathExists(candidate)) {
return candidate;
}
}
return candidates[0];
}
export async function upsertJsonServerEntry(
filePath: string,
configKey: string,
entry: Record<string, unknown>,
): Promise<{ alreadyExists: boolean }> {
const existing = await readTextFile(filePath);
const source = existing.trim() === '' ? '{}\n' : existing;
const errors: ParseError[] = [];
const parsed = parse(source, errors, {
allowTrailingComma: true,
disallowComments: false,
}) as unknown;
if (errors.length > 0) {
const firstError = errors[0];
throw new UsageError(
`Cannot update ${filePath}: invalid JSON/JSONC (${printParseErrorCode(firstError.error)} at offset ${firstError.offset}).`,
);
}
if (!isPlainObject(parsed)) {
throw new UsageError(`Cannot update ${filePath}: the configuration root must be an object.`);
}
const section = parsed[configKey];
if (section !== undefined && !isPlainObject(section)) {
throw new UsageError(`Cannot update ${filePath}: "${configKey}" must be an object.`);
}
const serverEntry = section?.[DOCSEARCH_MCP_SERVER_NAME];
if (serverEntry !== undefined && !isPlainObject(serverEntry)) {
throw new UsageError(`Cannot update ${filePath}: "${configKey}.${DOCSEARCH_MCP_SERVER_NAME}" must be an object.`);
}
const alreadyExists = serverEntry !== undefined;
const mergedEntry = {
...(serverEntry ?? {}),
...entry,
};
const edits = modify(source, [configKey, DOCSEARCH_MCP_SERVER_NAME], mergedEntry, {
formattingOptions: inferFormatting(source),
});
const updated = ensureFinalNewline(applyEdits(source, edits));
await writeFileAtomic(filePath, updated);
return { alreadyExists };
}
export function buildTomlServerBlock(entry: Record<string, unknown>): string {
return [
`[mcp_servers.${DOCSEARCH_MCP_SERVER_NAME}]`,
...Object.entries(entry).map(([key, value]) => `${key} = ${JSON.stringify(value)}`),
'',
].join('\n');
}
export async function appendTomlServer(
filePath: string,
entry: Record<string, unknown>,
): Promise<{ alreadyExists: boolean }> {
const existing = await readTextFile(filePath);
const block = buildTomlServerBlock(entry);
const sectionHeader = `[mcp_servers.${DOCSEARCH_MCP_SERVER_NAME}]`;
const headerRegex = new RegExp(`^${escapeRegExp(sectionHeader)}[ \\t]*$`, 'gm');
const matches = [...existing.matchAll(headerRegex)];
if (matches.length > 1) {
throw new UsageError(`Cannot update ${filePath}: duplicate ${sectionHeader} sections.`);
}
const alreadyExists = matches.length === 1;
const content = alreadyExists
? mergeTomlSection(existing, matches[0], entry, filePath)
: appendBlock(existing, block);
await writeFileAtomic(filePath, content);
return { alreadyExists };
}
export async function writeRuleFile(filePath: string, content: string): Promise<{ alreadyExists: boolean }> {
const alreadyExists = await pathExists(filePath);
if (!alreadyExists) {
await writeFileAtomic(filePath, content);
}
return { alreadyExists };
}
export async function appendRuleSection(filePath: string, content: string): Promise<void> {
const existing = await readTextFile(filePath);
const hasStart = existing.includes(SECTION_START);
const hasEnd = existing.includes(SECTION_END);
if (hasStart !== hasEnd) {
throw new UsageError(`Cannot update ${filePath}: incomplete DocSearch managed section.`);
}
const section = `${SECTION_START}\n${content.trim()}\n${SECTION_END}`;
const updated =
hasStart && hasEnd
? existing.replace(new RegExp(`${escapeRegExp(SECTION_START)}[\\s\\S]*?${escapeRegExp(SECTION_END)}`), section)
: appendBlock(existing, `${section}\n`);
await writeFileAtomic(filePath, updated);
}
export async function writeSkill(skillDir: string, content: string): Promise<{ alreadyExists: boolean; path: string }> {
const skillPath = join(skillDir, DOCSEARCH_MCP_SERVER_NAME, 'SKILL.md');
const alreadyExists = await pathExists(skillPath);
if (!alreadyExists) {
await writeFileAtomic(skillPath, content);
}
return { alreadyExists, path: skillPath };
}
function mergeTomlSection(
existing: string,
headerMatch: RegExpMatchArray,
entry: Record<string, unknown>,
filePath: string,
): string {
const startIdx = headerMatch.index;
if (startIdx === undefined) {
throw new UsageError(`Cannot update ${filePath}: failed to locate the DocSearch MCP section.`);
}
const lineBreak = existing.includes('\r\n') ? '\r\n' : '\n';
const headerEnd = startIdx + headerMatch[0].length;
const rest = existing.slice(headerEnd);
const nextHeader = /^[ \t]*\[[^\]\r\n]+\][ \t]*(?:#.*)?$/m.exec(rest);
const endIdx = nextHeader?.index === undefined ? existing.length : headerEnd + nextHeader.index;
const lines = existing.slice(startIdx, endIdx).replace(/\s+$/, '').split(/\r?\n/);
for (const [key, value] of Object.entries(entry)) {
const keyRegex = new RegExp(`^(\\s*)${escapeRegExp(key)}\\s*=`);
const matchingLines = lines
.map((line, index) => ({ index, match: keyRegex.exec(line) }))
.filter((candidate) => candidate.match !== null);
if (matchingLines.length > 1) {
throw new UsageError(`Cannot update ${filePath}: duplicate "${key}" values in the DocSearch MCP section.`);
}
const replacement = `${key} = ${toTomlValue(value, filePath)}`;
const matchingLine = matchingLines[0];
if (matchingLine?.match) {
const comment = readTomlInlineComment(lines[matchingLine.index]);
lines[matchingLine.index] = `${matchingLine.match[1]}${replacement}${comment}`;
} else {
lines.push(replacement);
}
}
const mergedSection = `${lines.join(lineBreak)}${lineBreak}`;
return ensureFinalNewline(`${existing.slice(0, startIdx)}${mergedSection}${existing.slice(endIdx)}`);
}
function toTomlValue(value: unknown, filePath: string): string {
if (typeof value === 'string' || typeof value === 'boolean' || typeof value === 'number') {
return JSON.stringify(value);
}
throw new UsageError(`Cannot update ${filePath}: unsupported TOML value in the DocSearch MCP configuration.`);
}
function readTomlInlineComment(line: string): string {
let quote: '"' | "'" | undefined;
let escaped = false;
for (let index = 0; index < line.length; index++) {
const character = line[index];
if (escaped) {
escaped = false;
} else if (quote === '"' && character === '\\') {
escaped = true;
} else if (quote && character === quote) {
quote = undefined;
} else if (!quote && (character === '"' || character === "'")) {
quote = character;
} else if (!quote && character === '#') {
const whitespace = line.slice(0, index).match(/[ \t]*$/)?.[0] ?? '';
return `${whitespace || ' '}${line.slice(index)}`;
}
}
return '';
}
function appendBlock(existing: string, block: string): string {
const lineBreak = existing.includes('\r\n') ? '\r\n' : '\n';
const normalizedBlock = block.replace(/\r?\n/g, lineBreak);
if (!existing) {
return normalizedBlock.endsWith(lineBreak) ? normalizedBlock : `${normalizedBlock}${lineBreak}`;
}
const separator = existing.endsWith(lineBreak) ? lineBreak : `${lineBreak}${lineBreak}`;
return `${existing}${separator}${
normalizedBlock.endsWith(lineBreak) ? normalizedBlock : `${normalizedBlock}${lineBreak}`
}`;
}
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function inferFormatting(source: string): FormattingOptions {
const lineBreak = source.includes('\r\n') ? '\r\n' : '\n';
const indentation = source.match(/^[ \t]+(?=")/m)?.[0];
const insertSpaces = indentation?.includes('\t') !== true;
return {
eol: lineBreak,
insertSpaces,
tabSize: insertSpaces ? (indentation?.length ?? 2) : 1,
};
}
function isPlainObject(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}
function ensureFinalNewline(content: string): string {
if (content.endsWith('\n')) {
return content;
}
return `${content}\n`;
}
async function readTextFile(filePath: string): Promise<string> {
try {
return await readFile(filePath, 'utf-8');
} catch (error) {
if (isMissingFileError(error)) {
return '';
}
throw error;
}
}
async function writeFileAtomic(filePath: string, content: string): Promise<void> {
const targetPath = await resolveExistingPath(filePath);
await mkdir(dirname(targetPath), { recursive: true });
let mode: number | undefined;
try {
mode = (await stat(targetPath)).mode % 0o1000;
} catch (error) {
if (!isMissingFileError(error)) {
throw error;
}
}
const tempPath = `${targetPath}.${process.pid}.${Date.now()}.tmp`;
try {
await writeFile(tempPath, content, {
encoding: 'utf-8',
mode,
});
await rename(tempPath, targetPath);
} finally {
await rm(tempPath, { force: true });
}
}
async function resolveExistingPath(filePath: string): Promise<string> {
try {
return await realpath(filePath);
} catch (error) {
if (isMissingFileError(error)) {
return filePath;
}
throw error;
}
}
function isMissingFileError(error: unknown): error is NodeJS.ErrnoException {
return error instanceof Error && 'code' in error && error.code === 'ENOENT';
}

View file

@ -0,0 +1,56 @@
// @vitest-environment node
import { describe, expect, it } from 'vitest';
import { renderMarkdown } from '../markdown';
describe('renderMarkdown', () => {
it('renders headings, sources, bullets, and dividers as plain text when color is disabled', () => {
const input = [
'Searched docsets:',
'',
'### Configure props',
'',
'Source: https://example.com/docs',
'',
'- `apiKey`: Your key.',
'- Read the [configuration guide](https://example.com/config).',
'--------------------------------',
].join('\n');
const output = renderMarkdown(input);
expect(output).toContain('Configure props');
expect(output).toContain('https://example.com/docs');
expect(output).toContain('apiKey');
expect(output).toContain('https://example.com/config');
expect(output).not.toContain('###');
expect(output).toMatch(/─{10,}/);
});
it('renders resolve metadata as a card with a score bar', () => {
const input = [
'- title: Next.js Docs',
'- docset_id: repo/vercel/next.js',
'- description: Official documentation.',
'- trustScore: 100',
].join('\n');
const output = renderMarkdown(input);
expect(output).toContain('Next.js Docs');
expect(output).toContain('repo/vercel/next.js');
expect(output).toContain('trust');
expect(output).toContain('100');
expect(output).toMatch(/[▰▱#-]{10}/);
});
it('keeps code fences as a bordered block without backticks', () => {
const input = ['```ts', 'const x = 1;', '```'].join('\n');
const output = renderMarkdown(input);
expect(output).toContain('const x = 1;');
expect(output).not.toContain('```');
});
});

View file

@ -0,0 +1,34 @@
// @vitest-environment node
import { describe, expect, it } from 'vitest';
import { renderHelp, renderLanding, renderLogo } from '../render';
describe('renderLanding', () => {
it('includes the logo, tagline, commands, and discovery link', () => {
const output = renderLanding();
expect(output).toContain('▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓');
expect(output).toContain('The open documentation search for AI agents');
expect(output).toContain('setup');
expect(output).toContain('docs');
expect(output).toContain('resolve');
expect(output).toContain('query');
expect(output).toContain('https://docsearch.algolia.com');
});
it('renders the logo as multi-line art', () => {
expect(renderLogo().slice(1, -1).split('\n')).toHaveLength(21);
});
it('documents setup and query flags', () => {
const output = renderHelp();
expect(output).toContain('--all');
expect(output).toContain('--cursor');
expect(output).toContain('--max-results');
expect(output).toContain('--max-docsets');
expect(output).toContain('--top-n');
expect(output).toContain('project + detected agents');
});
});

View file

@ -0,0 +1,196 @@
/* eslint-disable import/no-unresolved -- NodeNext source imports use runtime .js extensions. */
import { color, symbols } from './theme.js';
const CODE_FENCE = /^```(.*)$/;
const HORIZONTAL_RULE = /^\s*([-*_]|─){3,}\s*$/;
const HEADING = /^(#{1,6})\s+(.*)$/;
const SOURCE = /^Source:\s+(.+)$/;
const BULLET = /^(\s*)[-*]\s+(.+)$/;
const META_FIELD = /^([A-Za-z][\w]*):\s+([\s\S]*)$/;
const SECTION_LABEL = /^[A-Z][^:`]*:\s*$/;
const TABLE_ROW = /^\s*\|.*\|\s*$/;
const TABLE_SEPARATOR = /^\s*\|[\s:|-]+\|\s*$/;
const MAX_WIDTH = 88;
const RULE_WIDTH = 56;
export function renderMarkdown(text: string): string {
const width = Math.max(40, Math.min(process.stdout.columns ?? 80, MAX_WIDTH));
const lines = text.replace(/\n+$/, '').split('\n');
const output: string[] = [];
let inCodeBlock = false;
for (const line of lines) {
const fence = line.match(CODE_FENCE);
if (fence) {
output.push(inCodeBlock ? renderCodeBottom() : renderCodeTop(fence[1].trim()));
inCodeBlock = !inCodeBlock;
} else if (inCodeBlock) {
output.push(renderCodeLine(line));
} else {
output.push(...renderProseLine(line, width));
}
}
return `${output.join('\n')}\n`;
}
function renderProseLine(line: string, width: number): string[] {
if (line.trim() === '') {
return [''];
}
if (HORIZONTAL_RULE.test(line)) {
return [color.dim(symbols.rule.repeat(RULE_WIDTH))];
}
const heading = line.match(HEADING);
if (heading) {
return [`${color.magenta(symbols.accent)} ${color.bold(color.cyan(heading[2]))}`];
}
const source = line.match(SOURCE);
if (source) {
return [` ${color.dim(symbols.link)} ${color.underline(color.blue(source[1]))}`];
}
const bullet = line.match(BULLET);
if (bullet) {
return renderBullet(bullet[2], width);
}
if (TABLE_ROW.test(line)) {
return [renderTableRow(line)];
}
if (SECTION_LABEL.test(line)) {
return [color.bold(line)];
}
return wrap(line, width).map((part) => styleInline(part));
}
function renderBullet(content: string, width: number): string[] {
const field = content.match(META_FIELD);
if (field && !content.startsWith('`')) {
return renderMetaField(field[1], field[2], width);
}
const marker = color.cyan(symbols.bullet);
return wrap(content, width - 2).map((part, index) =>
index === 0 ? `${marker} ${styleInline(part)}` : ` ${styleInline(part)}`,
);
}
function renderMetaField(key: string, value: string, width: number): string[] {
if (key === 'title') {
return [`${color.magenta(symbols.diamond)} ${color.bold(value)}`];
}
if (key === 'docset_id') {
return [` ${color.dim('id')} ${color.cyan(value)}`];
}
if (/score$/i.test(key)) {
return [renderScore(key.replace(/score$/i, ''), value)];
}
if (key === 'description') {
return wrap(value, width - 2).map((part) => ` ${color.dim(part)}`);
}
const indent = ' '.repeat(key.length + 4);
return wrap(value, width - key.length - 4).map((part, index) =>
index === 0 ? ` ${color.dim(key)} ${styleInline(part)}` : `${indent}${styleInline(part)}`,
);
}
function renderScore(label: string, value: string): string {
const score = Number(value);
const paddedLabel = color.dim(label.padEnd(11));
if (!Number.isFinite(score)) {
return ` ${paddedLabel} ${value}`;
}
const clamped = Math.max(0, Math.min(100, score));
const filled = Math.round((clamped / 100) * 10);
const bar = symbols.scoreOn.repeat(filled) + symbols.scoreOff.repeat(10 - filled);
const tint = scoreTint(score);
return ` ${paddedLabel} ${tint(bar)} ${tint(String(value))}`;
}
function scoreTint(score: number): (text: string) => string {
if (score >= 80) {
return color.green;
}
if (score >= 50) {
return color.yellow;
}
return color.red;
}
function renderTableRow(line: string): string {
if (TABLE_SEPARATOR.test(line)) {
return color.dim(symbols.rule.repeat(RULE_WIDTH));
}
const cells = line
.trim()
.replace(/^\|/, '')
.replace(/\|$/, '')
.split('|')
.map((cell) => styleInline(cell.trim()));
return ` ${cells.join(color.dim(` ${symbols.codeGutter} `))}`;
}
function renderCodeTop(language: string): string {
const chip = language ? ` ${color.dim(language)}` : '';
return `${color.dim(`${symbols.codeTop}${symbols.rule}`)}${chip}`;
}
function renderCodeLine(line: string): string {
return `${color.dim(`${symbols.codeGutter} `)}${color.gray(line)}`;
}
function renderCodeBottom(): string {
return color.dim(`${symbols.codeBottom}${symbols.rule}`);
}
function styleInline(text: string): string {
return text
.replace(
/\[([^\]]+)\]\(([^)]+)\)/g,
(_match, label: string, url: string) => `${color.underline(color.blue(label))}${color.dim(` (${url})`)}`,
)
.replace(/`([^`]+)`/g, (_match, code: string) => color.cyan(code))
.replace(/\*\*([^*]+)\*\*/g, (_match, bold: string) => color.bold(bold));
}
function wrap(text: string, width: number): string[] {
const limit = Math.max(20, width);
const words = text.split(/\s+/).filter(Boolean);
const lines: string[] = [];
let current = '';
for (const word of words) {
if (current === '') {
current = word;
} else if (current.length + 1 + word.length <= limit) {
current += ` ${word}`;
} else {
lines.push(current);
current = word;
}
}
if (current !== '') {
lines.push(current);
}
return lines.length > 0 ? lines : [''];
}

View file

@ -0,0 +1,189 @@
/* eslint-disable import/no-unresolved -- NodeNext source imports use runtime .js extensions. */
import { CLI_NAME, CLI_VERSION } from '../constants.js';
import type { SetupResult } from '../setup/setup.js';
import { brandShade, color, symbols } from './theme.js';
const LOGO: string[] = [
' ',
' ░░░░░░░░░░░░░░░░░░░░░░░░░░░',
' ░░░░░',
' ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ░░░░',
' ░░░░',
' ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ░░░░',
' ░░░░',
' ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ░░░',
' ░░░',
' ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ░░░',
' ░░░',
' ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ░░░',
' ░░░',
' ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ░░░',
' ░░░░',
' ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ░░░░',
' ░░░',
' ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ░░░░',
' ░░░░░',
' ░░░░░░░░░░░░░░░░░░░░░░░░░░',
' ',
];
const LOGO_TOP_LEVEL = 1;
const LOGO_BOTTOM_LEVEL = 0;
export function renderLogo(): string {
const span = Math.max(1, LOGO.length - 1);
const rows = LOGO.map((line, index) => {
const level = LOGO_TOP_LEVEL - (index / span) * (LOGO_TOP_LEVEL - LOGO_BOTTOM_LEVEL);
return brandShade(level)(line);
});
return `\n${rows.join('\n')}\n`;
}
export function renderBanner(): string {
const title = color.bold(color.cyan('DocSearch'));
const subtitle = color.dim('search public docs & set up the DocSearch MCP');
return `\n${color.magenta(symbols.diamond)} ${title} ${color.dim(`v${CLI_VERSION}`)}\n ${subtitle}\n`;
}
interface LandingCommand {
args?: string;
description: string;
name: string;
}
const LANDING_COMMANDS: LandingCommand[] = [
{ description: 'Configure the DocSearch MCP for your AI agents', name: 'setup' },
{ args: '<library> <query>', description: "One-shot search across a library's docs", name: 'docs' },
{ args: '<library>', description: 'Find matching docsets for a product', name: 'resolve' },
{
args: '<docset-id[,docset-id...]> <query>',
description: 'Search inside specific docsets',
name: 'query',
},
];
export function renderLanding(): string {
const prefix = `${CLI_NAME} `;
const plainLefts = LANDING_COMMANDS.map(
(command) => ` $ ${prefix}${command.name}${command.args ? ` ${command.args}` : ''}`,
);
const column = Math.max(...plainLefts.map((left) => left.length)) + 3;
const commandLines = LANDING_COMMANDS.map((command, index) => {
const styledLeft = ` ${color.dim('$')} ${color.dim(prefix.trim())} ${color.green(command.name)}${
command.args ? ` ${color.cyan(command.args)}` : ''
}`;
const padding = ' '.repeat(Math.max(2, column - plainLefts[index].length));
return `${styledLeft}${padding}${command.description}`;
});
return [
renderLogo(),
color.dim(' The open documentation search for AI agents'),
'',
...commandLines,
'',
` ${color.dim('try:')} ${color.bold(`${CLI_NAME} docs Next.js`)} ${color.dim('"how do middleware matchers work"')}`,
'',
` ${color.dim('Docs & MCP setup at')} ${color.underline(color.blue('https://docsearch.algolia.com/mcp'))}`,
'',
].join('\n');
}
export function renderHelp(): string {
const heading = (text: string): string => color.bold(color.yellow(text));
const cmd = (text: string): string => color.green(text);
const meta = (text: string): string => color.dim(text);
return [
renderBanner(),
heading('Usage'),
` ${color.bold(CLI_NAME)} ${color.cyan('<command>')} ${meta('[options]')}`,
'',
heading('Commands'),
` ${cmd('setup')} Configure the DocSearch MCP for your AI agents`,
` ${cmd('docs')} ${color.cyan('<library> <query>')} One-shot search across a library's docs`,
` ${cmd('resolve')} ${color.cyan('<library>')} Find matching docsets for a product`,
` ${cmd('query')} ${color.cyan('<docset-id[,docset-id...]> <query>')} Search inside specific docsets`,
'',
heading('Setup options'),
` ${color.green('--project')} Install at the detected repository root`,
` ${color.green('--global')} Install into user-level agent settings`,
` ${color.green('--all')} Configure every supported agent`,
` ${color.green('--cursor')}, ${color.green('--claude')} Configure Cursor or Claude Code`,
` ${color.green('--codex')}, ${color.green('--opencode')} Configure Codex or OpenCode`,
` ${color.green('--gemini')} Configure Gemini CLI`,
` ${color.green('--yes')}, ${color.green('-y')} Run setup non-interactively ${meta('(project + detected agents)')}`,
'',
heading('Query options'),
` ${color.green('--max-results')} ${color.cyan('<n>')} Limit documentation results`,
` ${color.green('--max-docsets')} ${color.cyan('<n>')} Limit docsets searched by ${cmd('docs')}`,
` ${color.green('--top-n')} ${color.cyan('<n>')} Limit docsets returned by ${cmd('resolve')}`,
` ${color.green('--json')} Print raw MCP results as JSON`,
'',
heading('Global options'),
` ${color.green('--endpoint')} ${color.cyan('<url>')} Override the hosted DocSearch MCP endpoint`,
` ${color.green('--help')}, ${color.green('-h')} Show help`,
'',
heading('Examples'),
` ${meta('$')} ${CLI_NAME} setup`,
` ${meta('$')} ${CLI_NAME} setup ${color.green('--cursor --claude --project')}`,
` ${meta('$')} ${CLI_NAME} docs Next.js ${color.dim('"how do middleware matchers work"')}`,
` ${meta('$')} ${CLI_NAME} resolve ${color.dim('"Algolia InstantSearch React"')}`,
` ${meta('$')} ${CLI_NAME} query nextjs ${color.dim('"middleware matcher config"')} --json`,
'',
].join('\n');
}
export function renderResultHeader(command: string, subject: string): string {
return `\n${color.magenta(symbols.diamond)} ${color.bold(color.cyan(command))} ${color.dim(symbols.arrow)} ${color.bold(subject)}\n`;
}
export function renderError(message: string): string {
return `${color.red(symbols.cross)} ${color.red(message)}\n`;
}
export function renderNotice(message: string): string {
return `${color.yellow(symbols.info)} ${message}\n`;
}
export interface SetupSummaryMeta {
endpoint: string;
location?: string;
scope: string;
}
export function renderSetupResults(results: SetupResult[], meta: SetupSummaryMeta): string {
if (results.length === 0) {
return renderNotice('No agents selected. Nothing was configured.');
}
const label = results.length === 1 ? 'agent' : 'agents';
const lines: string[] = [
`\n${color.green(symbols.tick)} ${color.bold(`Configured DocSearch MCP for ${results.length} ${label}`)}`,
` ${color.dim('scope')} ${meta.scope}`,
...(meta.location ? [` ${color.dim('root')} ${meta.location}`] : []),
` ${color.dim('endpoint')} ${color.underline(meta.endpoint)}`,
'',
];
for (const result of results) {
lines.push(`${color.magenta(symbols.diamond)} ${color.bold(result.agent)}`);
lines.push(renderStep('MCP server', result.mcpStatus, result.mcpPath));
lines.push(renderStep('Rule', result.ruleStatus, result.rulePath));
lines.push(renderStep('Skill', result.skillStatus, result.skillPath));
lines.push('');
}
lines.push(color.dim(`Restart your agent to load the new configuration.`));
return `${lines.join('\n')}\n`;
}
function renderStep(name: string, status: string, path: string): string {
const badge = status === 'updated' ? color.yellow(status) : color.green(status);
return ` ${color.green(symbols.tick)} ${name} ${color.dim(symbols.bullet)} ${badge}\n ${color.dim(path)}`;
}

View file

@ -0,0 +1,51 @@
/* eslint-disable import/no-unresolved -- NodeNext source imports use runtime .js extensions. */
import { color, symbols } from './theme.js';
const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
const FRAME_INTERVAL_MS = 80;
export interface Spinner {
fail: (text?: string) => void;
stop: () => void;
succeed: (text?: string) => void;
}
export function startSpinner(text: string, stream: NodeJS.WriteStream = process.stderr): Spinner {
const animate = stream.isTTY === true && !('NO_COLOR' in process.env);
if (!animate) {
stream.write(`${color.dim(symbols.bullet)} ${text}\n`);
return { fail: noop, stop: noop, succeed: noop };
}
let frame = 0;
stream.write('\u001B[?25l');
const timer = setInterval(() => {
frame = (frame + 1) % FRAMES.length;
stream.write(`\r${color.cyan(FRAMES[frame])} ${text}`);
}, FRAME_INTERVAL_MS);
function clear(): void {
clearInterval(timer);
stream.write('\r\u001B[K\u001B[?25h');
}
return {
fail(message) {
clear();
stream.write(`${color.red(symbols.cross)} ${message ?? text}\n`);
},
stop() {
clear();
},
succeed(message) {
clear();
stream.write(`${color.green(symbols.tick)} ${message ?? text}\n`);
},
};
}
function noop(): void {
// Intentionally empty: non-animated spinners have nothing to clear.
}

View file

@ -0,0 +1,65 @@
import pc from 'picocolors';
const colorEnabled =
!('NO_COLOR' in process.env) && (process.stdout.isTTY === true || process.env.FORCE_COLOR !== undefined);
function paint(fn: (input: string) => string): (text: string) => string {
return (text: string) => (colorEnabled ? fn(text) : text);
}
export const color = {
blue: paint(pc.blue),
bold: paint(pc.bold),
cyan: paint(pc.cyan),
dim: paint(pc.dim),
gray: paint(pc.gray),
green: paint(pc.green),
magenta: paint(pc.magenta),
red: paint(pc.red),
underline: paint(pc.underline),
yellow: paint(pc.yellow),
};
const richUnicode =
process.platform !== 'win32' || process.env.WT_SESSION !== undefined || process.env.TERM_PROGRAM !== undefined;
export const symbols = {
accent: richUnicode ? '▍' : '|',
arrow: richUnicode ? '→' : '->',
bullet: richUnicode ? '•' : '*',
checkboxOff: richUnicode ? '◯' : '[ ]',
checkboxOn: richUnicode ? '◉' : '[x]',
codeBottom: richUnicode ? '└' : '+',
codeGutter: richUnicode ? '│' : '|',
codeTop: richUnicode ? '┌' : '+',
cross: richUnicode ? '✖' : 'x',
diamond: richUnicode ? '◆' : '*',
info: richUnicode ? '' : 'i',
link: richUnicode ? '↗' : '->',
pointer: richUnicode ? '' : '>',
rule: richUnicode ? '─' : '-',
scoreOff: richUnicode ? '▱' : '-',
scoreOn: richUnicode ? '▰' : '#',
tick: richUnicode ? '✔' : 'v',
};
export function isInteractive(): boolean {
return (
process.stdin.isTTY === true && process.stderr.isTTY === true && typeof process.stdin.setRawMode === 'function'
);
}
const DOCSEARCH_CYAN = [44, 200, 247] as const;
const DOCSEARCH_NEBULA = [84, 104, 255] as const;
const DOCSEARCH_PINK = [248, 44, 170] as const;
export function brandShade(level: number): (text: string) => string {
const clamped = Math.max(0, Math.min(1, level));
const [from, to, progress] =
clamped >= 0.5
? [DOCSEARCH_NEBULA, DOCSEARCH_PINK, (clamped - 0.5) * 2]
: [DOCSEARCH_CYAN, DOCSEARCH_NEBULA, clamped * 2];
const [red, green, blue] = from.map((channel, index) => Math.round(channel + (to[index] - channel) * progress));
return (text: string) => (colorEnabled ? `\u001B[38;2;${red};${green};${blue}m${text}\u001B[39m` : text);
}

View file

@ -0,0 +1,22 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"declaration": false,
"declarationMap": false,
"lib": [
"ES2022"
],
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "dist",
"rootDir": "src",
"target": "ES2022"
},
"include": [
"src/**/*.ts"
],
"exclude": [
"src/**/*.test.ts",
"src/**/__tests__/**"
]
}

View file

@ -0,0 +1,8 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'node',
include: ['src/**/*.test.ts'],
},
});