From 2f6ddc94b27d41027eae5d84caa971ebfd670633 Mon Sep 17 00:00:00 2001 From: Reza Rahman <13340707+rezrah@users.noreply.github.com> Date: Fri, 26 Jun 2026 11:40:06 +0100 Subject: [PATCH 01/12] add mvp mcp server --- .changeset/heading-usage-guidance.md | 5 + .changeset/primer-brand-mcp.md | 22 + .github/workflows/ci.yml | 3 + package-lock.json | 2781 ++++++++++++++--- packages/mcp/.gitignore | 4 + packages/mcp/.prettierignore | 3 + packages/mcp/README.md | 43 + packages/mcp/eslint.config.mjs | 25 + packages/mcp/jest.config.mjs | 29 + packages/mcp/package.json | 65 + packages/mcp/scripts/generate-catalog.mjs | 637 ++++ packages/mcp/scripts/smoke.mjs | 63 + .../mcp/src/brand/detect-framework.test.ts | 48 + packages/mcp/src/brand/detect-framework.ts | 58 + packages/mcp/src/brand/docs-source.ts | 122 + packages/mcp/src/brand/resolve-assets.test.ts | 19 + packages/mcp/src/brand/resolve-assets.ts | 106 + packages/mcp/src/brand/resolve-install.ts | 76 + packages/mcp/src/catalog/load.ts | 21 + packages/mcp/src/catalog/types.ts | 76 + packages/mcp/src/index.ts | 15 + packages/mcp/src/logger.ts | 24 + packages/mcp/src/review/rules.test.ts | 91 + packages/mcp/src/review/rules.ts | 256 ++ packages/mcp/src/review/types.ts | 22 + packages/mcp/src/server.ts | 57 + packages/mcp/src/test-support/catalog.ts | 122 + packages/mcp/src/tools/format.ts | 36 + .../mcp/src/tools/primer-brand-asset/index.ts | 1 + .../primer-brand-asset.test.ts | 21 + .../primer-brand-asset/primer-brand-asset.ts | 55 + .../src/tools/primer-brand-component/index.ts | 1 + .../primer-brand-component.test.ts | 52 + .../primer-brand-component.ts | 94 + .../mcp/src/tools/primer-brand-docs/index.ts | 1 + .../primer-brand-docs/primer-brand-docs.ts | 71 + .../src/tools/primer-brand-examples/index.ts | 1 + .../primer-brand-examples.test.ts | 29 + .../primer-brand-examples.ts | 66 + .../src/tools/primer-brand-review/index.ts | 1 + .../primer-brand-review.test.ts | 24 + .../primer-brand-review.ts | 79 + .../mcp/src/tools/primer-brand-setup/index.ts | 1 + .../primer-brand-setup.test.ts | 43 + .../primer-brand-setup/primer-brand-setup.ts | 139 + .../src/tools/primer-brand-tokens/index.ts | 1 + .../primer-brand-tokens.test.ts | 44 + .../primer-brand-tokens.ts | 115 + packages/mcp/src/tools/register.ts | 51 + packages/mcp/src/tools/types.ts | 39 + packages/mcp/src/util/text.ts | 35 + packages/mcp/tsconfig.eslint.json | 5 + packages/mcp/tsconfig.json | 23 + packages/react/src/Heading/Heading.tsx | 10 + 54 files changed, 5376 insertions(+), 455 deletions(-) create mode 100644 .changeset/heading-usage-guidance.md create mode 100644 .changeset/primer-brand-mcp.md create mode 100644 packages/mcp/.gitignore create mode 100644 packages/mcp/.prettierignore create mode 100644 packages/mcp/README.md create mode 100644 packages/mcp/eslint.config.mjs create mode 100644 packages/mcp/jest.config.mjs create mode 100644 packages/mcp/package.json create mode 100644 packages/mcp/scripts/generate-catalog.mjs create mode 100644 packages/mcp/scripts/smoke.mjs create mode 100644 packages/mcp/src/brand/detect-framework.test.ts create mode 100644 packages/mcp/src/brand/detect-framework.ts create mode 100644 packages/mcp/src/brand/docs-source.ts create mode 100644 packages/mcp/src/brand/resolve-assets.test.ts create mode 100644 packages/mcp/src/brand/resolve-assets.ts create mode 100644 packages/mcp/src/brand/resolve-install.ts create mode 100644 packages/mcp/src/catalog/load.ts create mode 100644 packages/mcp/src/catalog/types.ts create mode 100644 packages/mcp/src/index.ts create mode 100644 packages/mcp/src/logger.ts create mode 100644 packages/mcp/src/review/rules.test.ts create mode 100644 packages/mcp/src/review/rules.ts create mode 100644 packages/mcp/src/review/types.ts create mode 100644 packages/mcp/src/server.ts create mode 100644 packages/mcp/src/test-support/catalog.ts create mode 100644 packages/mcp/src/tools/format.ts create mode 100644 packages/mcp/src/tools/primer-brand-asset/index.ts create mode 100644 packages/mcp/src/tools/primer-brand-asset/primer-brand-asset.test.ts create mode 100644 packages/mcp/src/tools/primer-brand-asset/primer-brand-asset.ts create mode 100644 packages/mcp/src/tools/primer-brand-component/index.ts create mode 100644 packages/mcp/src/tools/primer-brand-component/primer-brand-component.test.ts create mode 100644 packages/mcp/src/tools/primer-brand-component/primer-brand-component.ts create mode 100644 packages/mcp/src/tools/primer-brand-docs/index.ts create mode 100644 packages/mcp/src/tools/primer-brand-docs/primer-brand-docs.ts create mode 100644 packages/mcp/src/tools/primer-brand-examples/index.ts create mode 100644 packages/mcp/src/tools/primer-brand-examples/primer-brand-examples.test.ts create mode 100644 packages/mcp/src/tools/primer-brand-examples/primer-brand-examples.ts create mode 100644 packages/mcp/src/tools/primer-brand-review/index.ts create mode 100644 packages/mcp/src/tools/primer-brand-review/primer-brand-review.test.ts create mode 100644 packages/mcp/src/tools/primer-brand-review/primer-brand-review.ts create mode 100644 packages/mcp/src/tools/primer-brand-setup/index.ts create mode 100644 packages/mcp/src/tools/primer-brand-setup/primer-brand-setup.test.ts create mode 100644 packages/mcp/src/tools/primer-brand-setup/primer-brand-setup.ts create mode 100644 packages/mcp/src/tools/primer-brand-tokens/index.ts create mode 100644 packages/mcp/src/tools/primer-brand-tokens/primer-brand-tokens.test.ts create mode 100644 packages/mcp/src/tools/primer-brand-tokens/primer-brand-tokens.ts create mode 100644 packages/mcp/src/tools/register.ts create mode 100644 packages/mcp/src/tools/types.ts create mode 100644 packages/mcp/src/util/text.ts create mode 100644 packages/mcp/tsconfig.eslint.json create mode 100644 packages/mcp/tsconfig.json diff --git a/.changeset/heading-usage-guidance.md b/.changeset/heading-usage-guidance.md new file mode 100644 index 0000000000..23b87284ea --- /dev/null +++ b/.changeset/heading-usage-guidance.md @@ -0,0 +1,5 @@ +--- +'@primer/react-brand': patch +--- + +Added inert JSDoc annotations to the `Heading` component's `as` and `size` props clarifying that `as` sets only the semantic heading level while `size` controls the visual size. diff --git a/.changeset/primer-brand-mcp.md b/.changeset/primer-brand-mcp.md new file mode 100644 index 0000000000..d93045026e --- /dev/null +++ b/.changeset/primer-brand-mcp.md @@ -0,0 +1,22 @@ +--- +'@primer/brand-mcp': minor +--- + +Added `@primer/brand-mcp`, a Model Context Protocol (MCP) server that helps AI agents use Primer Brand correctly when building GitHub marketing and landing pages. + +It exposes version-aware tools like `primer_brand_component`, `primer_brand_examples`, `primer_brand_tokens`, `primer_brand_asset`, `primer_brand_docs`, and `primer_brand_review`, which read the `@primer/react-brand` package installed in your project and validates generated JSX/CSS against known design system conventions during development (e.g. unknown components, invalid props, hardcoded values, off-brand patterns). + +Add it to your MCP client (e.g. Copilot CLI, in `~/.copilot/mcp-config.json`): + +```json +{ + "mcpServers": { + "primer-brand": { + "type": "local", + "command": "npx", + "args": ["@primer/brand-mcp@latest"], + "tools": ["*"] + } + } +} +``` diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 51ae10bbe2..4c6ee2b036 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,3 +49,6 @@ jobs: - name: Run unit tests run: npm run test + + - name: Run MCP server smoke tests + run: npm run smoke --workspace=packages/mcp diff --git a/package-lock.json b/package-lock.json index 92469f0917..dda90f1664 100644 --- a/package-lock.json +++ b/package-lock.json @@ -213,45 +213,6 @@ "node": ">=4" } }, - "apps/storybook/node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, "apps/storybook/node_modules/storybook": { "version": "10.3.4", "resolved": "https://registry.npmjs.org/storybook/-/storybook-10.3.4.tgz", @@ -2744,6 +2705,30 @@ "integrity": "sha512-lB59uJoaGIfOOL9knQqQRfhl9g7x8/wqFkp13zTdkRu1huG9kg6IJs1O8hqj9rs6h7orGxHJUKb+mX3rPbWGhA==", "license": "Apache-2.0" }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@csstools/color-helpers": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", @@ -6150,295 +6135,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@esbuild/netbsd-arm64": { "version": "0.27.7", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", @@ -6455,23 +6151,6 @@ "node": ">=18" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@esbuild/openbsd-arm64": { "version": "0.27.7", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", @@ -6488,23 +6167,6 @@ "node": ">=18" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@esbuild/openharmony-arm64": { "version": "0.27.7", "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", @@ -6521,74 +6183,6 @@ "node": ">=18" } }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", @@ -6956,6 +6550,18 @@ "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, + "node_modules/@hono/node-server": { + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, "node_modules/@humanfs/core": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", @@ -8667,6 +8273,292 @@ "langium": "^4.0.0" } }, + "node_modules/@modelcontextprotocol/inspector": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/inspector/-/inspector-0.16.8.tgz", + "integrity": "sha512-7kk6uOGY9ySgCFsRuRplWzvjiEwulG876pfnjQxqaBJAcUlp3N1yrOt7YQMBZsxvop+RGw50IehiPuGs+7oh+w==", + "dev": true, + "license": "MIT", + "workspaces": [ + "client", + "server", + "cli" + ], + "dependencies": { + "@modelcontextprotocol/inspector-cli": "^0.16.8", + "@modelcontextprotocol/inspector-client": "^0.16.8", + "@modelcontextprotocol/inspector-server": "^0.16.8", + "@modelcontextprotocol/sdk": "^1.18.0", + "concurrently": "^9.2.0", + "node-fetch": "^3.3.2", + "open": "^10.2.0", + "shell-quote": "^1.8.3", + "spawn-rx": "^5.1.2", + "ts-node": "^10.9.2", + "zod": "^3.25.76" + }, + "bin": { + "mcp-inspector": "cli/build/cli.js" + }, + "engines": { + "node": ">=22.7.5" + } + }, + "node_modules/@modelcontextprotocol/inspector-cli": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/inspector-cli/-/inspector-cli-0.16.8.tgz", + "integrity": "sha512-u8x8Dbb8Dos34M7N8p4e4AF++Bi1D+lq+dkRCvLi5Qub/dI75Z7YTIXBezA4LbIISly+Ecn05fdofzZwqyOvpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.18.0", + "commander": "^13.1.0", + "spawn-rx": "^5.1.2" + }, + "bin": { + "mcp-inspector-cli": "build/cli.js" + } + }, + "node_modules/@modelcontextprotocol/inspector-cli/node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@modelcontextprotocol/inspector-client": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/inspector-client/-/inspector-client-0.16.8.tgz", + "integrity": "sha512-4sTk/jUnQ1lDv9kbx1nN45SsoApDxW8hjKLKcHnHh9nfRVEN9SW+ylUjNvVCDP74xSNpD8v5p6NJyVWtZYfPWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.18.0", + "@radix-ui/react-checkbox": "^1.1.4", + "@radix-ui/react-dialog": "^1.1.3", + "@radix-ui/react-icons": "^1.3.0", + "@radix-ui/react-label": "^2.1.0", + "@radix-ui/react-popover": "^1.1.3", + "@radix-ui/react-select": "^2.1.2", + "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-switch": "^1.2.6", + "@radix-ui/react-tabs": "^1.1.1", + "@radix-ui/react-toast": "^1.2.6", + "@radix-ui/react-tooltip": "^1.1.8", + "ajv": "^6.12.6", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.1", + "cmdk": "^1.0.4", + "lucide-react": "^0.523.0", + "pkce-challenge": "^4.1.0", + "prismjs": "^1.30.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-simple-code-editor": "^0.14.1", + "serve-handler": "^6.1.6", + "tailwind-merge": "^2.5.3", + "zod": "^3.25.76" + }, + "bin": { + "mcp-inspector-client": "bin/start.js" + } + }, + "node_modules/@modelcontextprotocol/inspector-client/node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@modelcontextprotocol/inspector-client/node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/@modelcontextprotocol/inspector-client/node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/@modelcontextprotocol/inspector-client/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@modelcontextprotocol/inspector-server": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/inspector-server/-/inspector-server-0.16.8.tgz", + "integrity": "sha512-plv0SiPgQAT0/LjC0MmGsoo/sdpS6V4TpOUAxO4J3DnvnLLaInnNh9hiU1SlGgCjsRv0nN9TvX9pWRqVnZH9kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.18.0", + "cors": "^2.8.5", + "express": "^5.1.0", + "shell-quote": "^1.8.3", + "spawn-rx": "^5.1.2", + "ws": "^8.18.0", + "zod": "^3.25.76" + }, + "bin": { + "mcp-inspector-server": "build/index.js" + } + }, + "node_modules/@modelcontextprotocol/inspector-server/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@modelcontextprotocol/inspector/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/@modelcontextprotocol/inspector/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/@napi-rs/simple-git": { "version": "0.1.22", "resolved": "https://registry.npmjs.org/@napi-rs/simple-git/-/simple-git-0.1.22.tgz", @@ -10205,6 +10097,10 @@ "resolved": "packages/fonts", "link": true }, + "node_modules/@primer/brand-mcp": { + "resolved": "packages/mcp", + "link": true + }, "node_modules/@primer/brand-primitives": { "resolved": "packages/design-tokens", "link": true @@ -10377,6 +10273,856 @@ "resolved": "packages/react", "link": true }, + "node_modules/@radix-ui/number": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.2.tgz", + "integrity": "sha512-ceTwaxc4I5IOi97DgCotl3pqiyRGvffcc0oOsE2dQYaJOFIDsDt4VWG6xEbg1QePv9QWausCEIppud/tJ1wNig==", + "dev": true, + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.4.tgz", + "integrity": "sha512-7AdCK9PQyiljKoBDbN8OuctCbd/esdwZPQ8RtOE3SsyQtUpiPb+ND75q0jEhC1m1ecBI0MFNeLJvwIh9iKHRcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.10.tgz", + "integrity": "sha512-j2VTDz1vgCsmuG0k5lBfOcM8n5JPFqZBcMryasFjHYMhwxYL5SRUV5lMSUpRdNtw3D/Sv8pzJtrlAgkssYSsQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.5.tgz", + "integrity": "sha512-pREzrmNnVwGvYaBoM64huTRK7B3lrTRuwj8A9nwhPiEtMb+yudiWh6zWAqEtP0Dzd5+iBa1Ki7V1pCxV8ExMdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-controllable-state": "1.2.3", + "@radix-ui/react-use-previous": "1.1.2", + "@radix-ui/react-use-size": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.10.tgz", + "integrity": "sha512-IVVz4EvBcKjrzKgof714qDnz/SzQAkLA2Emh5edlHbgcE6fNd3Un6CJLlaYcnm8N4JmAtzQgse4dOKxcD2yc9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-slot": "1.3.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.3.tgz", + "integrity": "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.4.tgz", + "integrity": "sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.17.tgz", + "integrity": "sha512-TDTYmpdq8dI2+Xgvgj9AJ8Ghqq+Eph/TRVEdaFQPDItIY+6QSkU7MJMeevw1568Yw/2Ijz8BTphPSP2XejKphw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.13", + "@radix-ui/react-focus-guards": "1.1.4", + "@radix-ui/react-focus-scope": "1.1.10", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-portal": "1.1.12", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-slot": "1.3.0", + "@radix-ui/react-use-controllable-state": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.2.tgz", + "integrity": "sha512-C3vFhbyi4SW3PmbAi6Awpu4OzJtd0MxGurvSsYtr7p7nM8RNB3VAF3CUmnp2j50knpkrRcB7+ycVXzgLgF6yNA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.13.tgz", + "integrity": "sha512-2v+zNAWWe0ySxgC0D0yeXMPQ23xZVgXZTerTz+JKlmdRj6gfTqmCcR29jb6d290DezXPGgruHWDX/vYUebtErg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-callback-ref": "1.1.2", + "@radix-ui/react-use-escape-keydown": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.4.tgz", + "integrity": "sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.10.tgz", + "integrity": "sha512-Fas/lXQqhVvqwAb64s5RFeHiHYElZ6SUQbZaNd6EkfhP/Al7wTIQ9WIR4QVX475tlu5yFCEdDcJH6/UwsZjMWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-callback-ref": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-icons": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.2.tgz", + "integrity": "sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "react": "^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.2.tgz", + "integrity": "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.10.tgz", + "integrity": "sha512-ib0zvq2ZsAqKm5tRnqGJn3vOxSgIts5ToxsXT0q1S/GfLD1Zj7UOEnkw8u2w6sRmn47djpQWuSU1DCL1R29/yw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.17.tgz", + "integrity": "sha512-/YSAOdJ7YJvdn7bn5sdSx2egW+SKY+u7O5RyAVs94Ymrg2fg5QTSFPMRkzvhGyFuE4/qsmPBdrwYoZMZh/4f+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.13", + "@radix-ui/react-focus-guards": "1.1.4", + "@radix-ui/react-focus-scope": "1.1.10", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-popper": "1.3.1", + "@radix-ui/react-portal": "1.1.12", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-slot": "1.3.0", + "@radix-ui/react-use-controllable-state": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.3.1.tgz", + "integrity": "sha512-bhnq/0DEPTi2lsOD3J5rTL65qUKHbKbhqHsmN9TMiclSXpipi651ooUKPPp6G5lF/WiHBdn1s0Wuqsn+myVAvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.10", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-callback-ref": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.2", + "@radix-ui/react-use-rect": "1.1.2", + "@radix-ui/react-use-size": "1.1.2", + "@radix-ui/rect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.12.tgz", + "integrity": "sha512-m309havGzsjLHHaIX50G5PlvRs3xkgPCsGk/5PTvYm8D5q33yG0J7w/712PTOhid7NTaFETtnSXjngHQavvhVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.6.tgz", + "integrity": "sha512-zdTk4PlUO0E18HnZ3wYbW0KkJJxWCdiNYp6g6X1PtONFhxVkg01vliTJAmwIszU6mHiyBOoW9P0rAugl5/hULQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.6.tgz", + "integrity": "sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.3.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.13.tgz", + "integrity": "sha512-9gkwneI0guf8JDmrFxPjJF6Ozzgioyw+/lonYNCwefS9ZHA05er0BVHiXr+LbWGHxUfczvMY6G1oiZZi1VzjRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-collection": "1.1.10", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-callback-ref": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.3.1.tgz", + "integrity": "sha512-w6eDvY78LE9ZUiNnXCA1QVK8RYN7k9galFv09kjVydJqBAgHd7Y9A6h0UJ/6DCZNGZMZrB2ohcSW1Bo9d8+wWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.2", + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-collection": "1.1.10", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.13", + "@radix-ui/react-focus-guards": "1.1.4", + "@radix-ui/react-focus-scope": "1.1.10", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-popper": "1.3.1", + "@radix-ui/react-portal": "1.1.12", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-slot": "1.3.0", + "@radix-ui/react-use-callback-ref": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.2.3", + "@radix-ui/react-use-layout-effect": "1.1.2", + "@radix-ui/react-use-previous": "1.1.2", + "@radix-ui/react-visually-hidden": "1.2.6", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.0.tgz", + "integrity": "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.3.1.tgz", + "integrity": "sha512-55bQtCnOB0BohomSHi6qvQXpJEEqUGDm6hRrM0Bph5OXwhSegqkd8IqgBAQkM1IlgUlWZIxpxRcpOEfRIgimyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-controllable-state": "1.2.3", + "@radix-ui/react-use-previous": "1.1.2", + "@radix-ui/react-use-size": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.15.tgz", + "integrity": "sha512-kxc9gI6/HfcU4nfMMVS3AmQK414kbU1IE6UCJmMmxjhO3cRPXOyYnmvyKD+ODt7q56nRq9l7Wovi6uaGwKgMlg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-roving-focus": "1.1.13", + "@radix-ui/react-use-controllable-state": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast": { + "version": "1.2.17", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.17.tgz", + "integrity": "sha512-uL4kyyWy000pPL43fGGCV5qT6ZchCWEQZOSlkYiPwPt8Hy1iW38RjeptIvz1/SZesrW6Vn58Ct3sV7tfEfiAbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-collection": "1.1.10", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.13", + "@radix-ui/react-portal": "1.1.12", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-callback-ref": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.2.3", + "@radix-ui/react-use-layout-effect": "1.1.2", + "@radix-ui/react-visually-hidden": "1.2.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.10.tgz", + "integrity": "sha512-NlNe8D0dWEpVfXFli90IO6X07Josx/b1iu98tDnx9Xv0HT4wLIL+m2VOheMHhK7qbp2HoTBqALEFzGyZs/levw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.13", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-popper": "1.3.1", + "@radix-ui/react-portal": "1.1.12", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-slot": "1.3.0", + "@radix-ui/react-use-controllable-state": "1.2.3", + "@radix-ui/react-visually-hidden": "1.2.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.2.tgz", + "integrity": "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.3.tgz", + "integrity": "sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.3", + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.3.tgz", + "integrity": "sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.2.tgz", + "integrity": "sha512-2uVLvLjgO7NZCWw01/FdqRwmA42J0BcjPMUCA+koFEOAb+zjqIP7SiFz/7zWPrKnVmSqr76Omq2ALyCuX4dhLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.2.tgz", + "integrity": "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.2.tgz", + "integrity": "sha512-IGBQPtRFdhN6MQ8dbegVmBq1LVZluya3F1jWY+puIcQC3MHctRwTDSBWCkL/3ZcnMJLTMJ++Z+ktmvg0F89iCw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.2.tgz", + "integrity": "sha512-d8a+bBY/FxikNPlgJJoaBHZX+zKVbWHYJGTLnLvveQgFSTntkGdEKv3JDtHrMS0DNYpllz2nRsTLGLKYttbpmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.2.tgz", + "integrity": "sha512-giWQp+4mxjBPt4KZ0MmyuykFNWfbDxKt4x+fPkRYmgRFJSbCZFzUglvMb/Kjn38tm10YP4ufiQZDx3zna4LU6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.6.tgz", + "integrity": "sha512-jCE0WljWifTI4niIMCll06kGpsJTAPiZVU9H4WR1N6qW7At9ystHbN7dDB+we2xH535roFHj7qKS+RGj0FMDWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.2.tgz", + "integrity": "sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA==", + "dev": true, + "license": "MIT" + }, "node_modules/@react-aria/focus": { "version": "3.22.0", "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.22.0.tgz", @@ -12244,6 +12990,34 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -13882,6 +14656,44 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", @@ -13915,6 +14727,19 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -14759,6 +15584,43 @@ "readable-stream": "^3.4.0" } }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/boxen": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.1.tgz", @@ -14907,6 +15769,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/bytes-iec": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/bytes-iec/-/bytes-iec-3.1.1.tgz", @@ -15002,7 +15873,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -15016,7 +15886,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -15323,6 +16192,19 @@ "dev": true, "license": "MIT" }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, "node_modules/clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -15566,6 +16448,23 @@ "node": ">=6" } }, + "node_modules/cmdk": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", + "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-id": "^1.1.0", + "@radix-ui/react-primitive": "^2.0.2" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -15696,6 +16595,47 @@ "typedarray": "^0.0.6" } }, + "node_modules/concurrently": { + "version": "9.2.3", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.3.tgz", + "integrity": "sha512-ihjs0E2SxvDgq/MK418hX6YycQgKhsqxpbZuZbHo0yKfqDWdymWMjWYIpCIzqDDLLKClHlXev8whW/8WXmJ0BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.4", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/confbox": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", @@ -15714,12 +16654,52 @@ "upper-case": "^2.0.2" } }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "license": "MIT" }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, "node_modules/copy-anything": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", @@ -15750,6 +16730,23 @@ "url": "https://opencollective.com/core-js" } }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cose-base": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", @@ -15786,6 +16783,13 @@ } } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cross-fetch": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz", @@ -16542,6 +17546,16 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/data-urls": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", @@ -16834,6 +17848,15 @@ "node": ">=0.4.0" } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -16891,6 +17914,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -17039,7 +18072,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -17057,6 +18089,12 @@ "dev": true, "license": "MIT" }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, "node_modules/electron-to-chromium": { "version": "1.5.344", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz", @@ -17092,6 +18130,15 @@ "node": ">=14" } }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/enhanced-resolve": { "version": "5.21.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.0.tgz", @@ -17265,7 +18312,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -17318,7 +18364,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -17831,6 +18876,12 @@ "node": ">=6" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -19032,6 +20083,15 @@ "node": ">=0.10.0" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -19041,6 +20101,27 @@ "node": ">=0.8.x" } }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", + "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -19130,6 +20211,92 @@ "dev": true, "license": "MIT" }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/express/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -19283,6 +20450,30 @@ } } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -19308,6 +20499,27 @@ "node": ">=8" } }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/find-cache-dir": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", @@ -19549,6 +20761,28 @@ "node": ">=0.4.x" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/fraction.js": { "version": "5.3.4", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", @@ -19590,6 +20824,15 @@ } } }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/fromentries": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", @@ -19730,7 +20973,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -19774,7 +21016,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -20035,7 +21276,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -20151,7 +21391,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -20551,6 +21790,15 @@ "node": ">=0.10.0" } }, + "node_modules/hono": { + "version": "4.12.27", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.27.tgz", + "integrity": "sha512-1yrb/+w6HWQJrUCLkJ2IF5jNIPvvFkblV5RNOYl6bV+OA6p9GLcMpHFFGTosSvHvcAUibuUukRqhlYI4z32C7Q==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, "node_modules/hosted-git-info": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", @@ -20632,6 +21880,26 @@ "readable-stream": "^3.1.1" } }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -20733,7 +22001,6 @@ "version": "0.7.2", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "dev": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -20923,7 +22190,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "devOptional": true, "license": "ISC" }, "node_modules/ini": { @@ -20973,6 +22239,24 @@ "node": ">=10.13.0" } }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-alphabetical": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", @@ -21392,6 +22676,12 @@ "dev": true, "license": "MIT" }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -23443,6 +24733,15 @@ "@sideway/pinpoint": "^2.0.0" } }, + "node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-base64": { "version": "3.7.8", "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.8.tgz", @@ -23543,6 +24842,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -24303,6 +25608,16 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "0.523.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.523.0.tgz", + "integrity": "sha512-rUjQoy7egZT9XYVXBK1je9ckBnNp7qzRZOhLQx5RcEp2dCGlXo+mv6vf7Am4LimEcFBJIIZzSGfgTqc9QCrPSw==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/lz-string": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", @@ -24402,7 +25717,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -24776,6 +26090,27 @@ "dev": true, "license": "CC0-1.0" }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -26118,6 +27453,27 @@ "license": "MIT", "optional": true }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, "node_modules/node-exports-info": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", @@ -26646,7 +28002,6 @@ "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -26755,11 +28110,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "devOptional": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -27147,6 +28513,15 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/pascal-case": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", @@ -27201,6 +28576,13 @@ "node": ">=0.10.0" } }, + "node_modules/path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -27241,6 +28623,16 @@ "node": "20 || >=22" } }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -27303,6 +28695,16 @@ "node": ">= 6" } }, + "node_modules/pkce-challenge": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-4.1.0.tgz", + "integrity": "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -28909,6 +30311,16 @@ "react": ">=16.0.0" } }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/proc-log": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", @@ -28994,6 +30406,19 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/proxy-from-env": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", @@ -29051,6 +30476,22 @@ ], "license": "MIT" }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/quansync": { "version": "0.2.11", "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", @@ -29095,6 +30536,30 @@ ], "license": "MIT" }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/react": { "version": "19.2.4", "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", @@ -29380,6 +30845,17 @@ } } }, + "node_modules/react-simple-code-editor": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/react-simple-code-editor/-/react-simple-code-editor-0.14.1.tgz", + "integrity": "sha512-BR5DtNRy+AswWJECyA17qhUDvrrCZ6zXOCfkQY5zSmb96BVUbpVAv03WpcjcwtCwiLbIANx3gebHOcXYn1EHow==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/react-stately": { "version": "3.46.0", "resolved": "https://registry.npmjs.org/react-stately/-/react-stately-3.46.0.tgz", @@ -30329,6 +31805,22 @@ "points-on-path": "^0.2.1" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/rrweb-cssom": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", @@ -30614,6 +32106,57 @@ "node": ">=10" } }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/send/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/send/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/sentence-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/sentence-case/-/sentence-case-3.0.4.tgz", @@ -30626,6 +32169,101 @@ "upper-case-first": "^2.0.2" } }, + "node_modules/serve-handler": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.7.tgz", + "integrity": "sha512-CinAq1xWb0vR3twAv9evEU8cNWkXCb9kd5ePAHUKJBkOsUpR1wt/CvGdeca7vqumL1U5cSaeVQ6zZMxiJ3yWsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.0.0", + "content-disposition": "0.5.2", + "mime-types": "2.1.18", + "minimatch": "3.1.5", + "path-is-inside": "1.0.2", + "path-to-regexp": "3.3.0", + "range-parser": "1.2.0" + } + }, + "node_modules/serve-handler/node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-handler/node_modules/content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-handler/node_modules/mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-handler/node_modules/mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "~1.33.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-handler/node_modules/path-to-regexp": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", + "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/serve-handler/node_modules/range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/server-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz", @@ -30688,6 +32326,12 @@ "node": ">= 0.4" } }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, "node_modules/shallow-clone": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", @@ -30767,6 +32411,19 @@ "node": ">=8" } }, + "node_modules/shell-quote": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.4.tgz", + "integrity": "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/shiki": { "version": "3.23.0", "resolved": "https://registry.npmjs.org/shiki/-/shiki-3.23.0.tgz", @@ -30784,15 +32441,14 @@ } }, "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" }, @@ -30807,7 +32463,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -30824,7 +32479,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -30843,7 +32497,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -30966,6 +32619,17 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/spawn-rx": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/spawn-rx/-/spawn-rx-5.1.2.tgz", + "integrity": "sha512-/y7tJKALVZ1lPzeZZB9jYnmtrL7d0N2zkorii5a7r7dhHkWIuLTzZpZzMJLK1dmYRgX/NCc4iarTO3F7BS2c/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.7", + "rxjs": "^7.8.1" + } + }, "node_modules/spawn-wrap": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", @@ -31168,6 +32832,15 @@ "node": ">=8" } }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -32033,6 +33706,17 @@ "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", "license": "MIT" }, + "node_modules/tailwind-merge": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.1.tgz", + "integrity": "sha512-Oo6tHdpZsGpkKG88HJ8RR1rg/RdnEkQEfMoEk2x1XRI3F1AxeU+ijRXpiVUF4UbLfcxxRGw6TbUINKYdWVsQTQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tapable": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", @@ -32341,6 +34025,15 @@ "node": ">=8.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/tough-cookie": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", @@ -32545,6 +34238,57 @@ "code-block-writer": "^13.0.3" } }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, "node_modules/tsconfig-paths": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", @@ -32649,6 +34393,62 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -33654,6 +35454,15 @@ "node": ">= 4.0.0" } }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/unplugin": { "version": "2.3.11", "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz", @@ -33853,6 +35662,13 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", @@ -33889,6 +35705,15 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", @@ -34386,6 +36211,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -34801,7 +36636,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "devOptional": true, "license": "ISC" }, "node_modules/write-file-atomic": { @@ -34948,6 +36782,16 @@ "node": ">=12" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -34965,12 +36809,20 @@ "version": "3.25.58", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.58.tgz", "integrity": "sha512-DVLmMQzSZwNYzQoMaM3MQWnxr2eq+AtM9Hx3w1/Yl0pH8sLTSjN4jGP7w6f7uand6Hw44tsnSu1hz1AOA6qI2Q==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + }, "node_modules/zod-validation-error": { "version": "3.5.4", "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.5.4.tgz", @@ -35049,6 +36901,25 @@ "npm": ">=8.0.0" } }, + "packages/mcp": { + "name": "@primer/brand-mcp", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.24.0", + "zod": "3.25.58" + }, + "bin": { + "brand-mcp": "dist/index.js" + }, + "devDependencies": { + "@modelcontextprotocol/inspector": "^0.16.6", + "@types/node": "24.12.0" + }, + "engines": { + "node": ">=24.0.0 <25" + } + }, "packages/react": { "name": "@primer/react-brand", "version": "0.69.0", diff --git a/packages/mcp/.gitignore b/packages/mcp/.gitignore new file mode 100644 index 0000000000..9a244f31d9 --- /dev/null +++ b/packages/mcp/.gitignore @@ -0,0 +1,4 @@ +dist/ +*.tsbuildinfo +.test/ +coverage/ diff --git a/packages/mcp/.prettierignore b/packages/mcp/.prettierignore new file mode 100644 index 0000000000..b5d9e671dc --- /dev/null +++ b/packages/mcp/.prettierignore @@ -0,0 +1,3 @@ +dist/ +coverage/ +node_modules/ diff --git a/packages/mcp/README.md b/packages/mcp/README.md new file mode 100644 index 0000000000..bf895f3569 --- /dev/null +++ b/packages/mcp/README.md @@ -0,0 +1,43 @@ +# @primer/brand-mcp + +A [Model Context Protocol](https://modelcontextprotocol.io) server that helps AI agents use **Primer Brand** (`@primer/react-brand`) correctly when building GitHub marketing and landing pages. + +It is a version-aware, `stdio` (local) server which reads the docs and metadata of your installed `@primer/react-brand` dependency. + +## Tools + +| Tool | What it does | +| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------- | +| `primer_brand_setup` | Framework-aware setup (Auto-installs `ThemeProvider`, Mona Sans fonts, styles, `'use client'` for RSC) - the parts agents forget. | +| `primer_brand_component` | List components, or get one component's API: props, allowed values, sub-components, examples, accessibility. | +| `primer_brand_examples` | Ranked, copy-and-adapt examples for a goal, taken from the library's own tested stories. Falls back to a default set. | +| `primer_brand_tokens` | Resolve design tokens by intent (color, space, typography) to CSS variables and values. | +| `primer_brand_asset` | Find Octicons (`@primer/octicons-react`) and Octovisuals (`@primer/octovisuals-react`) as code imports. | +| `primer_brand_docs` | Search and read Primer Brand guidance (principles, accessibility, getting started). | +| `primer_brand_review` | Check generated JSX/CSS against the design system: unknown components, invalid props, hardcoded values, off-brand tells. | + +## Getting started + +Requires Node.js >= 24. + +### VS Code + +```jsonc +{ + "servers": { + "Primer Brand": { + "type": "stdio", + "command": "npx", + "args": ["@primer/brand-mcp@latest"] + } + } +} +``` + +### Copilot CLI / other stdio clients + +Run the server with `npx @primer/brand-mcp@latest`. + +## License + +MIT diff --git a/packages/mcp/eslint.config.mjs b/packages/mcp/eslint.config.mjs new file mode 100644 index 0000000000..42ba018d8d --- /dev/null +++ b/packages/mcp/eslint.config.mjs @@ -0,0 +1,25 @@ +import rootConfig from '../../eslint.config.mjs' + +export default [ + ...rootConfig, + { + files: ['**/*.ts'], + languageOptions: { + parserOptions: { + tsconfigRootDir: import.meta.dirname, + project: ['./tsconfig.eslint.json'], + }, + }, + rules: { + // MCP tool descriptions and tool output are machine-facing protocol text, + // not localized end-user UI copy, so the i18n / HTML-escaping rules do not apply. + 'i18n-text/no-en': 'off', + 'github/unescaped-html-literal': 'off', + // This is a Node server: builtin modules and the SDK's subpath exports are expected. + 'import/no-namespace': 'off', + 'import/no-nodejs-modules': 'off', + // Subpath resolution of the SDK's exports map is validated by tsc, not the eslint resolver. + 'import/no-unresolved': 'off', + }, + }, +] diff --git a/packages/mcp/jest.config.mjs b/packages/mcp/jest.config.mjs new file mode 100644 index 0000000000..565f308ebc --- /dev/null +++ b/packages/mcp/jest.config.mjs @@ -0,0 +1,29 @@ +/** + * Jest config for the ESM TypeScript MCP server. + */ +export default { + preset: 'ts-jest/presets/default-esm', + testEnvironment: 'node', + extensionsToTreatAsEsm: ['.ts'], + // Allows NodeNext-style `./example.js` specifiers to resolve to the `./example.ts` source. + moduleNameMapper: { + '^(\\.{1,2}/.*)\\.js$': '$1', + }, + transform: { + '^.+\\.ts$': [ + 'ts-jest', + { + useESM: true, + tsconfig: { + module: 'NodeNext', + moduleResolution: 'NodeNext', + verbatimModuleSyntax: false, + types: ['node', 'jest'], + }, + }, + ], + }, + // eslint-disable-next-line github/unescaped-html-literal + testMatch: ['/src/**/*.test.ts'], + clearMocks: true, +} diff --git a/packages/mcp/package.json b/packages/mcp/package.json new file mode 100644 index 0000000000..935bfe4b02 --- /dev/null +++ b/packages/mcp/package.json @@ -0,0 +1,65 @@ +{ + "name": "@primer/brand-mcp", + "version": "0.69.0", + "description": "MCP (Model Context Protocol) server that helps AI agents use Primer Brand (@primer/react-brand) correctly when building GitHub marketing and landing pages.", + "keywords": [ + "primer", + "brand", + "mcp", + "model-context-protocol", + "design-system", + "ai" + ], + "homepage": "https://primer.style/brand", + "bugs": { + "url": "https://github.com/primer/brand/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/primer/brand.git", + "directory": "packages/mcp" + }, + "license": "MIT", + "author": "GitHub, Inc.", + "type": "module", + "bin": { + "primer-brand-mcp": "./dist/index.js" + }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./package.json": "./package.json" + }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "clean": "rm -rf dist", + "generate:catalog": "node scripts/generate-catalog.mjs", + "build": "npm run clean && npm run generate:catalog && tsc -p tsconfig.json", + "check": "tsc --noEmit -p tsconfig.json", + "format": "prettier --check '**/*.{js,mjs,ts,json,md}'", + "format:fix": "prettier --write '**/*.{js,mjs,ts,json,md}'", + "lint": "eslint '**/*.ts' --max-warnings=0", + "lint:fix": "npm run lint -- --fix", + "test": "NODE_OPTIONS=--experimental-vm-modules jest", + "inspect": "npm run build && npx @modelcontextprotocol/inspector node dist/index.js", + "smoke": "npm run build && node scripts/smoke.mjs", + "start": "node dist/index.js", + "prepack": "npm run build" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.24.0", + "zod": "3.25.58" + }, + "devDependencies": { + "@modelcontextprotocol/inspector": "^0.16.6", + "@types/node": "24.12.0" + }, + "engines": { + "node": ">=24.0.0 <25" + } +} diff --git a/packages/mcp/scripts/generate-catalog.mjs b/packages/mcp/scripts/generate-catalog.mjs new file mode 100644 index 0000000000..396eadb9d5 --- /dev/null +++ b/packages/mcp/scripts/generate-catalog.mjs @@ -0,0 +1,637 @@ +#!/usr/bin/env node +/** + * Builds `dist/catalog.json` from other workspace packages react source, Storybook stories, next-docs + * pages, installed icon packages, and built design tokens. + */ +import {existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync} from 'node:fs' +import {dirname, join, resolve} from 'node:path' +import {fileURLToPath} from 'node:url' + +const scriptDir = dirname(fileURLToPath(import.meta.url)) +const packageRoot = resolve(scriptDir, '..') +const repoRoot = resolve(packageRoot, '..', '..') +const reactSrc = resolve(repoRoot, 'packages', 'react', 'src') +const docsContentRoot = resolve(repoRoot, 'apps', 'next-docs', 'content') +const nodeModules = resolve(repoRoot, 'node_modules') +const outFile = resolve(packageRoot, 'dist', 'catalog.json') + +const writeStderrLog = message => process.stderr.write(`[generate-catalog] ${message}\n`) + +const readFileOrNull = path => { + try { + return readFileSync(path, 'utf8') + } catch { + return null + } +} + +/** Capture a balanced `{...}` or `(...)` region starting at the opening delimiter index. */ +function captureBalanced(source, openIndex, open, close) { + let depth = 0 + for (let index = openIndex; index < source.length; index += 1) { + const char = source[index] + if (char === open) depth += 1 + else if (char === close) { + depth -= 1 + if (depth === 0) return source.slice(openIndex, index + 1) + } + } + return null +} + +// --------------------------------------------------------------------------- +// Components +// --------------------------------------------------------------------------- + +// Public components via the barrel and the sub-barrels it re-exports; mirrors the real surface +// (incl. nested `river/River`, `forms/Checkbox`) and skips internal `recipes/`. +function discoverComponentFiles() { + const found = [] + const seen = new Set() + + const findComponentFile = (relativeDirectory, name) => { + const nestedPath = join(reactSrc, relativeDirectory, name, `${name}.tsx`) + if (existsSync(nestedPath)) return {name, dir: join(relativeDirectory, name), file: nestedPath} + const flatPath = join(reactSrc, relativeDirectory, `${name}.tsx`) + if (existsSync(flatPath)) return {name, dir: relativeDirectory, file: flatPath} + return null + } + + const scanBarrel = relativeDirectory => { + const barrel = readFileOrNull(join(reactSrc, relativeDirectory, 'index.ts')) + if (!barrel) return + for (const match of barrel.matchAll(/export \* from ['"]\.\/([\w/-]+)['"]/g)) { + const exportPath = match[1] + const baseName = exportPath.split('/').pop() + if (/^[A-Z]/.test(baseName)) { + const component = findComponentFile(relativeDirectory, exportPath) + if (component && !seen.has(component.name)) { + seen.add(component.name) + found.push(component) + } + } else if (existsSync(join(reactSrc, relativeDirectory, exportPath, 'index.ts'))) { + scanBarrel(join(relativeDirectory, exportPath)) + } + } + } + + scanBarrel('') + if (found.length === 0) writeStderrLog(`no components discovered under ${reactSrc}`) + return found +} + +/** Sub-components from `Object.assign(Root, {Heading: ...})` and `Name.Sub = ...` assignments. */ +function extractSubcomponents(name, source) { + const subcomponents = new Set() + for (const match of source.matchAll(/Object\.assign\(/g)) { + const openBraceIndex = source.indexOf('{', match.index) + if (openBraceIndex === -1) continue + const assignedObject = captureBalanced(source, openBraceIndex, '{', '}') + if (!assignedObject) continue + // Capture both `Heading: ...` and shorthand `Visual` keys (the latter is a property whose key + // is its value, e.g. `Object.assign(Root, {Visual, Content: RiverContent})`). + for (const propertyKey of assignedObject.matchAll(/(?:^|[{,])\s*([A-Za-z_$][\w$]*)\s*(?=[:,}])/g)) + subcomponents.add(`${name}.${propertyKey[1]}`) + } + for (const match of source.matchAll(new RegExp(`\\b${name}\\.(\\w+)\\s*=`, 'g'))) { + subcomponents.add(`${name}.${match[1]}`) + } + return [...subcomponents] +} + +/** True only for a pure string-literal union like `'a' | 'b'` — not generics, indexing, or brackets. */ +function isClosedStringUnion(type) { + return /^'[^']*'(\s*\|\s*'[^']*')*$/.test(type.trim()) +} + +/** `const X = ['a', 'b'] as const` arrays in a source, keyed by name — only PURE string-literal + * arrays, since a stray identifier would yield a partial enum that wrongly rejects valid values. */ +function collectConstArrays(source) { + const arrays = new Map() + for (const match of source.matchAll(/(?:export\s+)?const\s+(\w+)\s*=\s*\[([^\]]*)\]\s*as\s+const/g)) { + const arrayContents = match[2] + const values = [...arrayContents.matchAll(/'([^']+)'|"([^"]+)"/g)].map(entry => entry[1] ?? entry[2]) + const leftover = arrayContents.replace(/'[^']*'|"[^"]*"/g, '').replace(/[\s,]/g, '') + if (values.length > 0 && leftover === '') arrays.set(match[1], values) + } + return arrays +} + +/** Parse the members of a props object literal, attaching each prop's preceding JSDoc as its description. */ +function parsePropsBlock(block, arrays) { + const props = [] + let depth = 0 + let pendingDoc = [] + const takePendingDescription = () => { + const text = pendingDoc.join(' ').replace(/\s+/g, ' ').trim() + pendingDoc = [] + if (text.length === 0) return undefined + return text.length > 240 ? `${text.slice(0, 240).replace(/\s+\S*$/, '')}…` : text + } + for (const rawLine of block.slice(1, -1).split('\n')) { + const line = rawLine.trim() + if (line === '') { + pendingDoc = [] + continue + } + // A JSDoc/comment immediately above a prop becomes that prop's description. + if (line.startsWith('/*') || line.startsWith('*') || line.startsWith('//')) { + const text = line + .replace(/^\/\*\*?/, '') + .replace(/\*\/$/, '') + .replace(/^\*\s?/, '') + .replace(/^\/\/\s?/, '') + .trim() + if (text && !text.startsWith('@')) pendingDoc.push(text) + continue + } + const braceDepthDelta = (line.match(/{/g) || []).length - (line.match(/}/g) || []).length + if (depth > 0) { + depth += braceDepthDelta + continue + } + const match = /^'?([A-Za-z][\w-]*)'?(\??):\s*(.+?);?\s*$/.exec(line) + if (match) { + const [, propName, optional, rawType] = match + let type = rawType.trim() + const opensObject = type === '{' || type.endsWith('{') + const description = takePendingDescription() + if (propName !== 'data-testid' && !type.includes('=>')) { + if (opensObject) type = 'object' + // Allowed values for a CLOSED string-literal union, else a `(typeof X)[number]` indexed const + // array. A wrong/partial enum would make primer_brand_review reject valid values, so otherwise omit. + const arrayMatch = /^\(?\s*typeof\s+(\w+)\s*\)?\[number\]$/.exec(type) ?? /^(\w+)\[number\]$/.exec(type) + const enumValues = isClosedStringUnion(type) + ? [...type.matchAll(/'([^']+)'/g)].map(value => value[1]) + : arrayMatch && arrays.get(arrayMatch[1]) + props.push({ + name: propName, + type, + ...(enumValues && enumValues.length > 0 ? {enum: enumValues} : {}), + ...(description ? {description} : {}), + required: optional !== '?', + }) + } + } else { + pendingDoc = [] + } + depth += braceDepthDelta + } + return props +} + +// Props from the exported props type alias (suffix varies). A reference alias or discriminated +// union has no object literal, so it yields no props rather than scraping a later unrelated block. +function extractProps(name, source) { + const arrays = collectConstArrays(source) + for (const typeAliasName of [`type ${name}Props`, `type ${name}RootProps`, `type ${name}BaseProps`]) { + const aliasOffset = source.indexOf(typeAliasName) + if (aliasOffset === -1) continue + const open = source.indexOf('{', aliasOffset) + const nextDeclarationOffset = source + .slice(aliasOffset) + .search(/\n\s*(export|const|let|var|function|interface|enum)\b/) + const declarationEnd = nextDeclarationOffset === -1 ? source.length : aliasOffset + nextDeclarationOffset + if (open === -1 || open > declarationEnd) continue + const block = captureBalanced(source, open, '{', '}') + if (block) return parsePropsBlock(block, arrays) + } + return [] +} + +const EXAMPLE_NOISE = [ + /\bstyles\./g, + /\buse(?:State|Effect|Ref|Callback|Transition|Memo)\b/g, + /className=/g, + /\bdecorators\b/g, + /lorem ipsum/gi, +] + +/** + * Mirror primer_brand_review's two error-level rules (unknown sub-component, invalid enum value) so a + * surfaced example can never contradict the catalog it ships inside. + */ +function exampleContradictsCatalog(code, byName) { + for (const match of code.matchAll(/<([A-Z][A-Za-z0-9]*)\.([A-Z][A-Za-z0-9]*)/g)) { + const component = byName.get(match[1]) + if (component && !component.subcomponents.includes(`${match[1]}.${match[2]}`)) return true + } + for (const component of byName.values()) { + const enumProps = component.props.filter(prop => Array.isArray(prop.enum) && prop.enum.length > 0) + if (enumProps.length === 0) continue + for (const usage of code.matchAll(new RegExp(`<${component.name}\\b[^>]*>`, 'g'))) { + for (const prop of enumProps) { + for (const attributeMatch of usage[0].matchAll(new RegExp(`\\b${prop.name}=["']([^"']+)["']`, 'g'))) { + if (!prop.enum.includes(attributeMatch[1])) return true + } + } + } + } + return false +} + +// The single best tested story example for a component, or undefined. Each matching story file's +// `export const`/`export function` regions yield balanced `=> (…)`/`return (…)` JSX, scored so +// sub-composition helps and Storybook plumbing/length hurt. +function bestStoryExample(directory, name, byName) { + // Story files whose basename matches the component; examples before features. + const storyFiles = () => { + const storyDir = join(reactSrc, directory) + let entries + try { + entries = readdirSync(storyDir) + } catch { + return [] + } + const lowerName = name.toLowerCase() + const filesWithSuffix = suffix => + entries + .filter(entry => entry.toLowerCase() === `${lowerName}.${suffix}.stories.tsx`) + .map(entry => join(storyDir, entry)) + return [...filesWithSuffix('examples'), ...filesWithSuffix('features')] + } + + // Balanced JSX from each top-level story declaration. + const jsxSnippets = source => { + const exportMatches = [...source.matchAll(/^export (?:const|function) (\w+)/gm)] + const snippets = [] + for (let index = 0; index < exportMatches.length; index += 1) { + const declarationBody = source.slice(exportMatches[index].index, exportMatches[index + 1]?.index ?? source.length) + for (const match of declarationBody.matchAll(/(?:=>|\breturn)\s*\(/g)) { + const capturedJsx = captureBalanced(declarationBody, match.index + match[0].length - 1, '(', ')') + if (capturedJsx && capturedJsx.includes('<')) snippets.push(capturedJsx) + } + } + return snippets + } + + // Strip wrapping parens and dedent into a copyable block. + const dedentJsx = capturedJsx => { + let jsxBody = capturedJsx.trim() + if (jsxBody.startsWith('(') && jsxBody.endsWith(')')) jsxBody = jsxBody.slice(1, -1) + const lines = jsxBody + .replace(/^\s*\n/, '') + .replace(/\s+$/, '') + .split('\n') + const indents = lines.filter(line => line.trim()).map(line => line.match(/^[ \t]*/)[0].length) + const dedent = indents.length > 0 ? Math.min(...indents) : 0 + return lines + .map(line => line.slice(dedent)) + .join('\n') + .trim() + } + + // Must render the component; sub-composition helps, plumbing/length hurt. -1 rejects. + const scoreSnippet = (code, fromExamplesFile) => { + if (!new RegExp(`<${name}(?:\\b|\\.)`).test(code)) return -1 + let total = fromExamplesFile ? 3 : 0 + total += (code.match(new RegExp(`<${name}\\.`, 'g')) || []).length + for (const pattern of EXAMPLE_NOISE) total -= (code.match(pattern) || []).length + if (code.length < 40) total -= 5 + if (code.length > 2200) total -= Math.ceil((code.length - 2200) / 400) + return total + } + + let bestExample + for (const file of storyFiles()) { + const source = readFileOrNull(file) + if (!source) continue + const fromExamplesFile = /\.examples\.stories\.tsx$/i.test(file) + for (const capturedJsx of jsxSnippets(source)) { + const code = dedentJsx(capturedJsx) + if (code.length > 4000 || exampleContradictsCatalog(code, byName)) continue + const relevanceScore = scoreSnippet(code, fromExamplesFile) + if (relevanceScore >= 0 && (!bestExample || relevanceScore > bestExample.score)) + bestExample = {code, score: relevanceScore} + } + } + return bestExample?.code +} + +// Best curated example from a component's docs page — the fallback when no tested story exists, so +// primitives (Stack, Text, the form controls) still get one. `noinline` playground blocks (no +// leading `<`) are skipped; the earliest, most sub-composed block that renders the component wins. +function bestDocsExample(name, byName, docsDir) { + const source = readFileOrNull(join(docsDir, 'react.mdx')) + if (!source) return undefined + let bestExample + for (const fencedBlock of source.matchAll(/```(?:jsx|tsx)[^\n]*\n([\s\S]*?)```/g)) { + const code = fencedBlock[1].trim() + if ( + !code.startsWith('<') || + code.length > 2000 || + !new RegExp(`<${name}(?:\\b|\\.)`).test(code) || + exampleContradictsCatalog(code, byName) + ) { + continue + } + let snippetScore = 10 + (code.match(new RegExp(`<${name}\\.`, 'g')) || []).length + if (code.length > 1200) snippetScore -= Math.ceil((code.length - 1200) / 400) + if (!bestExample || snippetScore > bestExample.score) bestExample = {code, score: snippetScore} + } + return bestExample?.code +} + +/** + * A one-line description authored as a JSDoc comment directly above the component's declaration + * (e.g. `/** Use the hero… *\/ export const Hero = …`). Anchored to the declaration so file-section + * banners such as `/** Design tokens *\/` are never mistaken for a description. Returns the first + * sentence, or `undefined` when no such comment exists yet — the common case today, which is why + * descriptions fall back to the docs pages below. This is the preferred source so a future backfill + * that annotates the component source is picked up automatically with no change here. + */ +function jsdocDescription(name, source) { + // Tempered `(?:(?!\*\/)[\s\S])*?` keeps the comment a single block immediately adjacent to the + // declaration, so an earlier prop doc can never be swallowed and mistaken for the component's. + const jsdocRegex = new RegExp( + `/\\*\\*((?:(?!\\*/)[\\s\\S])*?)\\*/\\s*(?:export\\s+)?(?:const|function|class)\\s+${name}(?:Root)?\\b`, + ) + const match = jsdocRegex.exec(source) + if (!match) return undefined + const text = match[1] + .split('\n') + .map(line => line.replace(/^\s*\*\s?/, '').trim()) + .filter(Boolean) + .join(' ') + .trim() + // A comment that is just a doc-site pointer (`{@link \u2026}`) is navigation, not a description \u2014 + // defer to the docs page, which carries real prose. + if (/\{@link/i.test(text)) return undefined + const sentence = (text.split(/(?<=\.)\s/)[0] ?? '').trim() + if (sentence.length < 12 || /design token|stylesheet|css module/i.test(sentence)) return undefined + return sentence +} + +/** The raw body of the component's adjacent JSDoc block (same anchoring as `jsdocDescription`). */ +function jsdocBody(name, source) { + const match = new RegExp( + `/\\*\\*((?:(?!\\*/)[\\s\\S])*?)\\*/\\s*(?:export\\s+)?(?:const|function|class)\\s+${name}(?:Root)?\\b`, + ).exec(source) + if (!match) return '' + return match[1] + .split('\n') + .map(line => line.replace(/^\s*\*\s?/, '')) + .join('\n') + .trim() +} + +/** Text of a JSDoc `@tag` block, from the tag to the next `@tag` or end; surrounding code fences stripped. */ +function jsdocTag(body, tag) { + const match = new RegExp(`(?:^|\\n)@${tag}\\b[ \\t]*([\\s\\S]*?)(?=\\n@\\w+|$)`).exec(body) + if (!match) return undefined + const text = match[1] + .trim() + .replace(/^```\w*\n?/, '') + .replace(/\n?```$/, '') + .trim() + return text || undefined +} + +// Description (lead JSDoc sentence), `@remarks` note, and `@example` snippet from a component's source. +function componentGuidance(name, source) { + const body = jsdocBody(name, source) + const note = body ? jsdocTag(body, 'remarks') : undefined + return { + description: jsdocDescription(name, source), + note: note ? note.replace(/\s+/g, ' ') : undefined, + example: body ? jsdocTag(body, 'example') : undefined, + } +} + +// Map docs folders to their page, keyed by lowercased name, scanning every section. Lowercasing +// absorbs casing drift (e.g. `Textarea` source vs the `TextArea` docs folder). +function buildDocsIndex() { + const index = new Map() + let sections + try { + sections = readdirSync(docsContentRoot, {withFileTypes: true}) + } catch { + return index + } + for (const section of sections) { + if (!section.isDirectory()) continue + let entries + try { + entries = readdirSync(join(docsContentRoot, section.name), {withFileTypes: true}) + } catch { + continue + } + for (const entry of entries) { + if (!entry.isDirectory()) continue + const directory = join(docsContentRoot, section.name, entry.name) + const key = entry.name.toLowerCase() + if (!index.has(key) && existsSync(join(directory, 'index.mdx'))) index.set(key, directory) + } + } + return index +} + +// description/keywords from the component's next-docs frontmatter; nothing for undocumented primitives. +function docsMeta(name, docsIndex) { + const directory = docsIndex.get(name.toLowerCase()) + const source = directory ? readFileOrNull(join(directory, 'index.mdx')) : null + if (!source) return {} + const frontmatter = /^---\n([\s\S]*?)\n---/.exec(source) + if (!frontmatter) return {} + const block = frontmatter[1] + const descriptionMatch = /^description:\s*(.+)$/m.exec(block) + const description = descriptionMatch ? descriptionMatch[1].trim().replace(/^['"]|['"]$/g, '') : undefined + const keywordsMatch = /^keywords:\s*\[([^\]]*)\]/m.exec(block) + const keywords = keywordsMatch + ? [...keywordsMatch[1].matchAll(/'([^']+)'|"([^"]+)"/g)].map(entry => entry[1] ?? entry[2]).filter(Boolean) + : undefined + return {description, keywords} +} + +function buildComponents() { + const discovered = discoverComponentFiles() + const docsIndex = buildDocsIndex() + const jsdocExamples = new Map() + // Phase 1 — API metadata straight from each component's own source, plus human-facing copy + // (description, note, keywords): a source JSDoc is preferred, with the docs page as fallback. + const components = discovered.map(({name, file}) => { + const source = readFileOrNull(file) ?? '' + const docsFrontmatter = docsMeta(name, docsIndex) + const guidance = componentGuidance(name, source) + if (guidance.example) jsdocExamples.set(name, guidance.example) + const description = guidance.description ?? docsFrontmatter.description + return { + name, + module: '@primer/react-brand', + subcomponents: extractSubcomponents(name, source), + props: extractProps(name, source), + examples: [], + ...(description ? {description} : {}), + ...(docsFrontmatter.keywords && docsFrontmatter.keywords.length > 0 ? {keywords: docsFrontmatter.keywords} : {}), + ...(guidance.note ? {note: guidance.note} : {}), + } + }) + // Phase 2 — attach one example, kept consistent with the metadata above (so it passes + // primer_brand_review): an authored JSDoc `@example` wins, else the best tested story, else a curated + // docs example so primitives are not left empty. + const byName = new Map(components.map(component => [component.name, component])) + for (const [index, {dir}] of discovered.entries()) { + const component = components[index] + const jsdocCode = jsdocExamples.get(component.name) + if (jsdocCode && !exampleContradictsCatalog(jsdocCode, byName)) { + component.examples = [{title: `${component.name} example`, code: jsdocCode, source: 'jsdoc'}] + continue + } + const storyCode = bestStoryExample(dir, component.name, byName) + if (storyCode) { + component.examples = [{title: `${component.name} example`, code: storyCode, source: 'story'}] + continue + } + const docsDir = docsIndex.get(component.name.toLowerCase()) + const docsCode = docsDir ? bestDocsExample(component.name, byName, docsDir) : undefined + if (docsCode) component.examples = [{title: `${component.name} example`, code: docsCode, source: 'docs'}] + } + return components +} + +// --------------------------------------------------------------------------- +// Assets (Octicons + Octovisuals) +// --------------------------------------------------------------------------- + +function exportedNames(packageName) { + const packageDir = join(nodeModules, ...packageName.split('/')) + const names = new Set() + for (const subPath of ['dist', 'lib', '.']) { + let entries + try { + entries = readdirSync(join(packageDir, subPath)) + } catch { + continue + } + for (const entry of entries) { + if (!entry.endsWith('.d.ts')) continue + const types = readFileOrNull(join(packageDir, subPath, entry)) ?? '' + // `declare const AlertIcon: Icon` (octicons) and `export declare const Foo` (octovisuals). + for (const match of types.matchAll(/(?:export\s+)?declare\s+const\s+([A-Z]\w+)/g)) names.add(match[1]) + for (const match of types.matchAll(/export\s+const\s+([A-Z]\w+)/g)) names.add(match[1]) + for (const match of types.matchAll(/export\s*\{([^}]*)\}/g)) { + for (const specifier of match[1].split(',')) { + const identifier = specifier + .trim() + .split(/\s+as\s+/) + .pop() + ?.trim() + if (identifier && /^[A-Z]\w+$/.test(identifier)) names.add(identifier) + } + } + } + } + return [...names] +} + +function buildAssets() { + const assets = [] + for (const name of exportedNames('@primer/octicons-react')) { + if (name.endsWith('Icon')) assets.push({name, module: '@primer/octicons-react', kind: 'icon'}) + } + for (const name of exportedNames('@primer/octovisuals-react')) { + // Octovisuals components are also named `...Icon`; they are distinguished by package, not suffix. + if (/^[A-Z]/.test(name) && !/(Props|Type|Metadata)$/.test(name)) { + assets.push({name, module: '@primer/octovisuals-react', kind: 'illustration'}) + } + } + return assets +} + +// --------------------------------------------------------------------------- +// Tokens (built CSS custom properties) +// --------------------------------------------------------------------------- + +function collectCssFiles(directory, files = []) { + let entries + try { + entries = readdirSync(directory, {withFileTypes: true}) + } catch { + return files + } + for (const entry of entries) { + const fullPath = join(directory, entry.name) + if (entry.isDirectory()) collectCssFiles(fullPath, files) + else if (entry.name.endsWith('.css')) files.push(fullPath) + } + return files +} + +function buildTokens() { + const tokensRoot = join(nodeModules, '@primer', 'brand-primitives', 'lib', 'design-tokens', 'css', 'tokens') + if (!existsSync(tokensRoot)) { + writeStderrLog('design tokens not built; skipping token catalog (run build:lib to include them)') + return [] + } + const byName = new Map() + for (const file of collectCssFiles(tokensRoot)) { + const group = + dirname(file) + .slice(tokensRoot.length + 1) + .split('/')[0] || 'base' + for (const match of (readFileOrNull(file) ?? '').matchAll(/(--[\w-]+):\s*([^;]+);/g)) { + const name = match[1] + if (!byName.has(name)) byName.set(name, {name, value: match[2].trim(), group}) + } + } + return [...byName.values()] +} + +// --------------------------------------------------------------------------- + +function main() { + const components = runPhaseSafely('components', buildComponents, []) + const assets = runPhaseSafely('assets', buildAssets, []) + const tokens = runPhaseSafely('tokens', buildTokens, []) + + // Sanity floor: fail loudly if extraction collapses, rather than silently shipping a broken + // catalog. Thresholds sit far below normal output (~70 components, ~400 assets), so they only + // fire on a structural regression — the "we went a level deeper and never noticed" case. + assertMinimumCount('components', components, 50) + assertMinimumCount('assets', assets, 200) + if (tokens.length === 0) { + writeStderrLog('WARNING: 0 design tokens — build @primer/brand-primitives (npm run build:lib) to include them') + } + + const generatedFromVersion = (() => { + const packageJsonText = readFileOrNull(resolve(repoRoot, 'packages', 'react', 'package.json')) + try { + return packageJsonText ? JSON.parse(packageJsonText).version ?? 'unknown' : 'unknown' + } catch { + return 'unknown' + } + })() + const catalog = { + brandPackage: '@primer/react-brand', + generatedFromVersion, + generatedAt: new Date().toISOString(), + components, + assets, + tokens, + } + + mkdirSync(dirname(outFile), {recursive: true}) + writeFileSync(outFile, `${JSON.stringify(catalog, null, 2)}\n`) + writeStderrLog( + `wrote ${components.length} components, ${assets.length} assets, ${tokens.length} tokens -> ${outFile}`, + ) +} + +function assertMinimumCount(label, items, minimum) { + if (items.length < minimum) { + throw new Error( + `catalog sanity check failed: ${label} = ${items.length} (expected >= ${minimum}). Extraction likely regressed; refusing to write a broken catalog.`, + ) + } +} + +function runPhaseSafely(label, phase, fallback) { + try { + return phase() + } catch (error) { + writeStderrLog(`phase "${label}" failed: ${error.message}`) + return fallback + } +} + +main() diff --git a/packages/mcp/scripts/smoke.mjs b/packages/mcp/scripts/smoke.mjs new file mode 100644 index 0000000000..6ec7e786be --- /dev/null +++ b/packages/mcp/scripts/smoke.mjs @@ -0,0 +1,63 @@ +#!/usr/bin/env node +/** + * End-to-end smoke test for the built server: spawn `dist/index.js` over stdio, connect a real + * MCP client, and assert its tools answer without error — including that the + * flagship `primer_brand_review` actually flags planted off-brand code. Run in CI via `npm run smoke` + * (which builds first); also handy locally. Exits non-zero on the first failed assertion. + */ +import {Client} from '@modelcontextprotocol/sdk/client/index.js' +import {StdioClientTransport} from '@modelcontextprotocol/sdk/client/stdio.js' + +function assert(condition, message) { + if (!condition) throw new Error(message) +} + +const transport = new StdioClientTransport({command: 'node', args: ['dist/index.js']}) +const client = new Client({name: 'primer-brand-mcp-smoke', version: '0.0.0'}) + +try { + await client.connect(transport) + + const {tools} = await client.listTools() + const names = tools.map(tool => tool.name).sort() + process.stdout.write(`tools (${tools.length}): ${names.join(', ')}\n`) + // Zero-maintenance signal: a healthy server registers multiple tools. The specific tools we rely + // on are exercised by the calls below, so there is no hardcoded tool list to keep in sync. + assert(tools.length > 1, `expected multiple tools, got ${tools.length}`) + + const call = async (name, args) => { + const result = await client.callTool({name, arguments: args}) + const text = result.content?.[0]?.text ?? '' + assert(!result.isError, `${name} returned an error: ${text.slice(0, 200)}`) + assert(text.length > 0, `${name} returned empty content`) + process.stdout.write(` \u2713 ${name}\n`) + return text + } + + // primer_brand_docs is intentionally not asserted here: it depends on a bundled docs dir / network, + // which isn't guaranteed offline in CI. It still appears in the printed tool list above. + await call('primer_brand_setup', {framework: 'next-app'}) + await call('primer_brand_component', {name: 'Hero'}) + await call('primer_brand_examples', {goal: 'education landing page'}) + await call('primer_brand_asset', {query: 'arrow', kind: 'icon'}) + await call('primer_brand_tokens', {query: 'accent background', limit: 4}) + + // Flagship check: deliberately off-brand code (raw
, hardcoded hex, pill radius, placeholder + // copy) must be flagged, proving the review engine runs end to end against the real catalog. + const review = await call('primer_brand_review', { + code: `import {Hero} from '@primer/react-brand' +Ship faster +
lorem ipsum
`, + }) + assert( + /error\(s\)/.test(review) && !/No issues found/.test(review), + 'primer_brand_review did not flag the planted violations', + ) + + process.stdout.write('\nsmoke: OK\n') +} catch (error) { + process.stderr.write(`\nsmoke: FAILED \u2014 ${error.message}\n`) + process.exitCode = 1 +} finally { + await client.close().catch(() => {}) +} diff --git a/packages/mcp/src/brand/detect-framework.test.ts b/packages/mcp/src/brand/detect-framework.test.ts new file mode 100644 index 0000000000..3aaf59b851 --- /dev/null +++ b/packages/mcp/src/brand/detect-framework.test.ts @@ -0,0 +1,48 @@ +import {mkdirSync, mkdtempSync, writeFileSync} from 'node:fs' +import {tmpdir} from 'node:os' +import {join} from 'node:path' + +import {detectFramework} from './detect-framework.js' + +function projectWith(deps: Record, options: {appDir?: boolean} = {}): string { + const dir = mkdtempSync(join(tmpdir(), 'primer-brand-mcp-fw-')) + writeFileSync(join(dir, 'package.json'), JSON.stringify({dependencies: deps})) + if (options.appDir) mkdirSync(join(dir, 'app')) + return dir +} + +describe('detectFramework', () => { + it('detects Vite', () => { + expect(detectFramework(projectWith({vite: '^5', react: '^19'})).id).toBe('vite') + }) + + it('detects Next App Router when an app/ dir exists (and flags RSC)', () => { + const framework = detectFramework(projectWith({next: '^15'}, {appDir: true})) + expect(framework.id).toBe('next-app') + expect(framework.rsc).toBe(true) + }) + + it('detects Next Pages Router without an app/ dir', () => { + const framework = detectFramework(projectWith({next: '^15'})) + expect(framework.id).toBe('next-pages') + expect(framework.rsc).toBe(false) + }) + + it('detects Remix', () => { + expect(detectFramework(projectWith({'@remix-run/react': '^2'})).id).toBe('remix') + }) + + it('detects Astro', () => { + expect(detectFramework(projectWith({astro: '^5', react: '^19'})).id).toBe('astro') + }) + + it('detects Next App Router from a src/app directory', () => { + const dir = projectWith({next: '^15'}) + mkdirSync(join(dir, 'src', 'app'), {recursive: true}) + expect(detectFramework(dir).id).toBe('next-app') + }) + + it('falls back to unknown when there is no project', () => { + expect(detectFramework('/no/such/directory/anywhere').id).toBe('unknown') + }) +}) diff --git a/packages/mcp/src/brand/detect-framework.ts b/packages/mcp/src/brand/detect-framework.ts new file mode 100644 index 0000000000..2006385266 --- /dev/null +++ b/packages/mcp/src/brand/detect-framework.ts @@ -0,0 +1,58 @@ +import {existsSync, readFileSync} from 'node:fs' +import {dirname, join, sep} from 'node:path' + +export type FrameworkId = 'next-app' | 'next-pages' | 'vite' | 'astro' | 'remix' | 'unknown' + +export interface FrameworkInfo { + id: FrameworkId + label: string + /** Uses React Server Components, so providers need a `'use client'` boundary. */ + rsc: boolean + projectDir: string | null +} + +/** Nearest `package.json` above `fromDir` that isn't inside `node_modules`. */ +function findProjectDir(fromDir: string): string | null { + let dir = fromDir + for (;;) { + if (!dir.split(sep).includes('node_modules') && existsSync(join(dir, 'package.json'))) return dir + const parent = dirname(dir) + if (parent === dir) return null + dir = parent + } +} + +function readDependencies(projectDir: string): Record { + try { + const parsed = JSON.parse(readFileSync(join(projectDir, 'package.json'), 'utf8')) as { + dependencies?: Record + devDependencies?: Record + } + return {...parsed.dependencies, ...parsed.devDependencies} + } catch { + return {} + } +} + +/** + * Best-effort detection of the consumer's framework so setup guidance can be tailored + * (where providers go, whether a `'use client'` boundary is required, which style import to use). + */ +export function detectFramework(fromDir: string = process.cwd()): FrameworkInfo { + const projectDir = findProjectDir(fromDir) + if (!projectDir) return {id: 'unknown', label: 'a React project', rsc: false, projectDir: null} + + const deps = readDependencies(projectDir) + if (deps.next) { + const appRouter = existsSync(join(projectDir, 'app')) || existsSync(join(projectDir, 'src', 'app')) + return appRouter + ? {id: 'next-app', label: 'Next.js (App Router)', rsc: true, projectDir} + : {id: 'next-pages', label: 'Next.js (Pages Router)', rsc: false, projectDir} + } + if (deps['@remix-run/react'] || deps['@remix-run/node']) { + return {id: 'remix', label: 'Remix', rsc: false, projectDir} + } + if (deps.astro) return {id: 'astro', label: 'Astro', rsc: false, projectDir} + if (deps.vite) return {id: 'vite', label: 'Vite + React', rsc: false, projectDir} + return {id: 'unknown', label: 'a React project', rsc: false, projectDir} +} diff --git a/packages/mcp/src/brand/docs-source.ts b/packages/mcp/src/brand/docs-source.ts new file mode 100644 index 0000000000..b249e63708 --- /dev/null +++ b/packages/mcp/src/brand/docs-source.ts @@ -0,0 +1,122 @@ +import {existsSync, readFileSync, realpathSync} from 'node:fs' +import {dirname, join, normalize, resolve, sep} from 'node:path' + +import type {Logger} from '../logger.js' +import type {BrandInstall} from './resolve-install.js' + +/** + * One entry in the documentation table of contents (parsed from `llms.txt`). + */ +export interface DocEntry { + label: string + /** Path relative to the docs root, e.g. `docs/components/Hero/index.md`. */ + path: string + description?: string +} + +export type DocOrigin = 'installed' | 'live' | 'none' + +export interface DocResult { + text: string + origin: DocOrigin + version: string | null +} + +const SITE = 'https://primer.style/brand' +const FETCH_TIMEOUT_MS = 2500 + +/** + * Tiered documentation source. + * + * Resolution order, by design: + * 1. The bundled, version-pinned docs of the installed `@primer/react-brand` (authoritative, + * offline, matches the consumer's version). + * 2. The live site's `llms.txt` / pages — best-effort enrichment for guidance that is not + * bundled, bounded by a short timeout and cached. + * + * Version-sensitive facts never depend on the network; this only covers prose guidance. + */ +export interface DocsSource { + origin: DocOrigin + version: string | null + index(): Promise + read(path: string): Promise +} + +export function createDocsSource(brand: BrandInstall, logger: Logger): DocsSource { + const cache = new Map() + + const hasLocalDocs = Boolean(brand.llmsPath && brand.docsDir) + const docsRoot = brand.docsDir ? dirname(brand.docsDir) : null + + async function fetchText(url: string): Promise { + const cached = cache.get(url) + if (cached !== undefined) return cached + try { + const response = await fetch(url, {signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)}) + if (!response.ok) return null + const text = await response.text() + cache.set(url, text) + return text + } catch (error) { + logger.debug(`live fetch failed for ${url}: ${(error as Error).message}`) + return null + } + } + + async function index(): Promise { + if (hasLocalDocs && brand.llmsPath) { + return parseToc(readFileSync(brand.llmsPath, 'utf8')) + } + const live = await fetchText(`${SITE}/llms.txt`) + return live ? parseToc(live) : [] + } + + async function read(path: string): Promise { + if (hasLocalDocs && docsRoot) { + const safe = safeJoin(docsRoot, path) + if (safe && existsSync(safe)) { + return {text: readFileSync(safe, 'utf8'), origin: 'installed', version: brand.version} + } + } + const live = await fetchText(`${SITE}/${path.replace(/^docs\//, '').replace(/\.md$/, '')}`) + if (live) return {text: live, origin: 'live', version: null} + return null + } + + return { + origin: hasLocalDocs ? 'installed' : 'live', + version: brand.version, + index, + read, + } +} + +/** + * Parse the `llms.txt` markdown table of contents into structured entries. + * Lines look like: `- [Hero](docs/components/Hero/index.md): A prominent banner...` + */ +export function parseToc(markdown: string): DocEntry[] { + const entries: DocEntry[] = [] + const linkLine = /^- \[(.+?)\]\((.+?)\)(?::\s*(.*))?$/ + for (const raw of markdown.split('\n')) { + const match = linkLine.exec(raw.trim()) + if (!match) continue + const [, label, path, description] = match + if (!label || !path) continue + entries.push({label, path, description: description?.trim() || undefined}) + } + return entries +} + +/** + * Join a user-supplied doc path onto the docs root, refusing anything that escapes it. + * Prevents path traversal (e.g. `../../etc/passwd`) reaching the filesystem. + */ +function safeJoin(root: string, path: string): string | null { + const target = resolve(join(root, normalize(path))) + const realRoot = realpathSync(root) + const prefix = realRoot.endsWith(sep) ? realRoot : `${realRoot}${sep}` + if (target !== realRoot && !target.startsWith(prefix)) return null + return target +} diff --git a/packages/mcp/src/brand/resolve-assets.test.ts b/packages/mcp/src/brand/resolve-assets.test.ts new file mode 100644 index 0000000000..2505602cad --- /dev/null +++ b/packages/mcp/src/brand/resolve-assets.test.ts @@ -0,0 +1,19 @@ +import {resolveInstalledAssets} from './resolve-assets.js' + +describe('resolveInstalledAssets', () => { + it('returns nothing when no asset packages are installed nearby', () => { + const result = resolveInstalledAssets('/no/such/directory/anywhere') + expect(result.assets).toEqual([]) + expect(result.sources).toEqual([]) + }) + + it('extracts icons from the installed Octicons package, with a version', () => { + const {assets, sources} = resolveInstalledAssets(process.cwd()) + const icons = assets.filter(asset => asset.kind === 'icon').map(asset => asset.name) + // Real package in node_modules: there should be many icons, including a stable one. + expect(icons.length).toBeGreaterThan(100) + expect(icons).toContain('ArrowRightIcon') + const octicons = sources.find(source => source.module === '@primer/octicons-react') + expect(octicons?.version).toMatch(/^\d+\.\d+\.\d+/) + }) +}) diff --git a/packages/mcp/src/brand/resolve-assets.ts b/packages/mcp/src/brand/resolve-assets.ts new file mode 100644 index 0000000000..a86a5a93c6 --- /dev/null +++ b/packages/mcp/src/brand/resolve-assets.ts @@ -0,0 +1,106 @@ +import {existsSync, readdirSync, readFileSync} from 'node:fs' +import {dirname, join} from 'node:path' + +import type {CatalogAsset} from '../catalog/types.js' + +interface AssetPackage { + module: string + kind: 'icon' | 'illustration' +} + +const ASSET_PACKAGES: AssetPackage[] = [ + {module: '@primer/octicons-react', kind: 'icon'}, + {module: '@primer/octovisuals-react', kind: 'illustration'}, +] + +export interface InstalledAssets { + assets: CatalogAsset[] + sources: Array<{module: string; version: string | null}> +} + +/** Walk up from `fromDir` to find an installed package directory. */ +function findPackageDir(packageName: string, fromDir: string): string | null { + let dir = fromDir + for (;;) { + const candidate = join(dir, 'node_modules', ...packageName.split('/')) + if (existsSync(join(candidate, 'package.json'))) return candidate + const parent = dirname(dir) + if (parent === dir) return null + dir = parent + } +} + +function readVersion(packageDir: string): string | null { + try { + return (JSON.parse(readFileSync(join(packageDir, 'package.json'), 'utf8')) as {version?: string}).version ?? null + } catch { + return null + } +} + +/** + * Collect exported PascalCase const names from a package's `.d.ts` type declarations. Octicons + * declare their icons as `declare const AlertIcon: Icon` in a sibling file, so all `.d.ts` files + * in the package's output directory are scanned, not just `index.d.ts`. + */ +function exportedNames(packageDir: string): string[] { + const names = new Set() + for (const sub of ['dist', 'lib', '.']) { + let entries: string[] + try { + entries = readdirSync(join(packageDir, sub)) + } catch { + continue + } + for (const entry of entries) { + if (!entry.endsWith('.d.ts')) continue + let source: string + try { + source = readFileSync(join(packageDir, sub, entry), 'utf8') + } catch { + continue + } + for (const match of source.matchAll(/(?:export\s+)?declare\s+const\s+([A-Z]\w+)/g)) { + if (match[1]) names.add(match[1]) + } + for (const match of source.matchAll(/export\s+const\s+([A-Z]\w+)/g)) { + if (match[1]) names.add(match[1]) + } + for (const match of source.matchAll(/export\s*\{([^}]*)\}/g)) { + for (const part of (match[1] ?? '').split(',')) { + const ident = part + .trim() + .split(/\s+as\s+/) + .pop() + ?.trim() + if (ident && /^[A-Z]\w+$/.test(ident)) names.add(ident) + } + } + } + } + return [...names] +} + +function keepName(name: string, kind: AssetPackage['kind']): boolean { + // Octovisuals components are also named `...Icon`, so they are kept by package, not suffix. + return kind === 'icon' ? name.endsWith('Icon') : /^[A-Z]/.test(name) && !/(Props|Type|Metadata)$/.test(name) +} + +/** + * Resolve the icon/illustration packages installed in the consumer's project and extract their + * exported names. This makes `primer_brand_asset` version-accurate: it reflects the Octicons/Octovisuals + * the project actually has, rather than the snapshot baked into the catalog at build time. + */ +export function resolveInstalledAssets(fromDir: string = process.cwd()): InstalledAssets { + const assets: CatalogAsset[] = [] + const sources: Array<{module: string; version: string | null}> = [] + for (const {module, kind} of ASSET_PACKAGES) { + const packageDir = findPackageDir(module, fromDir) + if (!packageDir) continue + const names = exportedNames(packageDir).filter(name => keepName(name, kind)) + if (names.length === 0) continue + sources.push({module, version: readVersion(packageDir)}) + for (const name of names) assets.push({name, module, kind}) + } + return {assets, sources} +} diff --git a/packages/mcp/src/brand/resolve-install.ts b/packages/mcp/src/brand/resolve-install.ts new file mode 100644 index 0000000000..7259fc86bc --- /dev/null +++ b/packages/mcp/src/brand/resolve-install.ts @@ -0,0 +1,76 @@ +import {existsSync, readFileSync} from 'node:fs' +import {dirname, join} from 'node:path' + +/** + * Information about the `@primer/react-brand` install discovered in the consumer's project. + * This is what makes the server version-aware: tools prefer the docs and version of the + * package the project actually depends on. + */ +export interface BrandInstall { + found: boolean + version: string | null + packageDir: string | null + /** Bundled version-pinned docs directory, when the installed version ships one. */ + docsDir: string | null + /** Bundled `llms.txt` table of contents, when present. */ + llmsPath: string | null +} + +const notFound: BrandInstall = { + found: false, + version: null, + packageDir: null, + docsDir: null, + llmsPath: null, +} + +/** + * Walk up from `fromDir` looking for `node_modules/@primer/react-brand`. We resolve from the + * working directory (not from this server's own location) so that, when launched inside a + * consumer project, we read their installed version. + */ +export function resolveBrandInstall(fromDir: string = process.cwd()): BrandInstall { + let dir = fromDir + for (;;) { + const packageDir = join(dir, 'node_modules', '@primer', 'react-brand') + if (existsSync(join(packageDir, 'package.json'))) { + const docsDir = join(packageDir, 'docs') + const llmsPath = join(packageDir, 'llms.txt') + return { + found: true, + version: readVersion(join(packageDir, 'package.json')), + packageDir, + docsDir: existsSync(docsDir) ? docsDir : null, + llmsPath: existsSync(llmsPath) ? llmsPath : null, + } + } + const parent = dirname(dir) + if (parent === dir) return notFound + dir = parent + } +} + +/** + * To properly disambiguate competing MCP libraries within Primer, we check if the project depends on `@primer/react` (the product UI library) but not Primer Brand. + * Used to gently steer agents that may have invoked the wrong design system's tools. + */ +export function looksLikeProductProject(fromDir: string = process.cwd()): boolean { + if (resolveBrandInstall(fromDir).found) return false + let dir = fromDir + for (;;) { + if (existsSync(join(dir, 'node_modules', '@primer', 'react', 'package.json'))) return true + const parent = dirname(dir) + if (parent === dir) return false + dir = parent + } +} + +function readVersion(packageJsonPath: string): string | null { + try { + const parsed = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as {version?: string} + return parsed.version ?? null + } catch { + // Unreadable manifest -> treat the version as unknown rather than failing discovery. + return null + } +} diff --git a/packages/mcp/src/catalog/load.ts b/packages/mcp/src/catalog/load.ts new file mode 100644 index 0000000000..8b0699a462 --- /dev/null +++ b/packages/mcp/src/catalog/load.ts @@ -0,0 +1,21 @@ +import {readFileSync} from 'node:fs' +import {fileURLToPath} from 'node:url' + +import type {Logger} from '../logger.js' +import {type Catalog, emptyCatalog} from './types.js' + +export function loadCatalog(logger: Logger): Catalog { + // Output next to the compiled output (`dist/catalog.json`) by the build-time generator. + const path = fileURLToPath(new URL('../catalog.json', import.meta.url)) + try { + const parsed = JSON.parse(readFileSync(path, 'utf8')) as Catalog + logger.debug( + `loaded catalog (${parsed.components.length} components, generated from @primer/react-brand@${parsed.generatedFromVersion})`, + ) + return parsed + } catch (error) { + logger.warn(`could not load catalog at ${path}: ${(error as Error).message}`) + // If missing, it falls back to an empty catalog. + return emptyCatalog() + } +} diff --git a/packages/mcp/src/catalog/types.ts b/packages/mcp/src/catalog/types.ts new file mode 100644 index 0000000000..e6694033c6 --- /dev/null +++ b/packages/mcp/src/catalog/types.ts @@ -0,0 +1,76 @@ +/** + * All types associated with the build-generated catalog that ships inside the package. + * It is the structured, offline source of truth the tools reason over so they never depend on the network for version-sensitive facts. + */ + +export interface CatalogProp { + name: string + type?: string + /** Allowed string-literal values, when the prop is an enum. */ + enum?: string[] + required?: boolean + default?: string + /** One-line prop description from its JSDoc, when documented in source. */ + description?: string +} + +export interface CatalogExample { + title: string + /** Source snippet (JSX), when one could be extracted. */ + code?: string + /** Where the snippet came from: an authored JSDoc `@example`, a tested Storybook story, or a docs page. */ + source: 'jsdoc' | 'story' | 'docs' + /** Storybook story id, when the example comes from a story. */ + storyId?: string +} + +export interface CatalogComponent { + /** Public name, e.g. `Hero`. */ + name: string + /** Package the component is imported from. */ + module: string + /** Compound sub-components, e.g. `['Hero.Heading', 'Hero.Description']`. */ + subcomponents: string[] + props: CatalogProp[] + examples: CatalogExample[] + /** One-line summary for listings, when available. */ + description?: string + /** Search keywords from the component's docs page, used to improve relevance ranking. */ + keywords?: string[] + /** A usage caveat from the component's JSDoc `@remarks` (e.g. a non-obvious prop interaction). */ + note?: string +} + +export interface CatalogAsset { + name: string + module: string + kind: 'icon' | 'illustration' +} + +export interface CatalogToken { + name: string + value: string + group: string +} + +export interface Catalog { + /** Package whose API this catalog describes. */ + brandPackage: string + /** Version of `@primer/react-brand` the catalog was generated from. */ + generatedFromVersion: string + generatedAt: string + components: CatalogComponent[] + assets: CatalogAsset[] + tokens: CatalogToken[] +} + +export function emptyCatalog(): Catalog { + return { + brandPackage: '@primer/react-brand', + generatedFromVersion: 'unknown', + generatedAt: new Date(0).toISOString(), + components: [], + assets: [], + tokens: [], + } +} diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts new file mode 100644 index 0000000000..849b181817 --- /dev/null +++ b/packages/mcp/src/index.ts @@ -0,0 +1,15 @@ +#!/usr/bin/env node +import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js' + +import {createServer} from './server.js' + +/** + * Entry point for MCP server. Transports over stdio. + */ +try { + const server = createServer() + await server.connect(new StdioServerTransport()) +} catch (error) { + process.stderr.write(`[primer-brand-mcp] fatal: ${(error as Error).message}\n`) + process.exit(1) +} diff --git a/packages/mcp/src/logger.ts b/packages/mcp/src/logger.ts new file mode 100644 index 0000000000..026b9c1224 --- /dev/null +++ b/packages/mcp/src/logger.ts @@ -0,0 +1,24 @@ +/** + * All diagnostic logging goes to stderr. Stdout is reserved exclusively for the MCP + * protocol stream (JSON-RPC over stdio), so an accidental `console.log` there would corrupt + * the transport. Keep this the only logging surface in the server. + */ + +export interface Logger { + debug(message: string): void + info(message: string): void + warn(message: string): void + error(message: string): void +} + +export function createLogger(enabled = true): Logger { + const write = (level: 'debug' | 'info' | 'warn' | 'error', message: string): void => { + if (enabled) process.stderr.write(`[primer-brand-mcp] ${level}: ${message}\n`) + } + return { + debug: message => write('debug', message), + info: message => write('info', message), + warn: message => write('warn', message), + error: message => write('error', message), + } +} diff --git a/packages/mcp/src/review/rules.test.ts b/packages/mcp/src/review/rules.test.ts new file mode 100644 index 0000000000..358c3b7d8c --- /dev/null +++ b/packages/mcp/src/review/rules.test.ts @@ -0,0 +1,91 @@ +import {existsSync, readFileSync} from 'node:fs' +import {fileURLToPath} from 'node:url' + +import type {Catalog} from '../catalog/types.js' +import {makeCatalog} from '../test-support/catalog.js' +import {allRules, brandComponentsUsed} from './rules.js' +import type {Finding} from './types.js' + +function review(code: string, catalog: Catalog = makeCatalog()): Finding[] { + return allRules.flatMap(rule => rule.run(code, catalog)) +} + +const errorsOf = (findings: Finding[]): Finding[] => findings.filter(finding => finding.severity === 'error') +const ruleIds = (findings: Finding[]): string[] => findings.map(finding => finding.rule) + +describe('primer_brand_review rules', () => { + it('flags an invented sub-component', () => { + const findings = review('Hi') + expect(ruleIds(findings)).toContain('unknown-subcomponent') + expect(errorsOf(findings)).toHaveLength(1) + }) + + it('flags an invalid enum prop value', () => { + const findings = review('x') + const finding = findings.find(entry => entry.rule === 'invalid-prop-value') + expect(finding).toBeDefined() + expect(finding?.message).toContain('start') + }) + + it('accepts valid, on-brand usage with no errors', () => { + const findings = review('Build') + expect(errorsOf(findings)).toHaveLength(0) + }) + + it('flags hardcoded hex and px', () => { + const ids = ruleIds(review('const s = {color: "#ff0000", padding: "24px"}')) + expect(ids).toContain('hardcoded-hex') + expect(ids).toContain('hardcoded-px') + }) + + it('flags raw card divs and placeholder copy', () => { + const ids = ruleIds(review('
lorem ipsum dolor
')) + expect(ids).toContain('raw-card-div') + expect(ids).toContain('placeholder-copy') + }) + + it('flags off-brand tells: pill radius and purple gradient', () => { + expect(ruleIds(review('x'))).toContain('pill-button') + expect(ruleIds(review('.hero{background: linear-gradient(90deg, purple, #4f46e5)}'))).toContain( + 'off-brand-gradient', + ) + }) + + it('flags a Heading with no explicit size (as does not set visual size)', () => { + expect(ruleIds(review('Welcome breakfast and check-in'))).toContain( + 'heading-explicit-size', + ) + }) + + it('does not flag a sized Heading or a sub-component heading', () => { + expect(ruleIds(review('Sized'))).not.toContain('heading-explicit-size') + expect(ruleIds(review('Big'))).not.toContain('heading-explicit-size') + }) + + it('reports which approved brand components were imported', () => { + const used = brandComponentsUsed("import {Hero, CTABanner} from '@primer/react-brand'", makeCatalog()) + expect(used.map(component => component.name).sort()).toEqual(['CTABanner', 'Hero']) + }) +}) + +// Self-referential guard: the design system's own canonical examples must pass review with no +// errors. A failure here means a rule produces false positives against known-correct code. +describe('primer_brand_review over generated canonical examples', () => { + const catalogPath = fileURLToPath(new URL('../../dist/catalog.json', import.meta.url)) + const hasCatalog = existsSync(catalogPath) + const testOrSkip = hasCatalog ? it : it.skip + + testOrSkip('produces no errors on any catalog example', () => { + const catalog = JSON.parse(readFileSync(catalogPath, 'utf8')) as Catalog + const examples = catalog.components.flatMap(component => + component.examples + .filter(example => example.code) + .map(example => ({name: component.name, code: example.code as string})), + ) + expect(examples.length).toBeGreaterThan(0) + for (const example of examples) { + const errors = errorsOf(allRules.flatMap(rule => rule.run(example.code, catalog))) + expect({component: example.name, errors}).toEqual({component: example.name, errors: []}) + } + }) +}) diff --git a/packages/mcp/src/review/rules.ts b/packages/mcp/src/review/rules.ts new file mode 100644 index 0000000000..dcf1ee0792 --- /dev/null +++ b/packages/mcp/src/review/rules.ts @@ -0,0 +1,256 @@ +import type {Catalog, CatalogComponent} from '../catalog/types.js' +import {escapeRegExp} from '../util/text.js' +import {type Finding, type Rule, evidence} from './types.js' + +/** + * The review rule set. Each rule is intentionally narrow and grounded in the catalog or in + * Primer Brand's published taste, so findings are objective ("this prop value is not allowed", + * "this is a retired GitHub style") rather than subjective. The set mirrors the brand checks + * an on-brand GitHub page is graded against: real components over raw HTML, design tokens over + * hardcoded values, and avoidance of well-known off-brand visual "tells". + */ + +const componentUsage = (code: string, name: string): RegExpMatchArray[] => [ + ...code.matchAll(new RegExp(`<${escapeRegExp(name)}\\b[^>]*>`, 'g')), +] + +/** `` style usages of `Root.Sub`. */ +const unknownSubcomponents: Rule = { + id: 'unknown-subcomponent', + run(code, catalog) { + const byName = new Map(catalog.components.map(component => [component.name, component])) + const findings: Finding[] = [] + const seen = new Set() + for (const match of code.matchAll(/<([A-Z][A-Za-z0-9]*)\.([A-Z][A-Za-z0-9]*)/g)) { + const root = match[1] + const sub = match[2] + if (!root || !sub) continue + const component = byName.get(root) + if (!component) continue + const qualified = `${root}.${sub}` + if (component.subcomponents.includes(qualified) || seen.has(qualified)) continue + seen.add(qualified) + const known = component.subcomponents.length + ? ` Known sub-components: ${component.subcomponents.join(', ')}.` + : '' + findings.push({ + severity: 'error', + rule: this.id, + message: `\`${qualified}\` is not a sub-component of \`${root}\`.${known}`, + evidence: evidence(match[0]), + }) + } + return findings + }, +} + +/** `` where the prop is an enum with a fixed value set. */ +const invalidPropValue: Rule = { + id: 'invalid-prop-value', + run(code, catalog) { + const findings: Finding[] = [] + for (const component of catalog.components) { + const enums = component.props.filter(prop => prop.enum && prop.enum.length > 0) + if (enums.length === 0) continue + for (const usage of componentUsage(code, component.name)) { + for (const prop of enums) { + const re = new RegExp(`\\b${escapeRegExp(prop.name)}=["']([^"']+)["']`, 'g') + for (const attr of usage[0].matchAll(re)) { + const value = attr[1] + if (value && !prop.enum?.includes(value)) { + findings.push({ + severity: 'error', + rule: this.id, + message: `\`${component.name}\` prop \`${ + prop.name + }\` does not accept \`"${value}"\`. Allowed: ${prop.enum?.map(v => `\`${v}\``).join(', ')}.`, + evidence: evidence(usage[0]), + }) + } + } + } + } + } + return findings + }, +} + +interface RawPattern { + id: string + test: RegExp + message: string +} + +const RAW_HTML_PATTERNS: RawPattern[] = [ + { + id: 'raw-form-elements', + test: /<(input|select|textarea|form)\b/i, + message: + 'Raw form elements detected. Use Primer Brand form components (e.g. `FormControl`, `TextInput`, `Select`).', + }, + { + id: 'raw-pricing-table', + test: /` detected. Use `PricingOptions` / `ComparisonTable` instead of a hand-built table.', + }, + { + id: 'raw-card-div', + test: /]*class(Name)?=["'`][^"'`]*\bcard\b/i, + message: 'A hand-rolled card `
` was detected. Use the Primer Brand `Card` component.', + }, + { + id: 'styled-heading', + test: /]*\b(style|class|className)=/i, + message: 'A styled raw heading was detected. Use the `Heading` (or `Hero.Heading`) component for type styles.', + }, +] + +const rawHtml: Rule = { + id: 'component-fidelity', + run(code) { + const findings: Finding[] = [] + for (const pattern of RAW_HTML_PATTERNS) { + const match = pattern.test.exec(code) + if (match) { + findings.push({severity: 'warning', rule: pattern.id, message: pattern.message, evidence: evidence(match[0])}) + } + } + return findings + }, +} + +const hardcodedValues: Rule = { + id: 'token-usage', + run(code) { + const findings: Finding[] = [] + const hex = [...new Set([...code.matchAll(/#[0-9a-fA-F]{3,8}\b/g)].map(match => match[0]))] + if (hex.length > 0) { + findings.push({ + severity: 'warning', + rule: 'hardcoded-hex', + message: `Hardcoded hex colors found (${hex + .slice(0, 4) + .join(', ')}). Use Primer Brand color tokens — see \`primer_brand_tokens\`.`, + }) + } + const px = [...new Set([...code.matchAll(/\b(\d{2,})px\b/g)].map(match => match[0]))].filter( + value => value !== '1px', + ) + if (px.length > 0) { + findings.push({ + severity: 'warning', + rule: 'hardcoded-px', + message: `Hardcoded pixel sizes found (${px + .slice(0, 4) + .join(', ')}). Use size/space tokens — see \`primer_brand_tokens\`.`, + }) + } + return findings + }, +} + +/** Statically detectable versions of the off-brand "tells" the visual judge penalizes. */ +const OFF_BRAND_TELLS: RawPattern[] = [ + { + id: 'off-brand-gradient', + test: /linear-gradient\([^)]*(purple|indigo|violet|#[46-9a-f][0-9a-f]?[0-9a-f]*f)/i, + message: + 'A purple/indigo gradient is the classic off-brand "SaaS" tell and is penalized hard. Use neutral surfaces with a sparing functional accent.', + }, + { + id: 'pill-button', + test: /border-?radius:\s*['"]?\s*(9999px|50%|100px)/i, + message: + 'Pill / fully-rounded shapes are off-brand. GitHub uses a modest, consistent corner radius except on Label components.', + }, + { + id: 'shadow-and-gradient', + test: /box-shadow:[^;]+;[\s\S]{0,200}gradient|gradient[\s\S]{0,200}box-shadow:/i, + message: + 'Combining box-shadow with a gradient is a retired GitHub style. Prefer flat surfaces with thin 1px borders.', + }, + { + id: 'glassmorphism', + test: /backdrop-filter:\s*blur|frosted/i, + message: 'Glassmorphism / frosted blur is off-brand. Use flat surfaces separated by subtle 1px borders.', + }, + { + id: 'placeholder-copy', + test: /lorem ipsum|your text here|placeholder text/i, + message: 'Placeholder copy detected. GitHub pages use real, specific, technical copy.', + }, + { + id: 'serif-font', + test: /font-family:\s*[^;]*\bserif\b(?!-)/i, + message: 'Serif/display fonts are off-brand. GitHub uses Mona Sans / a clean system sans.', + }, + { + id: 'heavy-font-weight', + test: /font-weight:\s*(800|900|bolder)\b/i, + message: 'Ultra-heavy/black weights are off-brand. Favour regular and medium, with bold for genuine emphasis.', + }, +] + +const offBrandTells: Rule = { + id: 'off-brand-tells', + run(code) { + const findings: Finding[] = [] + for (const tell of OFF_BRAND_TELLS) { + const match = tell.test.exec(code) + if (match) { + findings.push({severity: 'warning', rule: tell.id, message: tell.message, evidence: evidence(match[0])}) + } + } + return findings + }, +} + +/** + * `` derives its visual size from `as` when no `size` is given, and those defaults are + * often far too big for list items, cards, or body sections. Flag a standalone `Heading` with no explicit `size`; sub-component headings such as + * `Hero.Heading` carry their own context-appropriate sizing and are intentionally not matched. + */ +const headingExplicitSize: Rule = { + id: 'heading-explicit-size', + run(code) { + for (const match of code.matchAll(/]*>/g)) { + const tag = match[0] + // A spread could carry `size`, so don't second-guess it. + if (/\bsize=/.test(tag) || /\{\.\.\./.test(tag)) continue + return [ + { + severity: 'warning', + rule: this.id, + message: + '`Heading` `as` sets the semantic level, not the visual size — without an explicit `size` it renders at display scale, usually too big for list items, cards, or body sections. Set `size` for the context (e.g. `size="5"`, `size="6"`, or `size="subhead-medium"`).', + evidence: evidence(tag), + }, + ] + } + return [] + }, +} + +/** Credit for actually importing approved brand components — surfaced as guidance, not a failure. */ +export function brandComponentsUsed(code: string, catalog: Catalog): CatalogComponent[] { + const imported = new Set() + for (const block of code.matchAll(/import\s*(?:type\s*)?\{([^}]*)\}\s*from\s*['"]@primer\/react-brand['"]/g)) { + for (const part of (block[1] ?? '').split(',')) { + const name = part + .trim() + .split(/\s+as\s+/)[0] + ?.trim() + if (name) imported.add(name) + } + } + return catalog.components.filter(component => imported.has(component.name)) +} + +export const allRules: Rule[] = [ + unknownSubcomponents, + invalidPropValue, + rawHtml, + hardcodedValues, + offBrandTells, + headingExplicitSize, +] diff --git a/packages/mcp/src/review/types.ts b/packages/mcp/src/review/types.ts new file mode 100644 index 0000000000..2f03342521 --- /dev/null +++ b/packages/mcp/src/review/types.ts @@ -0,0 +1,22 @@ +import type {Catalog} from '../catalog/types.js' + +export type Severity = 'error' | 'warning' + +export interface Finding { + severity: Severity + rule: string + message: string + /** A short snippet of the offending source, for context. */ + evidence?: string +} + +export interface Rule { + id: string + run(code: string, catalog: Catalog): Finding[] +} + +/** Trim a matched snippet so findings stay compact in the agent's context. */ +export function evidence(snippet: string, max = 80): string { + const collapsed = snippet.replace(/\s+/g, ' ').trim() + return collapsed.length > max ? `${collapsed.slice(0, max - 1)}\u2026` : collapsed +} diff --git a/packages/mcp/src/server.ts b/packages/mcp/src/server.ts new file mode 100644 index 0000000000..fbf9c946b3 --- /dev/null +++ b/packages/mcp/src/server.ts @@ -0,0 +1,57 @@ +import {readFileSync} from 'node:fs' +import {fileURLToPath} from 'node:url' + +import {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js' + +import {createDocsSource} from './brand/docs-source.js' +import {resolveInstalledAssets} from './brand/resolve-assets.js' +import {looksLikeProductProject, resolveBrandInstall} from './brand/resolve-install.js' +import {loadCatalog} from './catalog/load.js' +import {createLogger} from './logger.js' +import {registerTools} from './tools/register.js' + +const INSTRUCTIONS = `Primer Brand design system tools for building GitHub web-based marketing experiences and landing pages with @primer/react-brand. + +Use these tools whenever a project builds marketing/landing pages, or depends on the @primer/react-brand design system. +This is NOT @primer/react (GitHub's product UI library); if you are building application/product UI, +use the Primer (product) tools instead. + +Typical flow: primer_brand_setup once at the start of a new page or project, primer_brand_examples to start +from correct patterns, primer_brand_component for exact props, primer_brand_tokens / primer_brand_asset for tokens and +icons, primer_brand_docs for guidance, and primer_brand_review as the final gate over your complete output (JSX and +CSS together) before you finish.` + +export function createServer(): McpServer { + const logger = createLogger() + + let version = '0.0.0' + try { + const packagePath = fileURLToPath(new URL('../package.json', import.meta.url)) + version = (JSON.parse(readFileSync(packagePath, 'utf8')) as {version?: string}).version ?? version + } catch { + // No package.json next to the bundle at runtime — fall back to the default version. + } + const server = new McpServer({name: 'Primer Brand', version}, {instructions: INSTRUCTIONS}) + + const catalog = loadCatalog(logger) + const brand = resolveBrandInstall() + if (brand.found) { + logger.info(`using @primer/react-brand@${brand.version ?? 'unknown'} from ${brand.packageDir}`) + } else if (looksLikeProductProject()) { + logger.warn('project depends on @primer/react (product UI), not @primer/react-brand; tools describe Primer Brand') + } else { + logger.info('no installed @primer/react-brand found; using the bundled snapshot') + } + + const installedAssets = resolveInstalledAssets() + const assetsOrigin = installedAssets.assets.length > 0 ? 'installed' : 'snapshot' + const assets = installedAssets.assets.length > 0 ? installedAssets.assets : catalog.assets + if (assetsOrigin === 'installed') { + const versions = installedAssets.sources.map(source => `${source.module}@${source.version ?? '?'}`).join(', ') + logger.info(`resolved icons/illustrations from installed packages: ${versions}`) + } + + const docs = createDocsSource(brand, logger) + registerTools(server, {catalog, brand, docs, logger, assets, assetsOrigin}) + return server +} diff --git a/packages/mcp/src/test-support/catalog.ts b/packages/mcp/src/test-support/catalog.ts new file mode 100644 index 0000000000..6438138196 --- /dev/null +++ b/packages/mcp/src/test-support/catalog.ts @@ -0,0 +1,122 @@ +import {createLogger} from '../logger.js' +import type {BrandInstall} from '../brand/resolve-install.js' +import type {DocsSource} from '../brand/docs-source.js' +import type {Catalog} from '../catalog/types.js' +import type {ToolContext} from '../tools/types.js' + +/** A small, deterministic catalog used across unit tests. */ +export function makeCatalog(overrides: Partial = {}): Catalog { + return { + brandPackage: '@primer/react-brand', + generatedFromVersion: '0.69.0', + generatedAt: '2026-06-24T00:00:00.000Z', + components: [ + { + name: 'Hero', + module: '@primer/react-brand', + subcomponents: ['Hero.Heading', 'Hero.Description', 'Hero.PrimaryAction'], + props: [ + { + name: 'align', + type: "'start' | 'center'", + enum: ['start', 'center'], + required: false, + description: 'Horizontal alignment of the hero content.', + }, + {name: 'variant', type: 'HeroVariant', required: false}, + ], + examples: [ + { + title: 'Hero example', + source: 'story', + code: 'Build like the best', + }, + ], + description: 'Prominent banner for the top of a landing page', + note: 'Use one Hero per page, at the very top.', + }, + { + name: 'CTABanner', + module: '@primer/react-brand', + subcomponents: ['CTABanner.Heading', 'CTABanner.ButtonGroup'], + props: [], + examples: [ + { + title: 'CTABanner example', + source: 'story', + code: ` + Build with the best + + + +`, + }, + ], + }, + {name: 'Pillar', module: '@primer/react-brand', subcomponents: [], props: [], examples: []}, + {name: 'SectionIntro', module: '@primer/react-brand', subcomponents: [], props: [], examples: []}, + {name: 'River', module: '@primer/react-brand', subcomponents: [], props: [], examples: []}, + {name: 'Stack', module: '@primer/react-brand', subcomponents: [], props: [], examples: []}, + { + name: 'PricingOptions', + module: '@primer/react-brand', + subcomponents: [], + props: [], + examples: [ + { + title: 'PricingOptions example', + source: 'story', + code: ` + + Pro + 10 + +`, + }, + ], + description: 'Pricing tiers and plan comparison', + keywords: ['plans', 'billing'], + }, + ], + assets: [ + {name: 'ArrowRightIcon', module: '@primer/octicons-react', kind: 'icon'}, + {name: 'ShieldIcon', module: '@primer/octicons-react', kind: 'icon'}, + {name: 'CopilotIcon', module: '@primer/octovisuals-react', kind: 'illustration'}, + ], + tokens: [ + {name: '--brand-color-accent-primary', value: 'var(--base-color-scale-green-7)', group: 'functional'}, + {name: '--base-size-32', value: '2rem', group: 'base'}, + {name: '--brand-color-border-default', value: 'var(--base-color-scale-gray-4)', group: 'functional'}, + {name: '--brand-borderWidth-thin', value: 'max(1px, 0.0625rem)', group: 'functional'}, + ], + ...overrides, + } +} + +const noopDocs: DocsSource = { + origin: 'none', + version: '0.69.0', + index: async () => [], + read: async () => null, +} + +const installedBrand: BrandInstall = { + found: true, + version: '0.69.0', + packageDir: '/fake/node_modules/@primer/react-brand', + docsDir: null, + llmsPath: null, +} + +export function makeContext(overrides: Partial = {}): ToolContext { + const catalog = overrides.catalog ?? makeCatalog() + return { + catalog, + brand: installedBrand, + docs: noopDocs, + logger: createLogger(false), + assets: catalog.assets, + assetsOrigin: 'snapshot', + ...overrides, + } +} diff --git a/packages/mcp/src/tools/format.ts b/packages/mcp/src/tools/format.ts new file mode 100644 index 0000000000..2b2e42f131 --- /dev/null +++ b/packages/mcp/src/tools/format.ts @@ -0,0 +1,36 @@ +import type {CatalogComponent} from '../catalog/types.js' + +import type {ToolContext} from './types.js' + +/** The fields a component is ranked against for relevance: name, description, and docs keywords. */ +export function componentSearchFields(component: CatalogComponent): Array { + return [component.name, component.description, component.keywords?.join(' ')] +} + +/** A short, consistent provenance line so agents (and humans) know what version answered. */ +export function versionNote(ctx: ToolContext): string { + if (ctx.brand.found && ctx.brand.version) { + return `_Source: \`@primer/react-brand@${ctx.brand.version}\` installed in this project._` + } + if (ctx.catalog.generatedFromVersion !== 'unknown') { + return `_Source: bundled snapshot of \`@primer/react-brand@${ctx.catalog.generatedFromVersion}\` (no install detected in this project)._` + } + return '_Source: bundled Primer Brand snapshot._' +} + +/** Render a single prop as a markdown bullet. */ +export function formatProp(prop: { + name: string + type?: string + enum?: string[] + required?: boolean + default?: string + description?: string +}): string { + const optional = prop.required ? '' : '?' + const valueType = + prop.enum && prop.enum.length > 0 ? prop.enum.map(value => `'${value}'`).join(' | ') : prop.type ?? 'unknown' + const defaultSuffix = prop.default ? ` _(default: \`${prop.default}\`)_` : '' + const descriptionSuffix = prop.description ? ` — ${prop.description}` : '' + return `- \`${prop.name}${optional}\`: \`${valueType}\`${defaultSuffix}${descriptionSuffix}` +} diff --git a/packages/mcp/src/tools/primer-brand-asset/index.ts b/packages/mcp/src/tools/primer-brand-asset/index.ts new file mode 100644 index 0000000000..c605ad8d8c --- /dev/null +++ b/packages/mcp/src/tools/primer-brand-asset/index.ts @@ -0,0 +1 @@ +export * from './primer-brand-asset.js' diff --git a/packages/mcp/src/tools/primer-brand-asset/primer-brand-asset.test.ts b/packages/mcp/src/tools/primer-brand-asset/primer-brand-asset.test.ts new file mode 100644 index 0000000000..bb367d3acf --- /dev/null +++ b/packages/mcp/src/tools/primer-brand-asset/primer-brand-asset.test.ts @@ -0,0 +1,21 @@ +import {makeContext} from '../../test-support/catalog.js' +import {primerBrandAssetTool} from './primer-brand-asset.js' + +describe('primer_brand_asset', () => { + it('finds an icon by query and emits an import statement', async () => { + const result = await primerBrandAssetTool.run({query: 'arrow', limit: 12}, makeContext()) + expect(result.text).toContain('ArrowRightIcon') + expect(result.text).toContain('@primer/octicons-react') + }) + + it('filters by kind', async () => { + const result = await primerBrandAssetTool.run({query: 'copilot', kind: 'illustration', limit: 12}, makeContext()) + expect(result.text).toContain('CopilotIcon') + expect(result.text).not.toContain('ShieldIcon') + }) + + it('notes when assets are resolved from the installed packages', async () => { + const result = await primerBrandAssetTool.run({query: 'arrow', limit: 12}, makeContext({assetsOrigin: 'installed'})) + expect(result.text.toLowerCase()).toContain('installed') + }) +}) diff --git a/packages/mcp/src/tools/primer-brand-asset/primer-brand-asset.ts b/packages/mcp/src/tools/primer-brand-asset/primer-brand-asset.ts new file mode 100644 index 0000000000..b09d36c2b3 --- /dev/null +++ b/packages/mcp/src/tools/primer-brand-asset/primer-brand-asset.ts @@ -0,0 +1,55 @@ +import {z} from 'zod' + +import {rank} from '../../util/text.js' +import type {ToolContext, ToolModule, ToolResult} from '../types.js' + +const inputSchema = z.object({ + query: z.string().describe('What you are looking for, e.g. "arrow", "shield", "rocket", "logo".'), + kind: z + .enum(['icon', 'illustration']) + .optional() + .describe('Restrict to Octicons ("icon") or Octovisuals ("illustration").'), + limit: z.number().int().min(1).max(50).optional().default(12).describe('Max results.'), +}) + +type Input = z.infer + +const description = `Find approved Primer Brand visuals as code imports: Octicons (@primer/octicons-react) and Octovisuals (@primer/octovisuals-react). Use these instead of emoji, clip art, or random icon sets — mismatched or emoji icons are off-brand.` + +export const primerBrandAssetTool: ToolModule = { + name: 'primer_brand_asset', + title: 'Primer Brand icons & illustrations', + description, + inputShape: inputSchema.shape, + annotations: {readOnlyHint: true}, + run(input, ctx: ToolContext): ToolResult { + const origin = + ctx.assetsOrigin === 'installed' + ? '_Resolved from the icon packages installed in your project._' + : '_From the bundled snapshot; your project may have a newer icon package installed._' + + let assets = ctx.assets + if (assets.length === 0) { + return {text: `No icon/illustration data is available.\n\n${origin}`, isError: true} + } + if (input.kind) { + assets = assets.filter(asset => asset.kind === input.kind) + } + + const ranked = rank(input.query, assets, asset => [asset.name]).map(entry => entry.item) + if (ranked.length === 0) { + return { + text: `No ${input.kind ?? 'asset'} matched "${ + input.query + }". Try a simpler term (e.g. "arrow", "check", "shield").\n\n${origin}`, + } + } + + const shown = ranked.slice(0, input.limit) + const more = ranked.length > shown.length ? `\n\n_${ranked.length - shown.length} more — refine your query._` : '' + const lines = shown + .map(asset => `- \`${asset.name}\` _(${asset.kind})_ — \`import {${asset.name}} from '${asset.module}'\``) + .join('\n') + return {text: `${lines}${more}\n\n${origin}`} + }, +} diff --git a/packages/mcp/src/tools/primer-brand-component/index.ts b/packages/mcp/src/tools/primer-brand-component/index.ts new file mode 100644 index 0000000000..44d43e89bb --- /dev/null +++ b/packages/mcp/src/tools/primer-brand-component/index.ts @@ -0,0 +1 @@ +export * from './primer-brand-component.js' diff --git a/packages/mcp/src/tools/primer-brand-component/primer-brand-component.test.ts b/packages/mcp/src/tools/primer-brand-component/primer-brand-component.test.ts new file mode 100644 index 0000000000..c0e69fa4a6 --- /dev/null +++ b/packages/mcp/src/tools/primer-brand-component/primer-brand-component.test.ts @@ -0,0 +1,52 @@ +import {makeContext} from '../../test-support/catalog.js' +import {primerBrandComponentTool} from './primer-brand-component.js' + +describe('primer_brand_component', () => { + it('lists components when no name is given', async () => { + const result = await primerBrandComponentTool.run({}, makeContext()) + expect(result.isError).toBeFalsy() + expect(result.text).toContain('Hero') + expect(result.text).toContain('PricingOptions') + }) + + it('shows component descriptions in the listing', async () => { + const result = await primerBrandComponentTool.run({}, makeContext()) + expect(result.text).toContain('Pricing tiers and plan comparison') + }) + + it('returns props, enums, sub-components and an example for a known component', async () => { + const result = await primerBrandComponentTool.run({name: 'Hero'}, makeContext()) + expect(result.text).toContain('Hero.Heading') + expect(result.text).toContain("'start' | 'center'") + expect(result.text).toContain('import {Hero}') + }) + + it('is case-insensitive', async () => { + const result = await primerBrandComponentTool.run({name: 'hero'}, makeContext()) + expect(result.text).toContain('# Hero') + }) + + it('renders a component note when present', async () => { + const result = await primerBrandComponentTool.run({name: 'Hero'}, makeContext()) + expect(result.text).toContain('**Note:**') + expect(result.text).toContain('Use one Hero per page') + }) + + it('renders prop descriptions', async () => { + const result = await primerBrandComponentTool.run({name: 'Hero'}, makeContext()) + expect(result.text).toContain('Horizontal alignment of the hero content.') + }) + + it('suggests alternatives for an unknown component', async () => { + const result = await primerBrandComponentTool.run({name: 'HeroBanner'}, makeContext()) + expect(result.isError).toBe(true) + expect(result.text).toContain('Hero') + }) + + it('suggests via docs keywords for an unknown component', async () => { + // "billing" is only a PricingOptions keyword, not a component name. + const result = await primerBrandComponentTool.run({name: 'billing'}, makeContext()) + expect(result.isError).toBe(true) + expect(result.text).toContain('PricingOptions') + }) +}) diff --git a/packages/mcp/src/tools/primer-brand-component/primer-brand-component.ts b/packages/mcp/src/tools/primer-brand-component/primer-brand-component.ts new file mode 100644 index 0000000000..4524e9f507 --- /dev/null +++ b/packages/mcp/src/tools/primer-brand-component/primer-brand-component.ts @@ -0,0 +1,94 @@ +import {z} from 'zod' + +import type {CatalogComponent} from '../../catalog/types.js' +import {rank} from '../../util/text.js' +import {componentSearchFields, formatProp, versionNote} from '../format.js' +import type {ToolContext, ToolModule, ToolResult} from '../types.js' + +const inputSchema = z.object({ + name: z + .string() + .optional() + .describe('Component name to look up (e.g. "Hero"). Omit to list every available component.'), +}) + +type Input = z.infer + +const description = `Primer Brand (@primer/react-brand) component reference for GitHub marketing and landing pages. Omit \`name\` to list every approved component; pass a \`name\` for its import, sub-components, props with allowed values, and a canonical example. The example is a real story snippet, so it may include Storybook \`{...args}\` spreads or imported demo assets to adapt rather than copy verbatim. Use this to avoid invented components or props. This is Primer Brand, not @primer/react product UI.` + +function listComponents(ctx: ToolContext): ToolResult { + const {components} = ctx.catalog + if (components.length === 0) { + return {text: `No component catalog is available.\n\n${versionNote(ctx)}`, isError: true} + } + const lines = [...components] + .sort((a, b) => a.name.localeCompare(b.name)) + .map(component => `- \`${component.name}\`${component.description ? ` — ${component.description}` : ''}`) + return { + text: `${ + components.length + } Primer Brand components are available. Call \`primer_brand_component\` with a \`name\` for full details.\n\n${lines.join( + '\n', + )}\n\n${versionNote(ctx)}`, + } +} + +function describeComponent(component: CatalogComponent, ctx: ToolContext): ToolResult { + const sections: string[] = [`# ${component.name} — \`${component.module}\``] + + sections.push(`\`\`\`tsx\nimport {${component.name}} from '${component.module}'\n\`\`\``) + + if (component.note) { + sections.push(`> **Note:** ${component.note}`) + } + + if (component.subcomponents.length > 0) { + sections.push(`**Sub-components:** ${component.subcomponents.map(name => `\`${name}\``).join(', ')}`) + } + + if (component.props.length > 0) { + const props = [...component.props].sort((a, b) => a.name.localeCompare(b.name)).map(formatProp) + sections.push(`## Props\n${props.join('\n')}`) + } else { + sections.push( + '## Props\n_No prop metadata was extracted for this component; check the docs with `primer_brand_docs`._', + ) + } + + const example = component.examples.find(entry => entry.code) + if (example?.code) { + sections.push(`## Example\n\`\`\`tsx\n${example.code.trim()}\n\`\`\``) + } + + sections.push(versionNote(ctx)) + return {text: sections.join('\n\n')} +} + +function getComponent(name: string, ctx: ToolContext): ToolResult { + const {components} = ctx.catalog + const match = components.find(component => component.name.toLowerCase() === name.toLowerCase()) + if (match) return describeComponent(match, ctx) + + const suggestions = rank(name, components, componentSearchFields) + .slice(0, 5) + .map(entry => `\`${entry.item.name}\``) + const hint = + suggestions.length > 0 + ? `Did you mean: ${suggestions.join(', ')}?` + : 'Call `primer_brand_component` with no arguments to list every component.' + return { + text: `There is no Primer Brand component named \`${name}\`. ${hint}\n\n${versionNote(ctx)}`, + isError: true, + } +} + +export const primerBrandComponentTool: ToolModule = { + name: 'primer_brand_component', + title: 'Primer Brand component reference', + description, + inputShape: inputSchema.shape, + annotations: {readOnlyHint: true}, + run(input, ctx) { + return input.name ? getComponent(input.name, ctx) : listComponents(ctx) + }, +} diff --git a/packages/mcp/src/tools/primer-brand-docs/index.ts b/packages/mcp/src/tools/primer-brand-docs/index.ts new file mode 100644 index 0000000000..e5bf00cb6d --- /dev/null +++ b/packages/mcp/src/tools/primer-brand-docs/index.ts @@ -0,0 +1 @@ +export * from './primer-brand-docs.js' diff --git a/packages/mcp/src/tools/primer-brand-docs/primer-brand-docs.ts b/packages/mcp/src/tools/primer-brand-docs/primer-brand-docs.ts new file mode 100644 index 0000000000..ecebbfc2cd --- /dev/null +++ b/packages/mcp/src/tools/primer-brand-docs/primer-brand-docs.ts @@ -0,0 +1,71 @@ +import {z} from 'zod' + +import {rank} from '../../util/text.js' +import {versionNote} from '../format.js' +import type {ToolContext, ToolModule, ToolResult} from '../types.js' + +const inputSchema = z.object({ + query: z.string().optional().describe('Search guidance, e.g. "accessibility", "color usage", "getting started".'), + path: z.string().optional().describe('Read a specific doc by its path (from a previous search result).'), +}) + +type Input = z.infer + +const description = `Search and read Primer Brand guidance — principles, accessibility, content, and getting started. Prefers the version-pinned docs bundled with the installed @primer/react-brand, falling back to the live site (https://primer.style/brand) for general guidance. This is brand concepts and how-to, not component APIs (use primer_brand_component for those).` + +async function readPath(path: string, ctx: ToolContext): Promise { + const doc = await ctx.docs.read(path) + if (!doc) { + return { + text: `Could not read \`${path}\`. Run \`primer_brand_docs\` with a \`query\` to find valid paths.`, + isError: true, + } + } + const origin = doc.origin === 'installed' ? 'installed package (version-pinned)' : 'live site' + return {text: `${doc.text}\n\n---\n_Doc source: ${origin}._`} +} + +async function search(query: string, ctx: ToolContext): Promise { + const entries = await ctx.docs.index() + if (entries.length === 0) { + return { + text: `No documentation index is available (no bundled docs found and the live site was unreachable).\n\n${versionNote( + ctx, + )}`, + isError: true, + } + } + + const ranked = query + ? rank(query, entries, entry => [entry.label, entry.description, entry.path]).map(entry => entry.item) + : entries + + const chosen = ranked.length > 0 ? ranked : entries + const note = + query && ranked.length === 0 + ? `No exact match for "${query}". Showing the full index:` + : query + ? `Results for "${query}":` + : 'Primer Brand documentation:' + + const lines = chosen + .slice(0, 25) + .map(entry => `- **${entry.label}** — \`${entry.path}\`${entry.description ? `: ${entry.description}` : ''}`) + return { + text: `${note}\n\n${lines.join('\n')}\n\nCall \`primer_brand_docs\` with a \`path\` to read one.\n\n${versionNote( + ctx, + )}`, + } +} + +export const primerBrandDocsTool: ToolModule = { + name: 'primer_brand_docs', + title: 'Primer Brand guidance', + description, + inputShape: inputSchema.shape, + annotations: {readOnlyHint: true}, + run(input, ctx) { + if (input.path) return readPath(input.path, ctx) + return search(input.query ?? '', ctx) + }, +} diff --git a/packages/mcp/src/tools/primer-brand-examples/index.ts b/packages/mcp/src/tools/primer-brand-examples/index.ts new file mode 100644 index 0000000000..19b3b283ee --- /dev/null +++ b/packages/mcp/src/tools/primer-brand-examples/index.ts @@ -0,0 +1 @@ +export * from './primer-brand-examples.js' diff --git a/packages/mcp/src/tools/primer-brand-examples/primer-brand-examples.test.ts b/packages/mcp/src/tools/primer-brand-examples/primer-brand-examples.test.ts new file mode 100644 index 0000000000..cbaea73b1d --- /dev/null +++ b/packages/mcp/src/tools/primer-brand-examples/primer-brand-examples.test.ts @@ -0,0 +1,29 @@ +import {makeContext} from '../../test-support/catalog.js' +import {primerBrandExamplesTool} from './primer-brand-examples.js' + +describe('primer_brand_examples', () => { + it('falls back to the default foundational set when nothing matches the goal', async () => { + const result = await primerBrandExamplesTool.run({goal: 'zzzz nonexistent zzzz'}, makeContext()) + expect(result.isError).toBeFalsy() + expect(result.text.toLowerCase()).toContain('default') + expect(result.text).toContain('Hero') + expect(result.text.toLowerCase()).toContain('adapt') + }) + + it('ranks examples by goal', async () => { + const result = await primerBrandExamplesTool.run({goal: 'pricing'}, makeContext()) + expect(result.text).toContain('PricingOptions') + }) + + it('matches on docs keywords, not just the name and description', async () => { + // "billing" appears only in PricingOptions' keywords — not its name or description. + const result = await primerBrandExamplesTool.run({goal: 'billing'}, makeContext()) + expect(result.text).toContain('PricingOptions') + expect(result.text.toLowerCase()).not.toContain('default') + }) + + it('never emits a stub block for a component that has no example', async () => { + const result = await primerBrandExamplesTool.run({goal: 'zzzz nonexistent zzzz'}, makeContext()) + expect(result.text).not.toContain('### Stack') + }) +}) diff --git a/packages/mcp/src/tools/primer-brand-examples/primer-brand-examples.ts b/packages/mcp/src/tools/primer-brand-examples/primer-brand-examples.ts new file mode 100644 index 0000000000..26f7d64374 --- /dev/null +++ b/packages/mcp/src/tools/primer-brand-examples/primer-brand-examples.ts @@ -0,0 +1,66 @@ +import {z} from 'zod' + +import type {CatalogComponent} from '../../catalog/types.js' +import {rank} from '../../util/text.js' +import {componentSearchFields, versionNote} from '../format.js' +import type {ToolModule, ToolResult} from '../types.js' + +const inputSchema = z.object({ + goal: z + .string() + .optional() + .describe( + 'What you are building, e.g. "education landing page" or "pricing section". Omit for a foundational starter set.', + ), +}) + +type Input = z.infer + +const description = `Get ranked, copy-and-adapt examples of correct Primer Brand usage for a goal, taken from the component library's own tested Storybook stories — real compositions to adapt, not turnkey templates. Pass a goal like "pricing section" or "education landing page" for the closest matching examples; with no match you get a default foundational set, so it is never a dead end. Because they come verbatim from stories, examples may include Storybook scaffolding — \`{...args}\` spreads, \`args\`-driven props, and imported demo assets (images, avatars); treat those as placeholders to fill in with real props and content, not literal code to copy. Use it to start from approved patterns instead of hand-building.` + +/** Foundational sections that anchor almost every GitHub landing page, in composition order. */ +const DEFAULT_COMPONENTS = ['Hero', 'SectionIntro', 'River', 'Pillar', 'CTABanner'] + +function exampleCode(component: CatalogComponent): string | undefined { + return component.examples.find(entry => entry.code)?.code?.trim() +} + +export const primerBrandExamplesTool: ToolModule = { + name: 'primer_brand_examples', + title: 'Primer Brand usage examples', + description, + inputShape: inputSchema.shape, + annotations: {readOnlyHint: true}, + run(input, ctx): ToolResult { + const goal = input.goal?.trim() || 'landing page' + // Only surface components that actually have a tested example — never emit a stub. + const withExamples = ctx.catalog.components.filter(component => exampleCode(component)) + if (withExamples.length === 0) { + return { + text: `No usage examples are available yet. Call \`primer_brand_component\` to explore component APIs.\n\n${versionNote( + ctx, + )}`, + isError: true, + } + } + + const matched = rank(goal, withExamples, componentSearchFields) + .slice(0, 6) + .map(entry => entry.item) + const useDefault = matched.length === 0 + const defaultSet = DEFAULT_COMPONENTS.map(name => withExamples.find(component => component.name === name)).filter( + (component): component is CatalogComponent => Boolean(component), + ) + // Guarantee content even if none of the default components have an example yet. + const shown = useDefault ? (defaultSet.length > 0 ? defaultSet : withExamples.slice(0, 5)) : matched + + const note = useDefault + ? `No example matched "${goal}", so here is the default foundational set — compose these in order and adapt the copy and props to your theme.` + : `Closest tested examples for "${goal}". Adapt the copy and props to your theme; don't paste verbatim.` + + const examples = shown + .map(component => `### ${component.name}\n\`\`\`tsx\n${exampleCode(component)}\n\`\`\``) + .join('\n\n') + return {text: [`# Examples for "${goal}"`, note, examples, versionNote(ctx)].join('\n\n')} + }, +} diff --git a/packages/mcp/src/tools/primer-brand-review/index.ts b/packages/mcp/src/tools/primer-brand-review/index.ts new file mode 100644 index 0000000000..5d81429b1a --- /dev/null +++ b/packages/mcp/src/tools/primer-brand-review/index.ts @@ -0,0 +1 @@ +export * from './primer-brand-review.js' diff --git a/packages/mcp/src/tools/primer-brand-review/primer-brand-review.test.ts b/packages/mcp/src/tools/primer-brand-review/primer-brand-review.test.ts new file mode 100644 index 0000000000..b082ed581f --- /dev/null +++ b/packages/mcp/src/tools/primer-brand-review/primer-brand-review.test.ts @@ -0,0 +1,24 @@ +import {makeContext} from '../../test-support/catalog.js' +import {primerBrandReviewTool} from './primer-brand-review.js' + +describe('primer_brand_review', () => { + it('reminds the agent to include CSS when given JSX only', async () => { + const code = `import {Hero} from '@primer/react-brand'\nShip faster` + const result = await primerBrandReviewTool.run({code}, makeContext()) + expect(result.text).toContain('Include your CSS') + }) + + it('does not remind when a stylesheet is included, and reviews the CSS', async () => { + const code = `Ship faster\n.hero { padding: 24px; }` + const result = await primerBrandReviewTool.run({code}, makeContext()) + expect(result.text).not.toContain('Include your CSS') + // The hardcoded px lives in the CSS — the full-output review must catch it. + expect(result.text).toContain('24px') + }) + + it('flags raw HTML elements in the full output', async () => { + const code = `
\n.field { margin: 12px; }` + const result = await primerBrandReviewTool.run({code}, makeContext()) + expect(result.text.toLowerCase()).toContain('raw') + }) +}) diff --git a/packages/mcp/src/tools/primer-brand-review/primer-brand-review.ts b/packages/mcp/src/tools/primer-brand-review/primer-brand-review.ts new file mode 100644 index 0000000000..748c7fc8d5 --- /dev/null +++ b/packages/mcp/src/tools/primer-brand-review/primer-brand-review.ts @@ -0,0 +1,79 @@ +import {z} from 'zod' + +import {allRules, brandComponentsUsed} from '../../review/rules.js' +import type {Finding} from '../../review/types.js' +import {versionNote} from '../format.js' +import type {ToolContext, ToolModule, ToolResult} from '../types.js' + +const inputSchema = z.object({ + code: z + .string() + .describe( + 'Your COMPLETE output to check: the JSX/TSX AND all CSS/stylesheets together. Include App.css and any *.module.css — hardcoded px/hex usually live there.', + ), + filename: z.string().optional().describe('Optional filename, used only to label the report.'), +}) + +type Input = z.infer + +const description = `The final on-brand gate. Before you finish, paste your COMPLETE output in one call — the JSX/TSX AND every stylesheet (App.css, *.module.css, styled blocks) together — because hardcoded sizes/colors and raw HTML most often hide in CSS. Flags non-compliant components or sub-components, invalid prop values, headings left at their oversized default (a \`Heading\` with no explicit \`size\`), raw HTML where a brand component exists, hardcoded colors/sizes that should be tokens, and off-brand visual tells (purple gradients, pill buttons, glassmorphism, placeholder copy). Run it on everything you wrote and fix what it reports rather than guessing.` + +function formatFindings(label: string, findings: Finding[]): string { + if (findings.length === 0) return '' + const lines = findings.map(finding => { + const evidenceLine = finding.evidence ? `\n > \`${finding.evidence}\`` : '' + return `- **${finding.rule}**: ${finding.message}${evidenceLine}` + }) + return `## ${label}\n${lines.join('\n')}` +} + +const hasJsx = (code: string): boolean => /<[A-Za-z][A-Za-z0-9.]*[\s/>]/.test(code) +// Clearly a stylesheet: a class/id/at-rule selector carrying CSS declarations (not a TS object/type). +const hasStylesheet = (code: string): boolean => + /[.#][\w-]+[^{}]*\{[^{}]*:[^{}]*;/.test(code) || /@(?:media|font-face|keyframes|supports|import)\b/.test(code) + +/** Nudge agents to include their CSS — where most hardcoded px/hex and raw elements actually hide. */ +function cssGateReminder(code: string): string { + if (!hasJsx(code) || hasStylesheet(code)) return '' + return '> **Include your CSS.** This looks like JSX/TSX only. Hardcoded `px`/hex and raw elements most often live in a separate stylesheet — re-run `primer_brand_review` with the JSX **and** every stylesheet (`App.css`, `*.module.css`, styled blocks) together. This is the final gate over your COMPLETE output.' +} + +export const primerBrandReviewTool: ToolModule = { + name: 'primer_brand_review', + title: 'Review code against Primer Brand', + description, + inputShape: inputSchema.shape, + annotations: {readOnlyHint: true}, + run(input, ctx: ToolContext): ToolResult { + const findings = allRules.flatMap(rule => rule.run(input.code, ctx.catalog)) + const errors = findings.filter(finding => finding.severity === 'error') + const warnings = findings.filter(finding => finding.severity === 'warning') + const used = brandComponentsUsed(input.code, ctx.catalog) + + const header = input.filename ? `# Brand review — \`${input.filename}\`` : '# Brand review' + const reminder = cssGateReminder(input.code) + const usedLine = + used.length > 0 + ? `Approved Primer Brand components used: ${used.map(component => `\`${component.name}\``).join(', ')}.` + : 'No `@primer/react-brand` components were imported. Build from Primer Brand components rather than raw HTML.' + + if (findings.length === 0) { + return { + text: [header, reminder, `No issues found. ${usedLine}`, versionNote(ctx)].filter(Boolean).join('\n\n'), + } + } + + const summary = `${errors.length} error(s), ${warnings.length} warning(s).` + const blocks = [ + header, + reminder, + summary, + usedLine, + formatFindings('Errors', errors), + formatFindings('Warnings', warnings), + versionNote(ctx), + ].filter(Boolean) + + return {text: blocks.join('\n\n')} + }, +} diff --git a/packages/mcp/src/tools/primer-brand-setup/index.ts b/packages/mcp/src/tools/primer-brand-setup/index.ts new file mode 100644 index 0000000000..fdc52eb179 --- /dev/null +++ b/packages/mcp/src/tools/primer-brand-setup/index.ts @@ -0,0 +1 @@ +export * from './primer-brand-setup.js' diff --git a/packages/mcp/src/tools/primer-brand-setup/primer-brand-setup.test.ts b/packages/mcp/src/tools/primer-brand-setup/primer-brand-setup.test.ts new file mode 100644 index 0000000000..d358d99791 --- /dev/null +++ b/packages/mcp/src/tools/primer-brand-setup/primer-brand-setup.test.ts @@ -0,0 +1,43 @@ +import {makeContext} from '../../test-support/catalog.js' +import {primerBrandSetupTool} from './primer-brand-setup.js' + +describe('primer_brand_setup', () => { + it('uses the ESM import path + fonts for Vite, with no use-client boundary or lib css import', async () => { + const result = await primerBrandSetupTool.run({framework: 'vite'}, makeContext()) + expect(result.text).toContain("from '@primer/react-brand/esm'") + expect(result.text).toContain('@primer/react-brand/fonts/fonts.css') + expect(result.text).toContain('main.tsx') + expect(result.text).not.toContain("'use client'") + expect(result.text).not.toContain("from '@primer/react-brand/lib") + }) + + it('includes a use-client boundary and RSC note for Next App Router', async () => { + const result = await primerBrandSetupTool.run({framework: 'next-app'}, makeContext()) + expect(result.text).toContain("'use client'") + expect(result.text).toContain('app/layout.tsx') + expect(result.text).toContain('RSC boundary') + }) + + it('returns Pages Router setup without a use-client boundary', async () => { + const result = await primerBrandSetupTool.run({framework: 'next-pages'}, makeContext()) + expect(result.text).toContain('pages/_app.tsx') + expect(result.text).not.toContain("'use client'") + }) + + it('returns Remix and Astro setups', async () => { + expect((await primerBrandSetupTool.run({framework: 'remix'}, makeContext())).text).toContain('app/root.tsx') + expect((await primerBrandSetupTool.run({framework: 'astro'}, makeContext())).text).toContain('client:load') + }) + + it('warns against double-loading styles and notes the colorMode option', async () => { + const result = await primerBrandSetupTool.run({framework: 'vite'}, makeContext()) + expect(result.text).toContain('main.css') + expect(result.text).toContain('colorMode') + }) + + it('falls back to a generic root snippet + tool pointers when the framework is unknown', async () => { + const result = await primerBrandSetupTool.run({framework: 'auto'}, makeContext()) + expect(result.text).toContain('Wrap the very root') + expect(result.text).toContain('primer_brand_review') + }) +}) diff --git a/packages/mcp/src/tools/primer-brand-setup/primer-brand-setup.ts b/packages/mcp/src/tools/primer-brand-setup/primer-brand-setup.ts new file mode 100644 index 0000000000..5c5929993d --- /dev/null +++ b/packages/mcp/src/tools/primer-brand-setup/primer-brand-setup.ts @@ -0,0 +1,139 @@ +import {z} from 'zod' + +import {detectFramework, type FrameworkId} from '../../brand/detect-framework.js' +import {versionNote} from '../format.js' +import type {ToolContext, ToolModule, ToolResult} from '../types.js' + +const fence = '```' + +const inputSchema = z.object({ + framework: z + .enum(['auto', 'next-app', 'next-pages', 'vite', 'astro', 'remix']) + .optional() + .default('auto') + .describe('Target framework. "auto" detects it from the project; override if detection is wrong.'), +}) + +type Input = z.infer + +const description = `Set up the Primer Brand (@primer/react-brand) foundation that agents routinely forget: the ThemeProvider at the app root, the Mona Sans font import, the correct style import, and the \`'use client'\` boundary for React Server Components. Detects the framework (Next App/Pages, Vite, Astro, Remix) and returns tailored, copy-ready setup. Call this once before building a Primer Brand page.` + +const STATIC: Record = { + 'next-app': {label: 'Next.js (App Router)', rsc: true}, + 'next-pages': {label: 'Next.js (Pages Router)', rsc: false}, + vite: {label: 'Vite + React', rsc: false}, + astro: {label: 'Astro', rsc: false}, + remix: {label: 'Remix', rsc: false}, + unknown: {label: 'a React project', rsc: false}, +} + +const ROOT_SNIPPETS: Record, string> = { + 'next-app': `// app/providers.tsx +'use client' +import {ThemeProvider} from '@primer/react-brand/esm' + +export function Providers({children}: {children: React.ReactNode}) { + return {children} +} + +// app/layout.tsx +import '@primer/react-brand/fonts/fonts.css' +import {Providers} from './providers' + +export default function RootLayout({children}: {children: React.ReactNode}) { + return ( + + + {children} + + + ) +}`, + 'next-pages': `// pages/_app.tsx +import '@primer/react-brand/fonts/fonts.css' +import {ThemeProvider} from '@primer/react-brand/esm' +import type {AppProps} from 'next/app' + +export default function App({Component, pageProps}: AppProps) { + return ( + + + + ) +}`, + vite: `// src/main.tsx +import '@primer/react-brand/fonts/fonts.css' +import {StrictMode} from 'react' +import {createRoot} from 'react-dom/client' +import {ThemeProvider} from '@primer/react-brand/esm' +import App from './App' + +createRoot(document.getElementById('root')!).render( + + + + + , +)`, + remix: `// app/root.tsx +import '@primer/react-brand/fonts/fonts.css' +import {ThemeProvider} from '@primer/react-brand/esm' +import {Outlet} from '@remix-run/react' + +export default function App() { + return ( + + + + ) +}`, + astro: `// src/components/BrandRoot.tsx — a React island wrapper +import '@primer/react-brand/fonts/fonts.css' +import {ThemeProvider} from '@primer/react-brand/esm' + +export default function BrandRoot({children}: {children: React.ReactNode}) { + return {children} +} + +// In an .astro file, hydrate it: ...`, +} + +const GENERIC_SNIPPET = `import '@primer/react-brand/fonts/fonts.css' +import {ThemeProvider} from '@primer/react-brand/esm' + +// Wrap the very root of your app so theming applies everywhere: + + +` + +function build(id: FrameworkId, ctx: ToolContext): string { + const {label, rsc} = STATIC[id] + const snippet = id === 'unknown' ? GENERIC_SNIPPET : ROOT_SNIPPETS[id] + + const rscNote = rsc + ? `\n\n> **RSC boundary:** \`ThemeProvider\` uses React context, so it must live in a \`'use client'\` component (the \`Providers\` wrapper above). It cannot go directly in the server-rendered \`layout.tsx\`.` + : '' + + return [ + `# Set up Primer Brand — ${label}`, + `## 1. Install\n${fence}bash\nnpm install @primer/react-brand\n${fence}`, + `## 2. Root setup (the step agents usually skip)\n${fence}tsx\n${snippet}\n${fence}${rscNote}\n\n_Optional: set the theme with \`\` — also accepts \`"dark"\` or \`"auto"\`._`, + `## 3. Fonts\nPrimer Brand uses **Mona Sans / Hubot Sans**. The \`fonts.css\` import above loads them — pages without these fonts read as off-brand.`, + `## 4. Styles\nImporting components from \`@primer/react-brand/esm\` auto-includes each component's styles. **Do not also import \`@primer/react-brand/lib/css/main.css\`** — that is the non-ESM path, and mixing the two double-loads styles.`, + `## 5. Build the page\n- \`primer_brand_examples\` for a correct starting composition, then \`primer_brand_component\` for exact props\n- \`primer_brand_tokens\` / \`primer_brand_asset\` for colors, spacing, and icons\n- \`primer_brand_review\` on your complete output — JSX and CSS together — before you finish`, + `## 6. Header & footer\nFor a global header use \`SubdomainNavBar\`; for the footer use \`MinimalFooter\`. Don't hand-roll a \`
\`, \`