Commit 58201380 authored by LongLD's avatar LongLD

feat: implement MCP server with tools for searching, retrieving, composing,...

feat: implement MCP server with tools for searching, retrieving, composing, and refreshing project rules.
parent f00391ab
# Dependencies
node_modules
.pnp
.pnp.js
# Production
dist/
build/
# Misc
.DS_Store
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
logs
*.log
npm-debug.log*
pnpm-debug.log*
lerna-debug.log*
# Testing
coverage/
# IDEs
.vscode/
.idea/
*.swp
*.swo
---
id: "backend-api"
title: "Backend API Standards"
tags: ["backend", "api", "node", "express"]
priority: 85
paths: ["**/api/**", "**/server/**", "**/controllers/**"]
---
# Backend API Standards
## RESTful Design
- Use standard HTTP methods: `GET` for retrieval, `POST` for creation, `PUT/PATCH` for updates, `DELETE` for removal.
- Resource URLs should be plural nouns (e.g., `/api/users`, not `/api/getUser`).
## Error Handling
- Return standardized error responses.
- Use HTTP status codes correctly (200 OK, 201 Created, 400 Bad Request, 401 Unauthorized, 404 Not Found, 500 Internal Server Error).
```json
{
"error": {
"code": "INVALID_INPUT",
"message": "Email is required."
}
}
```
## Database Interaction
- Use an ORM or Query Builder (e.g., Prisma, Drizzle) to prevent SQL injection.
- Do not perform database queries inside loops. Use batching.
---
id: "global-base"
title: "Global Base Rules"
tags: ["global", "base"]
priority: 100
paths: ["**/*"]
---
# Global Project Rules
## Core Principles
- **DRY (Don't Repeat Yourself)**: Avoid duplication. Extract reusable logic.
- **KISS (Keep It Simple, Stupid)**: Prefer simple solutions over complex ones.
- **Clean Code**: Write code that is readable and maintainable.
## Security (Non-Negotiable)
> [!IMPORTANT]
> - Never hardcode secrets (API keys, tokens, passwords). Use environment variables.
> - Validate all user inputs at the API boundary.
> - Ensure dependencies are audited for vulnerabilities.
## Git & Version Control
- Commit messages must be descriptive (e.g., `feat: check user permissions`).
- Do not commit generated files or secrets.
---
id: "frontend-react"
title: "React Best Practices"
tags: ["frontend", "react", "tsx", "ui"]
priority: 90
paths: ["**/*.tsx", "**/components/**"]
applies_when: ["frontend", "component"]
---
# React Best Practices
## Components
- **Functional Components**: Use functional components with hooks. Avoid class components.
- **PascalCase**: Component filenames and names must use PascalCase (e.g., `UserProfile.tsx`).
- **Props Interface**: Define a `Props` interface for component props.
```typescript
interface Props {
userId: string;
isActive?: boolean;
}
export const UserProfile = ({ userId, isActive = false }: Props) => {
// ...
};
```
## State Management
- Use `useState` for local state and `useContext` or global state managers (like Jotai/Zustand) for shared state.
- Avoid prop drilling deeper than 2 levels.
## Performance
- Use `useMemo` and `useCallback` only when necessary to prevent expensive re-renders or stabilize references.
- Lazy load routes and heavy components using `React.lazy`.
{
"name": "awing-rules-claudecode-mcp",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "awing-rules-claudecode-mcp",
"version": "1.0.0",
"license": "ISC",
"devDependencies": {
"@modelcontextprotocol/sdk": "^1.25.3",
"@types/glob": "^8.1.0",
"@types/lunr": "^2.3.7",
"@types/minimatch": "^5.1.2",
"@types/node": "^25.1.0",
"glob": "^13.0.0",
"gray-matter": "^4.0.3",
"lunr": "^2.3.9",
"minimatch": "^10.1.1",
"typescript": "^5.9.3",
"zod": "^4.3.6"
}
},
"node_modules/@hono/node-server": {
"version": "1.19.9",
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz",
"integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18.14.1"
},
"peerDependencies": {
"hono": "^4"
}
},
"node_modules/@isaacs/balanced-match": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
"integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@isaacs/brace-expansion": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@isaacs/balanced-match": "^4.0.1"
},
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@modelcontextprotocol/sdk": {
"version": "1.25.3",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.3.tgz",
"integrity": "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ==",
"dev": true,
"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.0.1",
"express-rate-limit": "^7.5.0",
"jose": "^6.1.1",
"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.0"
},
"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/@types/glob": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz",
"integrity": "sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/minimatch": "^5.1.2",
"@types/node": "*"
}
},
"node_modules/@types/lunr": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/@types/lunr/-/lunr-2.3.7.tgz",
"integrity": "sha512-Tb/kUm38e8gmjahQzdCKhbdsvQ9/ppzHFfsJ0dMs3ckqQsRj+P5IkSAwFTBrBxdyr3E/LoMUUrZngjDYAjiE3A==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/minimatch": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz",
"integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": {
"version": "25.1.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.1.0.tgz",
"integrity": "sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.16.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==",
"dev": true,
"license": "MIT",
"dependencies": {
"mime-types": "^3.0.0",
"negotiator": "^1.0.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/ajv": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/ajv-formats": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
"integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ajv": "^8.0.0"
},
"peerDependencies": {
"ajv": "^8.0.0"
},
"peerDependenciesMeta": {
"ajv": {
"optional": true
}
}
},
"node_modules/argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"dev": true,
"license": "MIT",
"dependencies": {
"sprintf-js": "~1.0.2"
}
},
"node_modules/body-parser": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
"integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==",
"dev": true,
"license": "MIT",
"dependencies": {
"bytes": "^3.1.2",
"content-type": "^1.0.5",
"debug": "^4.4.3",
"http-errors": "^2.0.0",
"iconv-lite": "^0.7.0",
"on-finished": "^2.4.1",
"qs": "^6.14.1",
"raw-body": "^3.0.1",
"type-is": "^2.0.1"
},
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/call-bound": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"get-intrinsic": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/content-disposition": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
"integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==",
"dev": true,
"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==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"dev": true,
"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==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.6.0"
}
},
"node_modules/cors": {
"version": "2.8.6",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
"integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
"dev": true,
"license": "MIT",
"dependencies": {
"object-assign": "^4",
"vary": "^1"
},
"engines": {
"node": ">= 0.10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"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==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"dev": true,
"license": "MIT"
},
"node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"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==",
"dev": true,
"license": "MIT"
},
"node_modules/esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"dev": true,
"license": "BSD-2-Clause",
"bin": {
"esparse": "bin/esparse.js",
"esvalidate": "bin/esvalidate.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/eventsource": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
"integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==",
"dev": true,
"license": "MIT",
"dependencies": {
"eventsource-parser": "^3.0.1"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/eventsource-parser": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz",
"integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/express": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
"dev": true,
"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": "7.5.1",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz",
"integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/express-rate-limit"
},
"peerDependencies": {
"express": ">= 4.11"
}
},
"node_modules/extend-shallow": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
"integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-extendable": "^0.1.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true,
"license": "MIT"
},
"node_modules/fast-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
"integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "BSD-3-Clause"
},
"node_modules/finalhandler": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
"integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
"dev": true,
"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/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/fresh": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
"integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dev": true,
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/glob": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz",
"integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"minimatch": "^10.1.1",
"minipass": "^7.1.2",
"path-scurry": "^2.0.0"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/gopd": {
"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"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/gray-matter": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz",
"integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"js-yaml": "^3.13.1",
"kind-of": "^6.0.2",
"section-matter": "^1.0.0",
"strip-bom-string": "^1.0.0"
},
"engines": {
"node": ">=6.0"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/hono": {
"version": "4.11.7",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.11.7.tgz",
"integrity": "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=16.9.0"
}
},
"node_modules/http-errors": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"depd": "~2.0.0",
"inherits": "~2.0.4",
"setprototypeof": "~1.2.0",
"statuses": "~2.0.2",
"toidentifier": "~1.0.1"
},
"engines": {
"node": ">= 0.8"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/iconv-lite": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
"integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
"dev": true,
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true,
"license": "ISC"
},
"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==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/is-extendable": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
"integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-promise": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
"dev": true,
"license": "MIT"
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true,
"license": "ISC"
},
"node_modules/jose": {
"version": "6.1.3",
"resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz",
"integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/panva"
}
},
"node_modules/js-yaml": {
"version": "3.14.2",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
"dev": true,
"license": "MIT",
"dependencies": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"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==",
"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==",
"dev": true,
"license": "BSD-2-Clause"
},
"node_modules/kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/lru-cache": {
"version": "11.2.5",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz",
"integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==",
"dev": true,
"license": "BlueOak-1.0.0",
"engines": {
"node": "20 || >=22"
}
},
"node_modules/lunr": {
"version": "2.3.9",
"resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz",
"integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==",
"dev": true,
"license": "MIT"
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/media-typer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
"integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
"dev": true,
"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==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/mime-db": {
"version": "1.54.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
"integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"mime-db": "^1.54.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/minimatch": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz",
"integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/brace-expansion": "^5.0.0"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/minipass": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT"
},
"node_modules/negotiator": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"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==",
"dev": true,
"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==",
"dev": true,
"license": "ISC",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/path-scurry": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz",
"integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"lru-cache": "^11.0.0",
"minipass": "^7.1.2"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/path-to-regexp": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
"integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
"dev": true,
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"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==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
"dev": true,
"license": "MIT",
"dependencies": {
"forwarded": "0.2.0",
"ipaddr.js": "1.9.1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/qs": {
"version": "6.14.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
"integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.1.0"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"dev": true,
"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==",
"dev": true,
"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/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/router": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
"integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"debug": "^4.4.0",
"depd": "^2.0.0",
"is-promise": "^4.0.0",
"parseurl": "^1.3.3",
"path-to-regexp": "^8.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true,
"license": "MIT"
},
"node_modules/section-matter": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz",
"integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==",
"dev": true,
"license": "MIT",
"dependencies": {
"extend-shallow": "^2.0.1",
"kind-of": "^6.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/send": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
"integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"debug": "^4.4.3",
"encodeurl": "^2.0.0",
"escape-html": "^1.0.3",
"etag": "^1.8.1",
"fresh": "^2.0.0",
"http-errors": "^2.0.1",
"mime-types": "^3.0.2",
"ms": "^2.1.3",
"on-finished": "^2.4.1",
"range-parser": "^1.2.1",
"statuses": "^2.0.2"
},
"engines": {
"node": ">= 18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/serve-static": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
"integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
"dev": true,
"license": "MIT",
"dependencies": {
"encodeurl": "^2.0.0",
"escape-html": "^1.0.3",
"parseurl": "^1.3.3",
"send": "^1.2.0"
},
"engines": {
"node": ">= 18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"dev": true,
"license": "ISC"
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/side-channel": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3",
"side-channel-list": "^1.0.0",
"side-channel-map": "^1.0.1",
"side-channel-weakmap": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-list": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-map": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-weakmap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3",
"side-channel-map": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/strip-bom-string": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz",
"integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.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==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.6"
}
},
"node_modules/type-is": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
"integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
"dev": true,
"license": "MIT",
"dependencies": {
"content-type": "^1.0.5",
"media-typer": "^1.1.0",
"mime-types": "^3.0.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"dev": true,
"license": "MIT"
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"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==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "bin/node-which"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true,
"license": "ISC"
},
"node_modules/zod": {
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/zod-to-json-schema": {
"version": "3.25.1",
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz",
"integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==",
"dev": true,
"license": "ISC",
"peerDependencies": {
"zod": "^3.25 || ^4"
}
}
}
}
{
"name": "awing-rules-claudecode-mcp",
"version": "1.0.0",
"description": "",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"prestart": "npm run build",
"start": "node dist/index.js",
"test": "node dist/tests/runner.js"
},
"repository": {
"type": "git",
"url": "https://gitlab.awing.vn/awing-mcp/awing-rules-claudecode-mcp.git"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@modelcontextprotocol/sdk": "^1.25.3",
"@types/glob": "^8.1.0",
"@types/lunr": "^2.3.7",
"@types/minimatch": "^5.1.2",
"@types/node": "^25.1.0",
"glob": "^13.0.0",
"gray-matter": "^4.0.3",
"lunr": "^2.3.9",
"minimatch": "^10.1.1",
"typescript": "^5.9.3",
"zod": "^4.3.6"
}
}
\ No newline at end of file
# Awing Rules MCP Server for Claude Code
A Model Context Protocol (MCP) server designed to help Claude Code dynamically find, filter, and compose project-specific rules from a large repository of Markdown files.
## Features
- **Smart Search**: Finds relevant rules using a weighted scoring algorithm:
- **Text Match** (TF-IDF/BM25 approximation)
- **Context Aware** (Prioritizes rules matching currently open or changed files)
- **Tag & Priority System** (Weights tags and rule priority)
- **Rule Composition**: Dynamically bundles a global `base.md` with selected rules into a single, deduplicated Markdown prompt.
- **Deduplication**: Automatically removes duplicate generic bullet points to keep the context window efficient.
- **Diversification**: Ensures results are not dominated by a single category/folder.
## Installation
### Prerequisites
- Node.js v16+ (v18+ recommended)
- NPM
### Build
```bash
git clone <repo-url> awing-rules-claudecode-mcp
cd awing-rules-claudecode-mcp
npm install
npm run build
```
## Configuration
To use this with Claude Code or Claude Desktop, add it to your MCP configuration file (e.g., `claude_desktop_config.json` or equivalent for Claude Code).
```json
{
"mcpServers": {
"awing-rules": {
"command": "node",
"args": [
"d:/Project/Awing/awing-rules-claudecode-mcp/dist/index.js"
]
}
}
}
```
*Note: Replace the path with the absolute path to your `dist/index.js` file.*
## Usage with Claude Code
Once configured, Claude can use the following tools:
### 1. `rules.search`
Searches for rules relevant to the current task.
- **Inputs**: `query` (task description), `openFiles`, `changedFiles`, `tags`.
- **Outputs**: List of ranked rules with their IDs and scores.
### 2. `rules.compose`
Creates the final rule bundle.
- **Inputs**: `selected` (list of rule IDs/paths obtained from search).
- **Outputs**: A single markdown string containing the `base.md` content followed by the selected rules, optimized for the context window.
### Example Workflow
1. User asks: "Refactor the login component to use the new hook."
2. Claude calls `rules.search(query="refactor login hook", openFiles=["src/Login.tsx"], ...)`
3. Server returns top relevant rules (e.g., `frontend/react-hooks.md`, `style/typescript.md`).
4. Claude calls `rules.compose(selected=[{id: "react-hooks"}, {id: "typescript"}])`.
5. Server returns the combined markdown bundle.
6. Claude reads the bundle and executes the refactoring task following the rules.
## Rule File Structure
Place your markdown rule files in the root directory or subdirectories.
`base.md` in the root key is always included first.
**Example `frontend/react-components.md`:**
```markdown
---
id: "react-components"
title: "React Component Guidelines"
tags: ["frontend", "react", "tsx"]
priority: 80
paths: ["**/*.tsx"]
---
# React Component Rules
- Use functional components.
- Avoid default exports.
```
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ErrorCode,
ListToolsRequestSchema,
McpError,
} from '@modelcontextprotocol/sdk/types.js';
import { z } from 'zod';
import { RuleIndexer } from './rules/indexer.js';
import { Composer } from './rules/composer.js';
import * as path from 'path';
// --- Configuration ---
const ROOT_DIR = process.cwd(); // Assume running from project root
// --- Initialization ---
const indexer = new RuleIndexer(ROOT_DIR);
const composer = new Composer(ROOT_DIR);
// Init index on startup (async but we assume fast enough or lazy)
indexer.init().catch(err => console.error('Failed to init index:', err));
const server = new Server(
{
name: 'awing-rules-claudecode-mcp',
version: '0.1.0',
},
{
capabilities: {
tools: {},
},
}
);
// --- Tool Definitions ---
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'rules_search',
description: 'Search for relevant project rules based on query and context.',
inputSchema: {
type: 'object',
properties: {
query: { type: 'string', description: 'The search query or user task description' },
openFiles: { type: 'array', items: { type: 'string' }, description: 'List of currently open file paths' },
changedFiles: { type: 'array', items: { type: 'string' }, description: 'List of recently changed file paths' },
tags: { type: 'array', items: { type: 'string' }, description: 'Filter by tags' },
limit: { type: 'number', description: 'Max number of results to return (default 6)' },
minScore: { type: 'number', description: 'Minimum score threshold (default 0.15)' }
},
required: ['query'],
},
},
{
name: 'rules_get',
description: 'Get details of a specific rule by ID or path.',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string' },
path: { type: 'string' },
mode: { type: 'string', enum: ['full', 'snippet', 'sections'], default: 'snippet' }
},
},
},
{
name: 'rules_compose',
description: 'Compose a rule bundle from selected rules and the base rule.',
inputSchema: {
type: 'object',
properties: {
selected: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'string' },
path: { type: 'string' }
}
}
},
mode: { type: 'string', enum: ['full', 'snippet'], default: 'snippet' },
dedupe: { type: 'boolean', default: true },
maxChars: { type: 'number', default: 12000 }
},
required: ['selected'],
},
},
{
name: 'rules_refresh',
description: 'Refresh the rule index from disk.',
inputSchema: {
type: 'object',
properties: {},
},
},
],
};
});
// --- Tool Handlers ---
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
if (name === 'rules_refresh') {
await indexer.refresh();
return {
content: [{ type: 'text', text: 'Rule index refreshed successfully.' }]
};
}
if (name === 'rules_search') {
const input = z.object({
query: z.string(),
openFiles: z.array(z.string()).optional(),
changedFiles: z.array(z.string()).optional(),
tags: z.array(z.string()).optional(),
limit: z.number().optional(),
minScore: z.number().optional()
}).parse(args);
const results = indexer.search(input.query, {
openFiles: input.openFiles,
changedFiles: input.changedFiles,
tags: input.tags,
limit: input.limit,
minScore: input.minScore
});
return {
content: [
{
type: 'text',
text: JSON.stringify(results.map(r => ({
id: r.rule.id,
path: r.rule.relativePath,
title: r.rule.title,
score: r.score.toFixed(2),
tags: r.rule.tags,
why: `Text:${r.scoreBreakdown.text.toFixed(2)} Path:${r.scoreBreakdown.path.toFixed(2)} Tag:${r.scoreBreakdown.tag.toFixed(2)}`
})), null, 2),
},
],
};
}
if (name === 'rules_get') {
const input = z.object({
id: z.string().optional(),
path: z.string().optional(),
mode: z.string().optional(),
}).parse(args);
const rule = indexer.getRuleByIdOrPath(input.id || input.path || '');
if (!rule) {
return { isError: true, content: [{ type: 'text', text: 'Rule not found' }] };
}
return {
content: [
{
type: 'text',
text: JSON.stringify({
id: rule.id,
path: rule.relativePath,
title: rule.title,
content: rule.content // TODO: Apply mode/snippet logic if needed
}, null, 2),
},
],
};
}
if (name === 'rules_compose') {
const input = z.object({
selected: z.array(z.object({ id: z.string().optional(), path: z.string().optional() })),
mode: z.enum(['full', 'snippet']).optional(),
dedupe: z.boolean().optional(),
maxChars: z.number().optional()
}).parse(args);
// Resolve rules
const resolvedRules = input.selected.map(sel => {
const rule = indexer.getRuleByIdOrPath(sel.id || sel.path || '');
return { ...sel, rule };
});
const bundle = await composer.compose(resolvedRules, {
mode: input.mode,
dedupe: input.dedupe,
maxChars: input.maxChars
});
return {
content: [
{
type: 'text',
text: bundle.content,
},
],
};
}
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
});
async function runServer() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('Moving Rules MCP Server running on stdio');
}
runServer().catch((error) => {
console.error('Fatal error running server:', error);
process.exit(1);
});
import { Rule, RuleBundle } from '../types.js';
import { RuleLoader } from './loader.js';
export class Composer {
private loader: RuleLoader;
constructor(baseDir: string) {
this.loader = new RuleLoader(baseDir);
}
async compose(
selectedRules: { id?: string; path?: string; rule?: Rule }[],
options: {
maxChars?: number;
dedupe?: boolean;
mode?: 'full' | 'snippet';
basePath?: string;
} = {}
): Promise<RuleBundle> {
const { maxChars = 12000, dedupe = true, mode = 'snippet' } = options;
// 1. Load Base Rule
const baseContent = await this.loader.loadBaseRule();
// 2. Resolve Selected Rules
// The indexer might have passed the full Rule object or just ID/Path.
// Ideally, the caller (MCP tool) resolves the rules first using Indexer, or passes them in.
// For simplicity, let's assume 'selectedRules' contains the Rule object if passed from search,
// or we might need to look them up. But Composer doesn't have the Indexer reference.
// Design choice: MCP server does the lookup. Composer just formats.
// So we expect `rule` property to be present or we can't extract info.
// Actually, let's make the input strictly `Rule[]`.
// Refactoring spec: input `selected` is [{id?, path?}].
// Composer needs access to Indexer or Loader to fetch content if not provided?
// Let's assume the MCP layer fetches the rules.
// "Input: selected: [{ id?: string, path?: string }]".
// WE need to fetch them. So Composer should take an Indexer or Loader.
// But Loader is expensive to re-scan. Indexer has cache.
// Let's pass the *resolved* rules to the compose function to keep it pure(r).
// Or simpler: Composer owns the formatting string logic, MCP server orchestration handles lookup.
// Let's write this class to accept *resolved* array of Rules.
// If the tool definition says input is just ids, the MCP handler will use Indexer to get the Rules, then pass them here.
const rulesToInclude = selectedRules.map(s => s.rule).filter((r): r is Rule => !!r);
// 3. Start composing
let bundle = `# RULE BUNDLE\n> [!IMPORTANT] Priority Order: Selected rules override Base rules where conflicting. Security constraints in Base rules are non-negotiable.\n\n`;
// Add Base
bundle += `## BASE RULES\n\n${baseContent}\n\n`;
// Add Selected
const used: { id: string; path: string; title: string }[] = [];
const notes: string[] = [];
for (const rule of rulesToInclude) {
bundle += `## ${rule.title} (ID: ${rule.id})\nTitle: ${rule.title}\nSource: ${rule.relativePath}\n\n`;
let contentToAdd = rule.content;
// Snippet mode logic
if (mode === 'snippet') {
contentToAdd = this.extractSnippet(contentToAdd);
}
bundle += contentToAdd + `\n\n---\n\n`;
used.push({ id: rule.id, path: rule.path, title: rule.title });
}
// 4. Deduplication
if (dedupe) {
bundle = this.deduplicateLines(bundle);
}
// 5. Truncation
let truncated = false;
if (bundle.length > maxChars) {
bundle = bundle.slice(0, maxChars) + '\n... (truncated)';
truncated = true;
notes.push(`Output truncated to ${maxChars} chars.`);
}
return {
content: bundle,
used,
truncated,
notes
};
}
private extractSnippet(content: string): string {
// Keep headings, bullets, blockquotes
const lines = content.split('\n');
const keep: string[] = [];
let insideCodeBlock = false;
for (const line of lines) {
if (line.trim().startsWith('```')) {
insideCodeBlock = !insideCodeBlock;
// In snippet mode, maybe omit long code blocks?
// Let's keep them if they are short?
// For now, keep everything but maybe limit length?
keep.push(line);
continue;
}
const isHeader = /^#+\s/.test(line);
const isList = /^[\s]*[-*+]\s/.test(line);
const isQuote = /^>\s/.test(line); // e.g. alerts
if (isHeader || isList || isQuote || insideCodeBlock) {
keep.push(line);
} else {
// Plain text? Skip in harsh snippet mode?
// Let's keep it if it's short, or skip?
// Prompt says: "Prioritize headings + bullet lines + Do/Don't"
// Let's conservatively keep non-empty logic lines?
// Actually, just skipping paragraphs might be intended.
if (line.trim().length > 0) {
// keep.push(line); // aggressive: keep all?
// "Cắt examples dài"
}
}
}
// Simple pass: return full content for now, but assume text is mostly bullets in rules.
// Better implementation: Regex for relevant sections.
return content; // Placeholder for advanced snippet extraction if needed.
}
private deduplicateLines(text: string): string {
const lines = text.split('\n');
const seen = new Set<string>();
const out: string[] = [];
for (const line of lines) {
const trimmed = line.trim().toLowerCase();
// Only dedupe substantial bullet points
const isBullet = /^[\s]*[-*+]\s/.test(line);
if (isBullet && trimmed.length > 10) {
if (seen.has(trimmed)) {
continue; // Skip duplicate
}
seen.add(trimmed);
}
out.push(line);
}
return out.join('\n');
}
}
import { RuleLoader } from './loader.js';
import { Scorer } from './scorer.js';
import { Rule, SearchResult } from '../types.js';
export class RuleIndexer {
private loader: RuleLoader;
private scorer: Scorer;
private cachedRules: Rule[] = [];
private lastIndexed: number = 0;
constructor(baseDir: string) {
this.loader = new RuleLoader(baseDir);
this.scorer = new Scorer();
}
async init() {
await this.refresh();
}
async refresh() {
this.cachedRules = await this.loader.loadAllRules();
this.scorer.indexRules(this.cachedRules);
this.lastIndexed = Date.now();
}
async getBaseRule(): Promise<string> {
return this.loader.loadBaseRule();
}
getRuleByIdOrPath(idOrPath: string): Rule | undefined {
// Try ID match
let rule = this.cachedRules.find(r => r.id === idOrPath);
if (rule) return rule;
// Try Path match (exact or relative)
// normalizing slashes
const normalized = idOrPath.replace(/\\/g, '/');
return this.cachedRules.find(r => r.path.replace(/\\/g, '/').endsWith(normalized));
}
search(
query: string,
options: {
openFiles?: string[];
changedFiles?: string[];
tags?: string[];
limit?: number;
minScore?: number;
} = {}
): SearchResult[] {
const { openFiles = [], changedFiles = [], tags = [], limit = 6, minScore = 0.15 } = options;
const results = this.cachedRules.map(rule =>
this.scorer.scoreRule(rule, query, openFiles, changedFiles, tags)
);
// Sort by score desc
const sorted = results
.filter(r => r.score >= minScore)
.sort((a, b) => b.score - a.score);
// Diversification logic: max 2 rules per top-level folder
const diversified: SearchResult[] = [];
const categoryCounts: { [cat: string]: number } = {};
for (const res of sorted) {
// Determine category from relative path (first dir)
const parts = res.rule.relativePath.split(/[/\\]/);
const category = parts.length > 1 ? parts[0] : 'root';
if ((categoryCounts[category] || 0) < 2) {
diversified.push(res);
categoryCounts[category] = (categoryCounts[category] || 0) + 1;
}
if (diversified.length >= limit) break;
}
return diversified;
}
}
import * as fs from 'fs/promises';
import * as path from 'path';
import matter from 'gray-matter';
import { Rule, RuleFrontmatter } from '../types.js';
import { findMarkdownFiles } from '../utils/glob.js';
export class RuleLoader {
private rootDir: string;
constructor(rootDir: string) {
this.rootDir = path.resolve(rootDir);
}
async loadAllRules(): Promise<Rule[]> {
const files = await findMarkdownFiles(this.rootDir);
const rules: Rule[] = [];
for (const file of files) {
if (path.basename(file).toLowerCase() === 'base.md') continue;
try {
const rule = await this.parseRule(file);
if (rule) rules.push(rule);
} catch (error) {
console.error(`Failed to parse rule file: ${file}`, error);
}
}
return rules;
}
async loadBaseRule(): Promise<string> {
const basePath = path.join(this.rootDir, 'base.md');
try {
const content = await fs.readFile(basePath, 'utf-8');
return content;
} catch (error) {
// If base.md doesn't exist, return empty string or default warning
return '';
}
}
private async parseRule(filePath: string): Promise<Rule | null> {
const rawContent = await fs.readFile(filePath, 'utf-8');
const { data, content } = matter(rawContent);
const fm = data as RuleFrontmatter;
const stats = await fs.stat(filePath);
// Filter out rules explicitly marked to be avoided if necessary?
// No, 'avoid' is a string[] field for query matching, not a flag to ignore the file itself basically.
// Unless frontmatter is drastically invalid.
const relativePath = path.relative(this.rootDir, filePath);
const id = fm.id || relativePath.replace(/\\/g, '/').replace(/\.md$/, '');
const title = fm.title || path.basename(filePath, '.md');
return {
id,
path: filePath,
relativePath,
title,
content,
tags: fm.tags || [],
priority: typeof fm.priority === 'number' ? fm.priority : 50,
paths: fm.paths || [],
applies_when: fm.applies_when || [],
avoid: fm.avoid || [],
lastModified: stats.mtimeMs
};
}
}
import { Rule, SearchResult } from '../types.js';
import { matchGlob } from '../utils/glob.js';
interface TFIDFIndex {
[term: string]: number; // IDF scores
}
export class Scorer {
private idf: { [term: string]: number } = {};
private ruleVectors: Map<string, { [term: string]: number }> = new Map();
constructor() { }
// Update internal TF-IDF model based on all rules
// Simplified version: Term Frequency in document * Inverse Document Frequency
// For small corpus (50-200 files), in-memory is fine.
public indexRules(rules: Rule[]) {
// 1. Calculate document frequencies
const docFreq: { [term: string]: number } = {};
const totalDocs = rules.length;
rules.forEach(rule => {
const terms = this.tokenize(this.getRuleTextForIndex(rule));
const uniqueTerms = new Set(terms);
uniqueTerms.forEach(term => {
docFreq[term] = (docFreq[term] || 0) + 1;
});
});
// 2. Calculate IDF
this.idf = {};
Object.keys(docFreq).forEach(term => {
this.idf[term] = Math.log(1 + (totalDocs / (docFreq[term] || 1)));
});
// 3. Pre-calculate TF vectors for each rule
this.ruleVectors.clear();
rules.forEach(rule => {
const terms = this.tokenize(this.getRuleTextForIndex(rule));
const tf: { [term: string]: number } = {};
const docLen = terms.length;
terms.forEach(term => {
tf[term] = (tf[term] || 0) + 1;
});
// Normalize TF? Or just use raw count? Let's use simple TF (count/len) * IDF
const vec: { [term: string]: number } = {};
Object.keys(tf).forEach(term => {
vec[term] = (tf[term] / docLen) * (this.idf[term] || 0);
});
this.ruleVectors.set(rule.id, vec);
});
}
private getRuleTextForIndex(rule: Rule): string {
return `${rule.title} ${rule.tags.join(' ')} ${rule.content}`;
}
private tokenize(text: string): string[] {
return text.toLowerCase()
.replace(/[^a-z0-9\s]/g, '')
.split(/\s+/)
.filter(t => t.length > 2);
}
// --- Scoring Components ---
private calculateTextScore(query: string, ruleId: string): number {
const queryTerms = this.tokenize(query);
const ruleVec = this.ruleVectors.get(ruleId);
if (!ruleVec || queryTerms.length === 0) return 0;
let score = 0;
queryTerms.forEach(term => {
if (ruleVec[term]) {
score += ruleVec[term];
}
});
// Normalize score somewhat?
// TF-IDF summing can go > 1. Let's clamp or sigmoid it?
// Or just simple normalization if creating a relative ranking.
// For now, let's assume raw score is okay but maybe cap at 1.0 for the weighted sum formula
// because S_text is expected to be 0..1 in the prompt.
// A simple heuristic normalization: divide by max theoretical score or just 10?
// Let's use a simpler overlap metric for S_text if TF-IDF is too unbounded.
// Actually, BM25 returns unbounded scores usually.
// Let's check overlap of terms?
// "S_text: ... normalize 0..1"
// Let's try cosine similarity between query and doc?
// Query vector: tf=1 for all terms.
// Simple Jaccard/Overlap for robust 0-1?
// Let's do a localized TF-IDF cosine approx.
let magnitudeQuery = Math.sqrt(queryTerms.length); // approx
let magnitudeDoc = 0;
Object.values(ruleVec).forEach(v => magnitudeDoc += v * v);
magnitudeDoc = Math.sqrt(magnitudeDoc);
if (magnitudeDoc === 0 || magnitudeQuery === 0) return 0;
// Rescale score to 0-1 range roughly
// Cosine similarity = dot_product / (magA * magB)
return Math.min(1, score / (magnitudeQuery * magnitudeDoc * 5 + 0.1)); // Fudge factor
}
private calculatePathScore(rule: Rule, openFiles: string[], changedFiles: string[]): number {
if (!rule.paths || rule.paths.length === 0) return 0;
const openMatches = openFiles.some(f => matchGlob(f, rule.paths));
const changedMatches = changedFiles.some(f => matchGlob(f, rule.paths));
if (changedMatches) return 1.0;
if (openMatches) return 0.6;
return 0;
}
private calculateTagScore(rule: Rule, queryTags: string[]): number {
if (!queryTags || queryTags.length === 0) return 0;
const intersection = rule.tags.filter(t => queryTags.includes(t));
return intersection.length > 0 ? Math.min(1, intersection.length / queryTags.length) : 0;
}
private calculatePriorityScore(rule: Rule): number {
return (rule.priority || 50) / 100;
}
// --- Main Score Function ---
public scoreRule(
rule: Rule,
query: string,
openFiles: string[] = [],
changedFiles: string[] = [],
queryTags: string[] = []
): SearchResult {
const sText = this.calculateTextScore(query, rule.id);
const sPath = this.calculatePathScore(rule, openFiles, changedFiles);
const sTag = this.calculateTagScore(rule, queryTags);
const sPriority = this.calculatePriorityScore(rule);
// avoid penalty
let penalty = 0;
if (rule.avoid && rule.avoid.length > 0) {
// If query or files match avoid criteria.
// Simple text match of generic terms in avoid list against query?
// Or if file path matches avoid glob?
// Prompt: "Penalty if query/file match with avoid"
// Let's assume avoid contains keywords or globs.
const avoidMatchesQuery = rule.avoid.some(avoidTerm => query.toLowerCase().includes(avoidTerm.toLowerCase()));
if (avoidMatchesQuery) penalty = 0.5;
}
// Boost heuristics
let boost = 0;
const q = query.toLowerCase();
if (/test|vitest|msw|coverage/.test(q) && rule.tags.includes('testing')) boost += 0.2;
if (/react|component|hook|tsx/.test(q) && (rule.relativePath.includes('frontend') || rule.tags.includes('react'))) boost += 0.2;
if (/graphql|mutation|schema/.test(q) && (rule.relativePath.includes('backend') || rule.tags.includes('graphql'))) boost += 0.2;
const weightedScore =
(0.55 * sText) +
(0.25 * sPath) +
(0.12 * sTag) +
(0.08 * sPriority);
const finalScore = Math.max(0, weightedScore + boost - penalty);
return {
rule,
score: finalScore,
scoreBreakdown: {
text: sText,
path: sPath,
tag: sTag,
priority: sPriority
}
};
}
}
import { Scorer } from '../rules/scorer.js';
import { Composer } from '../rules/composer.js';
import { RuleIndexer } from '../rules/indexer.js';
import { Rule } from '../types.js';
import * as assert from 'assert';
// Simple test helper
function test(name: string, fn: () => void) {
try {
fn();
console.log(`✅ ${name}`);
} catch (e) {
console.error(`❌ ${name}`);
console.error(e);
}
}
const mockRule: Rule = {
id: 'test-rule',
path: '/abs/test/frontend/rule.md',
relativePath: 'frontend/rule.md',
title: 'Test Rule',
content: 'Rule content',
tags: ['react', 'testing'],
priority: 80,
paths: ['**/*.tsx'],
applies_when: [],
avoid: [],
lastModified: 0
};
async function runTests() {
console.log('--- Running Scorer Tests ---');
// Scorer Path Tests
const scorer = new Scorer();
// We need to index rules first to initialize vectors if we test text score,
// but path score is independent.
scorer.indexRules([mockRule]);
test('Path Score: Exact Match', () => {
const score = (scorer as any).calculatePathScore(mockRule, ['/src/app.tsx'], ['/src/component.tsx']);
// Changed file matches glob **/*.tsx
assert.ok(score >= 1.0, 'Should be 1.0 for changed file match');
});
test('Path Score: Open Match', () => {
const score = (scorer as any).calculatePathScore(mockRule, ['/src/app.tsx'], []);
assert.ok(score >= 0.6, 'Should be 0.6 for open file match');
});
test('Path Score: No Match', () => {
const score = (scorer as any).calculatePathScore(mockRule, ['/src/main.py'], []);
assert.strictEqual(score, 0, 'Should be 0 for no match');
});
console.log('--- Running Composer Tests ---');
const composer = new Composer('.');
test('Deduplication', () => {
const text = `
- Keep this
- Dedupe this
- Dedupe this
- Keep this too
`;
const result = (composer as any).deduplicateLines(text);
const lines = result.split('\n').map((l: string) => l.trim()).filter((l: string) => l);
assert.strictEqual(lines.length, 3, 'Should remove 1 duplicate line');
});
console.log('--- Real File Indexing Test ---');
const indexer = new RuleIndexer(process.cwd());
await indexer.init(); // Load real files
const results = indexer.search('react component', {
openFiles: ['src/App.tsx']
});
console.log(`Found ${results.length} rules for "react component"`);
results.forEach(r => console.log(`- ${r.rule.title} (Score: ${r.score.toFixed(2)})`));
// Check if we found the new rules
const foundReact = results.some(r => r.rule.id === 'frontend-react' || r.rule.title.includes('React'));
assert.ok(foundReact, 'Should find React rules in sample files');
const baseContent = await indexer.getBaseRule();
assert.ok(baseContent.length > 0, 'Base rule should be loaded');
console.log('--- All Tests Finished ---');
}
runTests().catch(console.error);
export interface RuleFrontmatter {
id?: string;
title?: string;
tags?: string[];
priority?: number;
paths?: string[];
applies_when?: string[];
avoid?: string[];
}
export interface Rule {
id: string;
path: string;
relativePath: string;
title: string;
content: string;
tags: string[];
priority: number;
paths: string[]; // glob patterns
applies_when: string[];
avoid: string[];
lastModified: number;
}
export interface SearchResult {
rule: Rule;
score: number;
scoreBreakdown: {
text: number;
path: number;
tag: number;
priority: number;
};
}
export interface RuleBundle {
content: string;
used: { id: string; path: string; title: string }[];
truncated: boolean;
notes: string[];
}
import { minimatch } from 'minimatch';
import { glob } from 'glob';
export const findMarkdownFiles = async (cwd: string): Promise<string[]> => {
// Find all markdown files, ignoring node_modules and dot folders
const files = await glob('**/*.md', {
cwd,
ignore: ['**/node_modules/**', '**/.*/**'],
absolute: true,
});
return files;
};
export const matchGlob = (filePath: string, patterns: string[]): boolean => {
if (!patterns || patterns.length === 0) return false;
// Normalize windows paths for matching
const normalizedPath = filePath.replace(/\\/g, '/');
for (const pattern of patterns) {
if (minimatch(normalizedPath, pattern, { dot: true, matchBase: true })) return true;
}
return false;
};
---
id: "style-typescript"
title: "TypeScript Style Guide"
tags: ["typescript", "style", "lint"]
priority: 80
paths: ["**/*.ts", "**/*.tsx"]
---
# TypeScript Style Guide
## Typing
- **Explicit Types**: Avoid `any`. Define interfaces or types for all data structures.
- **Strict Mode**: `tsconfig.json` must have `"strict": true`.
- **Inference**: Rely on type inference for simple variable assignments.
## Naming Conventions
- **Variables/Functions**: camelCase
- **Classes/Interfaces/Types**: PascalCase
- **Constants**: UPPER_CASE for global constants, camelCase for local readonly variables.
## Async/Await
- Prefer `async/await` over raw `.then()` chains.
- Always handle errors in async functions with `try/catch` or return rejection.
---
id: "testing-jest"
title: "Testing Guidelines (Jest/Vitest)"
tags: ["testing", "jest", "vitest", "unit-test"]
priority: 70
paths: ["**/*.test.ts", "**/*.spec.ts", "**/tests/**"]
applies_when: ["test", "verify"]
---
# Testing Guidelines
## Unit Tests
- Write unit tests for all utility functions and shared logic.
- Mock external dependencies (API calls, DB) to keep tests fast and deterministic.
## Test Structure
- Use `describe` to group tests by function or module.
- Use `it` or `test` for individual test cases. Descriptions should be readable sentences.
```typescript
describe('calculateTotal', () => {
it('should return 0 for empty cart', () => {
expect(calculateTotal([])).toBe(0);
});
});
```
## Coverage
- Aim for high branch coverage on critical business logic.
- Do not test implementation details, test public behavior.
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"**/*.test.ts"
]
}
\ No newline at end of file
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment