update(script): dev and dist script

This commit is contained in:
Kyush 2026-03-11 03:49:11 +09:00
commit ebe980c6ff
5 changed files with 109 additions and 187 deletions

127
README.md
View file

@ -1,14 +1,18 @@
# Kyush LLM Router
Multi-user LLM routing proxy with API key management and usage monitoring.
Multi-user LLM routing proxy with API key management, usage monitoring, and script-based request/response manipulation.
## Features
- **Multi-user support**: Manage multiple users with individual API keys
- **Backend routing**: Route requests to multiple OpenAI-compatible backends (vLLM, SGLang, etc.)
- **Backend routing**: Route requests to multiple OpenAI-compatible backends (vLLM, SGLang, etc.) with load balancing
- **Permission management**: Control which users can access which backends
- **Usage monitoring**: Track request logs, token usage, and backend metrics
- **Admin dashboard**: Web UI for managing users, backends, and permissions
- **Script engine**: Execute JavaScript code in secure isolates to manipulate requests/responses
- `onRequest`: Modify requests before forwarding to backend
- `onResponse`: Modify responses before returning to client
- Script types: `per-user`, `per-backend`, `per-user-backend`
- **Admin dashboard**: Web UI for managing users, backends, permissions, and scripts
## Architecture
@ -17,6 +21,7 @@ Multi-user LLM routing proxy with API key management and usage monitoring.
│ LLM Clients │────▶│ Router Server (Port 3000) │
│ (OpenAI SDK) │ │ - Authentication │
└─────────────────┘ │ - Request Routing │
│ - Script Execution (isolated-vm) │
│ - Admin API │
└─────────────────┬────────────────────┘
@ -27,6 +32,7 @@ Multi-user LLM routing proxy with API key management and usage monitoring.
│ - users │ │ - request_logs │
│ - backends │ │ - usage_stats │
│ - permissions │ │ - metrics │
│ - user_scripts │ │ │
└─────────────────┘ └─────────────────┘
```
@ -36,12 +42,12 @@ Multi-user LLM routing proxy with API key management and usage monitoring.
1. Install dependencies:
```bash
npm install
pnpm install
```
2. Start development servers:
```bash
npm run dev
pnpm run dev
```
This starts:
@ -65,8 +71,20 @@ ADMIN_PASSWORD=your_secure_password docker-compose up -d
```
kyush-llm-router/
├── server/ # Express backend
│ ├── src/
│ │ ├── config/ # Database configuration
│ │ ├── models/ # Data models (User, Backend, Permission, Script)
│ │ ├── routes/ # API routes (admin, api, scripts, analytics)
│ │ ├── services/ # RouterService, ScriptEngine, AnalyticsService
│ │ └── utils/ # Utilities (apiKey, logger)
│ ├── tests/ # Integration tests
│ └── benchmarks/ # Performance benchmarks
├── client/ # Solid.js admin dashboard
├── database/ # SQL schema files
│ └── src/
│ ├── api/ # API client
│ ├── components/ # UI components
│ └── types/ # TypeScript types
├── shared/ # Shared TypeScript types
├── scripts/ # Development scripts
└── docs/ # Documentation
```
@ -76,34 +94,50 @@ kyush-llm-router/
### OpenAI-Compatible API (Port 3000)
```
POST /v1/chat/completions
POST /v1/completions
GET /v1/models
POST /v1/chat/completions # Chat completions with routing & scripting
GET /v1/models # List available models
```
### Admin API (Port 3001)
```
# User Management
POST /admin/users
GET /admin/users
PUT /admin/users/:id
DELETE /admin/users/:id
POST /admin/users # Create user
GET /admin/users # List all users
GET /admin/users/:id # Get user by ID
PUT /admin/users/:id # Update user
DELETE /admin/users/:id # Delete user
POST /admin/users/:id/regenerate-api-key # Regenerate API key
# Backend Management
POST /admin/backends
GET /admin/backends
PUT /admin/backends/:id
DELETE /admin/backends/:id
POST /admin/backends # Create backend
GET /admin/backends # List all backends
GET /admin/backends/:id # Get backend by ID
PUT /admin/backends/:id # Update backend
DELETE /admin/backends/:id # Delete backend
# Permission Management
POST /admin/permissions
DELETE /admin/permissions
GET /admin/permissions
POST /admin/permissions # Create permission
GET /admin/permissions # List all permissions
GET /admin/permissions/user/:userId # Get permissions by user
GET /admin/permissions/backend/:backendId # Get permissions by backend
DELETE /admin/permissions # Delete permission (query params: user_id, backend_id)
# Script Management
GET /admin/scripts # List all scripts
GET /admin/scripts/active # List active scripts
GET /admin/scripts/type/:type # List scripts by type
GET /admin/scripts/:id # Get script by ID
POST /admin/scripts # Create script
PUT /admin/scripts/:id # Update script
DELETE /admin/scripts/:id # Delete script
POST /admin/scripts/:id/activate # Activate script
POST /admin/scripts/:id/deactivate # Deactivate script
POST /admin/scripts/:id/test # Test script
# Analytics
GET /admin/analytics/usage
GET /admin/analytics/requests
GET /admin/analytics/usage # Get usage statistics
GET /admin/analytics/requests # Get request logs
```
## Environment Variables
@ -115,7 +149,54 @@ GET /admin/analytics/requests
| `CORE_DB_PATH` | Core database path | `./data/core.db` |
| `ANALYTICS_DB_PATH` | Analytics database path | `./data/analytics.db` |
| `ADMIN_PASSWORD` | Admin dashboard password | Required |
| `CORS_ORIGINS` | Comma-separated allowed origins | `http://localhost:5173,http://localhost:3001` |
## Script Engine
The router supports JavaScript code execution in secure isolated VMs for request/response manipulation.
### Script Types
- **per-user**: Applied to all requests from a specific user
- **per-backend**: Applied to all requests going to a specific backend
- **per-user-backend**: Applied to requests from a specific user to a specific backend
### Script Context
Scripts have access to:
- `user`: User information (id, name, email)
- `backend`: Backend information (id, name, base_url)
- `request`: Request details (method, path, headers, body, isStream)
- `response`: Response details (status, headers, body, isStream)
### Example Script
```javascript
export async function onRequest(context) {
// Add custom headers to all requests
context.request.headers['X-Custom-Header'] = 'value';
return context;
}
export async function onResponse(context) {
// Log response status
console.log(`Response status: ${context.response.status}`);
return context;
}
```
## Testing
Run tests:
```bash
pnpm test
```
Run benchmarks:
```bash
pnpm run bench
```
## License
MIT
MIT

View file

@ -6,7 +6,7 @@
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"start": "vite preview",
"typecheck": "tsc --noEmit"
},
"keywords": [],

View file

@ -3,9 +3,9 @@
"version": "1.0.0",
"description": "LLM routing server with multi-user API key management",
"scripts": {
"dev": "concurrently \"pnpm -r --filter server dev\" \"pnpm -r --filter client dev\"",
"dev": "pnpm --parallel dev",
"build": "pnpm -r build",
"start": "pnpm -r start",
"start": "pnpm --parallel start",
"test": "pnpm -r --filter server test",
"bench": "pnpm -r --filter server run bench"
},
@ -23,9 +23,5 @@
],
"engines": {
"node": ">=18.0.0"
},
"devDependencies": {
"concurrently": "^9.2.1",
"tsx": "^4.21.0"
}
}

157
pnpm-lock.yaml generated
View file

@ -6,14 +6,7 @@ settings:
importers:
.:
devDependencies:
concurrently:
specifier: ^9.2.1
version: 9.2.1
tsx:
specifier: ^4.21.0
version: 4.21.0
.: {}
client:
dependencies:
@ -622,10 +615,6 @@ packages:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
ansi-styles@4.3.0:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
asap@2.0.6:
resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==}
@ -699,10 +688,6 @@ packages:
resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
engines: {node: '>=18'}
chalk@4.1.2:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'}
chalk@5.6.2:
resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==}
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
@ -714,17 +699,6 @@ packages:
resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==}
engines: {node: 10.* || >= 12.*}
cliui@8.0.1:
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
engines: {node: '>=12'}
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
@ -736,11 +710,6 @@ packages:
component-emitter@1.3.1:
resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==}
concurrently@9.2.1:
resolution: {integrity: sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==}
engines: {node: '>=18'}
hasBin: true
content-disposition@1.0.1:
resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==}
engines: {node: '>=18'}
@ -942,10 +911,6 @@ packages:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
engines: {node: '>=6.9.0'}
get-caller-file@2.0.5:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*}
get-intrinsic@1.3.0:
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
engines: {node: '>= 0.4'}
@ -964,10 +929,6 @@ packages:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'}
has-flag@4.0.0:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'}
has-symbols@1.1.0:
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
engines: {node: '>= 0.4'}
@ -1198,10 +1159,6 @@ packages:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'}
require-directory@2.1.1:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'}
resolve-pkg-maps@1.0.0:
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
@ -1214,9 +1171,6 @@ packages:
resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==}
engines: {node: '>= 18'}
rxjs@7.8.2:
resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==}
safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
@ -1253,10 +1207,6 @@ packages:
setprototypeof@1.2.0:
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
shell-quote@1.8.3:
resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==}
engines: {node: '>= 0.4'}
side-channel-list@1.0.0:
resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==}
engines: {node: '>= 0.4'}
@ -1337,14 +1287,6 @@ packages:
resolution: {integrity: sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==}
engines: {node: '>=14.18.0'}
supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
supports-color@8.1.1:
resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==}
engines: {node: '>=10'}
tar-fs@2.1.4:
resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==}
@ -1371,13 +1313,6 @@ packages:
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
engines: {node: '>=0.6'}
tree-kill@1.2.2:
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
hasBin: true
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
tsx@4.21.0:
resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==}
engines: {node: '>=18.0.0'}
@ -1516,28 +1451,12 @@ packages:
engines: {node: '>=8'}
hasBin: true
wrap-ansi@7.0.0:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
engines: {node: '>=10'}
wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
y18n@5.0.8:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'}
yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
yargs-parser@21.1.1:
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
engines: {node: '>=12'}
yargs@17.7.2:
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
engines: {node: '>=12'}
zod@4.3.6:
resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
@ -1986,10 +1905,6 @@ snapshots:
ansi-regex@5.0.1: {}
ansi-styles@4.3.0:
dependencies:
color-convert: 2.0.1
asap@2.0.6: {}
assertion-error@2.0.1: {}
@ -2074,11 +1989,6 @@ snapshots:
chai@6.2.2: {}
chalk@4.1.2:
dependencies:
ansi-styles: 4.3.0
supports-color: 7.2.0
chalk@5.6.2: {}
chownr@1.1.4: {}
@ -2089,18 +1999,6 @@ snapshots:
optionalDependencies:
'@colors/colors': 1.5.0
cliui@8.0.1:
dependencies:
string-width: 4.2.3
strip-ansi: 6.0.1
wrap-ansi: 7.0.0
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
color-name@1.1.4: {}
combined-stream@1.0.8:
dependencies:
delayed-stream: 1.0.0
@ -2109,15 +2007,6 @@ snapshots:
component-emitter@1.3.1: {}
concurrently@9.2.1:
dependencies:
chalk: 4.1.2
rxjs: 7.8.2
shell-quote: 1.8.3
supports-color: 8.1.1
tree-kill: 1.2.2
yargs: 17.7.2
content-disposition@1.0.1: {}
content-type@1.0.5: {}
@ -2330,8 +2219,6 @@ snapshots:
gensync@1.0.0-beta.2: {}
get-caller-file@2.0.5: {}
get-intrinsic@1.3.0:
dependencies:
call-bind-apply-helpers: 1.0.2
@ -2358,8 +2245,6 @@ snapshots:
gopd@1.2.0: {}
has-flag@4.0.0: {}
has-symbols@1.1.0: {}
has-tostringtag@1.0.2:
@ -2557,8 +2442,6 @@ snapshots:
string_decoder: 1.3.0
util-deprecate: 1.0.2
require-directory@2.1.1: {}
resolve-pkg-maps@1.0.0: {}
rollup@4.59.0:
@ -2602,10 +2485,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
rxjs@7.8.2:
dependencies:
tslib: 2.8.1
safe-buffer@5.2.1: {}
safer-buffer@2.1.2: {}
@ -2647,8 +2526,6 @@ snapshots:
setprototypeof@1.2.0: {}
shell-quote@1.8.3: {}
side-channel-list@1.0.0:
dependencies:
es-errors: 1.3.0
@ -2756,14 +2633,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
supports-color@7.2.0:
dependencies:
has-flag: 4.0.0
supports-color@8.1.1:
dependencies:
has-flag: 4.0.0
tar-fs@2.1.4:
dependencies:
chownr: 1.1.4
@ -2792,10 +2661,6 @@ snapshots:
toidentifier@1.0.1: {}
tree-kill@1.2.2: {}
tslib@2.8.1: {}
tsx@4.21.0:
dependencies:
esbuild: 0.27.3
@ -2903,28 +2768,8 @@ snapshots:
siginfo: 2.0.0
stackback: 0.0.2
wrap-ansi@7.0.0:
dependencies:
ansi-styles: 4.3.0
string-width: 4.2.3
strip-ansi: 6.0.1
wrappy@1.0.2: {}
y18n@5.0.8: {}
yallist@3.1.1: {}
yargs-parser@21.1.1: {}
yargs@17.7.2:
dependencies:
cliui: 8.0.1
escalade: 3.2.0
get-caller-file: 2.0.5
require-directory: 2.1.1
string-width: 4.2.3
y18n: 5.0.8
yargs-parser: 21.1.1
zod@4.3.6: {}

View file

@ -5,7 +5,7 @@
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"start": "node dist/server/src/index.js",
"dev": "tsx watch src",
"typecheck": "tsc --noEmit",
"test": "vitest run",