From fe432d4a09b6d34f4075443b8fdc08dc590231fd Mon Sep 17 00:00:00 2001 From: Danijel Date: Tue, 17 Mar 2026 23:45:42 +0100 Subject: [PATCH] feat: add SolidTime MCP server for time tracking integration Implements a Model Context Protocol server that exposes SolidTime's time tracking API as 22+ tools for use with Claude, Cursor, and other MCP-compatible clients. Supports stdio and HTTP transport modes, Docker deployment, and self-hosted SolidTime instances. Co-Authored-By: Claude Opus 4.6 (1M context) --- .dockerignore | 9 + .env.example | 20 + .gitignore | 5 + .prettierrc | 7 + Dockerfile | 24 + LICENSE | 21 + README.md | 180 ++ docker-compose.yml | 17 + eslint.config.js | 18 + package-lock.json | 3117 ++++++++++++++++++++++++++++++++++ package.json | 57 + src/api-client.ts | 138 ++ src/constants.ts | 58 + src/formatting.ts | 78 + src/http-server.ts | 182 ++ src/index.ts | 27 + src/schemas.ts | 20 + src/server.ts | 107 ++ src/tools/clients.ts | 124 ++ src/tools/invitations.ts | 112 ++ src/tools/members.ts | 150 ++ src/tools/organizations.ts | 80 + src/tools/project-members.ts | 147 ++ src/tools/projects.ts | 178 ++ src/tools/tags.ts | 112 ++ src/tools/tasks.ts | 144 ++ src/tools/time-entries.ts | 478 ++++++ src/tools/users.ts | 35 + src/types.ts | 117 ++ tsconfig.json | 18 + 30 files changed, 5780 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 .prettierrc create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 README.md create mode 100644 docker-compose.yml create mode 100644 eslint.config.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/api-client.ts create mode 100644 src/constants.ts create mode 100644 src/formatting.ts create mode 100644 src/http-server.ts create mode 100644 src/index.ts create mode 100644 src/schemas.ts create mode 100644 src/server.ts create mode 100644 src/tools/clients.ts create mode 100644 src/tools/invitations.ts create mode 100644 src/tools/members.ts create mode 100644 src/tools/organizations.ts create mode 100644 src/tools/project-members.ts create mode 100644 src/tools/projects.ts create mode 100644 src/tools/tags.ts create mode 100644 src/tools/tasks.ts create mode 100644 src/tools/time-entries.ts create mode 100644 src/tools/users.ts create mode 100644 src/types.ts create mode 100644 tsconfig.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f627f6f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +node_modules +dist +.env +.env.* +!.env.example +.git +.history +.mcp.json +*.md diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..b24ecc3 --- /dev/null +++ b/.env.example @@ -0,0 +1,20 @@ +# ============================================================================= +# STDIO MODE (local, via `npm run dev` or `npm start`) +# ============================================================================= +# These are read from the server's environment when running in stdio mode. +SOLIDTIME_API_TOKEN=your-api-token-here +SOLIDTIME_ORGANIZATION_ID=your-organization-uuid-here +# Optional — defaults to https://app.solidtime.io +# SOLIDTIME_API_URL=https://your-instance.example.com +# Optional — IANA timezone name for displaying and accepting local times (e.g. Europe/Berlin, America/New_York) +# Without this, all times are shown and expected in UTC. +# SOLIDTIME_TIMEZONE=Europe/Berlin + +# ============================================================================= +# HTTP MODE (cloud, via `npm run start:http` or Docker) +# ============================================================================= +# In HTTP mode, API token and org ID come from MCP client headers, +# NOT from environment variables. Only server-level config goes here. +# +# PORT=3000 +# SOLIDTIME_API_URL=https://app.solidtime.io diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a2ef9d5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +.env +*.tsbuildinfo +test-all-tools.sh diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..a4a2d60 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "semi": true, + "singleQuote": false, + "trailingComma": "es5", + "printWidth": 100, + "tabWidth": 2 +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ebf7c5e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +FROM node:22-alpine AS builder + +WORKDIR /app + +COPY package.json package-lock.json ./ +RUN npm ci --ignore-scripts + +COPY tsconfig.json ./ +COPY src/ ./src/ +RUN npx tsc + +FROM node:22-alpine + +WORKDIR /app + +COPY package.json package-lock.json ./ +RUN npm ci --omit=dev --ignore-scripts && npm cache clean --force + +COPY --from=builder /app/dist ./dist + +ENV PORT=3000 +EXPOSE 3000 + +CMD ["node", "dist/http-server.js"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3dc791e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Manuel Ernst + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3fcc15d --- /dev/null +++ b/README.md @@ -0,0 +1,180 @@ +# solidtime-mcp-server + +[![CI](https://github.com/SwamiRama/solidtime-mcp-server/actions/workflows/ci.yml/badge.svg)](https://github.com/SwamiRama/solidtime-mcp-server/actions/workflows/ci.yml) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![npm](https://img.shields.io/npm/v/solidtime-mcp-server)](https://www.npmjs.com/package/solidtime-mcp-server) + +MCP server for [SolidTime](https://www.solidtime.io/) — the open-source time tracking app. Start/stop timers, manage time entries, projects, clients, tags, and tasks directly from Claude, Cursor, or any MCP-compatible client. + +## Features + +- **22 tools** covering time entries, projects, clients, tags, tasks, and user info +- **Start/stop timers** with automatic active-timer detection +- **Aggregated reports** grouped by day, week, project, client, and more +- **Auto member_id resolution** — no manual configuration needed +- **Actionable error messages** — every error tells you what to do next +- **Zero external dependencies** beyond the MCP SDK (uses native `fetch`) +- Works with self-hosted SolidTime instances and the hosted version + +## Quick Start + +### Using npx (no install) + +```bash +npx solidtime-mcp-server +``` + +### Install globally + +```bash +npm install -g solidtime-mcp-server +``` + +### Environment Variables + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `SOLIDTIME_API_TOKEN` | Yes | — | Your SolidTime API token | +| `SOLIDTIME_ORGANIZATION_ID` | Yes | — | Your organization UUID | +| `SOLIDTIME_API_URL` | No | `https://app.solidtime.io` | Base URL for self-hosted instances | + +Get your API token from **SolidTime > Settings > API**. + +## Claude Desktop Configuration + +Add to your `claude_desktop_config.json`: + +```json +{ + "mcpServers": { + "solidtime": { + "command": "npx", + "args": ["-y", "solidtime-mcp-server"], + "env": { + "SOLIDTIME_API_TOKEN": "your-token-here", + "SOLIDTIME_ORGANIZATION_ID": "your-org-uuid-here", + "SOLIDTIME_API_URL": "https://your-instance.example.com" + } + } + } +} +``` + +## Claude Code Configuration + +Add to your `.mcp.json`: + +```json +{ + "mcpServers": { + "solidtime": { + "command": "npx", + "args": ["-y", "solidtime-mcp-server"], + "env": { + "SOLIDTIME_API_TOKEN": "your-token-here", + "SOLIDTIME_ORGANIZATION_ID": "your-org-uuid-here", + "SOLIDTIME_API_URL": "https://your-instance.example.com" + } + } + } +} +``` + +## Tools + +### Time Entries (8 tools) + +| Tool | Description | +|------|-------------| +| `solidtime_start_timer` | Start a running timer (checks for existing active timer first) | +| `solidtime_stop_timer` | Stop the active timer | +| `solidtime_get_active_timer` | Get the currently running timer | +| `solidtime_list_time_entries` | List entries with filters (date range, project, client, tags, billable) | +| `solidtime_create_time_entry` | Create a completed entry with start and end times | +| `solidtime_update_time_entry` | Update any field on an existing entry | +| `solidtime_delete_time_entry` | Permanently delete an entry | +| `solidtime_get_time_entry_report` | Aggregated report by day/week/month/project/client/etc. | + +### Projects (4 tools) + +| Tool | Description | +|------|-------------| +| `solidtime_list_projects` | List all projects (filter by archived status) | +| `solidtime_create_project` | Create a project with name, color, billable rate | +| `solidtime_update_project` | Update project fields | +| `solidtime_delete_project` | Permanently delete a project | + +### Clients (3 tools) + +| Tool | Description | +|------|-------------| +| `solidtime_list_clients` | List all clients (filter by archived status) | +| `solidtime_create_client` | Create a client | +| `solidtime_update_client` | Update a client's name | + +### Tags (3 tools) + +| Tool | Description | +|------|-------------| +| `solidtime_list_tags` | List all tags | +| `solidtime_create_tag` | Create a tag | +| `solidtime_update_tag` | Update a tag's name | + +### Tasks (3 tools) + +| Tool | Description | +|------|-------------| +| `solidtime_list_tasks` | List tasks (filter by project, done status) | +| `solidtime_create_task` | Create a task within a project | +| `solidtime_update_task` | Update task name, done status, or estimated time | + +### Users (1 tool) + +| Tool | Description | +|------|-------------| +| `solidtime_get_current_user` | Get your user profile and resolved member ID | + +## Usage Examples + +**Start tracking time:** +> "Start a timer for the website redesign project" + +**Log completed work:** +> "Create a time entry for today 9:00-11:30 on the API project, tagged as development" + +**Get a weekly report:** +> "Show me a report of this week's hours grouped by project" + +**Check what's running:** +> "Is there a timer running?" + +## Troubleshooting + +### "Authentication failed" +Your `SOLIDTIME_API_TOKEN` is invalid or expired. Generate a new one in SolidTime under Settings > API. + +### "Permission denied" +Your token doesn't have access to the specified organization. Verify `SOLIDTIME_ORGANIZATION_ID`. + +### "Cannot reach SolidTime" +Check that `SOLIDTIME_API_URL` is correct and the instance is accessible. For self-hosted: ensure the URL includes the protocol (e.g., `https://solidtime.example.com`). + +### "Could not find member for user" +The authenticated user is not a member of the specified organization. Check `SOLIDTIME_ORGANIZATION_ID`. + +## Development + +```bash +git clone https://github.com/SwamiRama/solidtime-mcp-server.git +cd solidtime-mcp-server +npm install +npm run dev # Run with tsx (dev mode) +npm run build # Compile TypeScript +npm run lint # ESLint +npm run typecheck # Type checking +npm run inspector # Test with MCP Inspector +``` + +## License + +MIT diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..13e739f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,17 @@ +services: + solidtime-mcp: + build: . + ports: + - "${PORT:-3000}:3000" + environment: + - PORT=3000 + # Optional: default SolidTime API URL for all sessions. + # Clients can override this via the x-solidtime-api-url header. + - SOLIDTIME_API_URL=${SOLIDTIME_API_URL:-https://app.solidtime.io} + restart: unless-stopped + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/health"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..5a079b9 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,18 @@ +import eslint from "@eslint/js"; +import tseslint from "typescript-eslint"; + +export default tseslint.config( + eslint.configs.recommended, + ...tseslint.configs.recommended, + { + ignores: ["dist/"], + }, + { + rules: { + "@typescript-eslint/no-unused-vars": [ + "error", + { argsIgnorePattern: "^_" }, + ], + }, + } +); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..c05ef9d --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3117 @@ +{ + "name": "solidtime-mcp-server", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "solidtime-mcp-server", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.6.1", + "dotenv": "^17.3.1", + "zod": "^3.23.8" + }, + "bin": { + "solidtime-mcp-server": "dist/index.js" + }, + "devDependencies": { + "@eslint/js": "^9.0.0", + "@types/node": "^22.10.0", + "eslint": "^9.0.0", + "prettier": "^3.0.0", + "tsx": "^4.19.2", + "typescript": "^5.7.2", + "typescript-eslint": "^8.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "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", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.4.tgz", + "integrity": "sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.3", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/js": { + "version": "9.39.3", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.3.tgz", + "integrity": "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.10", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.10.tgz", + "integrity": "sha512-hZ7nOssGqRgyV3FVVQdfi+U4q02uB23bpnYpdvNXkYTRRyWx84b7yf1ans+dnJ/7h41sGL3CeQTfO+ZGxuO+Iw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.27.1.tgz", + "integrity": "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==", + "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/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.13.tgz", + "integrity": "sha512-akNQMv0wW5uyRpD2v2IEyRSZiR+BeGuoB6L310EgGObO44HSMNT8z1xzio28V8qOrgYaopIDNA18YgdXd+qTiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz", + "integrity": "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/type-utils": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.56.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.1.tgz", + "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.1.tgz", + "integrity": "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.56.1", + "@typescript-eslint/types": "^8.56.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz", + "integrity": "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz", + "integrity": "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz", + "integrity": "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.1.tgz", + "integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz", + "integrity": "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.56.1", + "@typescript-eslint/tsconfig-utils": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.1.tgz", + "integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz", + "integrity": "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "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/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "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==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "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==", + "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/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "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/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==", + "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==", + "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/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "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==", + "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/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/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/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "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==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "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/dotenv": { + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "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==", + "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==", + "license": "MIT" + }, + "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/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==", + "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==", + "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==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "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", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.3", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.3.tgz", + "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.3", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "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/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.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "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==", + "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.2.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz", + "integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==", + "license": "MIT", + "dependencies": { + "ip-address": "10.0.1" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "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==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "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-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.4.tgz", + "integrity": "sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA==", + "dev": true, + "license": "ISC" + }, + "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/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/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "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==", + "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==", + "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==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "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==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.4", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.4.tgz", + "integrity": "sha512-ooiZW1Xy8rQ4oELQ++otI2T9DsKpV0M6c6cO6JGx4RTfav9poFFLlet9UMXHZnoM1yG0HWGlQLswBGX3RZmHtg==", + "license": "MIT", + "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==", + "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==", + "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/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "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-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "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==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "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==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "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/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", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "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==", + "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==", + "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/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/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/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "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==", + "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==", + "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==", + "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==", + "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==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "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/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "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/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "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/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "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==", + "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/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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=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/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "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/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/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "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==", + "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==", + "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==", + "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==", + "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==", + "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==", + "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/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/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "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/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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==", + "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/typescript-eslint": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.1.tgz", + "integrity": "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.56.1", + "@typescript-eslint/parser": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/utils": "8.56.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.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/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "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==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..d5c4864 --- /dev/null +++ b/package.json @@ -0,0 +1,57 @@ +{ + "name": "solidtime-mcp-server", + "version": "1.0.0", + "description": "MCP server for SolidTime time tracking — start/stop timers, manage entries, projects, clients, tags, and tasks", + "author": "Manuel", + "license": "MIT", + "type": "module", + "bin": { + "solidtime-mcp-server": "dist/index.js" + }, + "main": "dist/index.js", + "files": [ + "dist" + ], + "scripts": { + "build": "tsc", + "dev": "tsx src/index.ts", + "dev:http": "tsx src/http-server.ts", + "start": "node dist/index.js", + "start:http": "node dist/http-server.js", + "lint": "eslint src/", + "format": "prettier --write src/", + "format:check": "prettier --check src/", + "typecheck": "tsc --noEmit", + "prepare": "npm run build", + "inspector": "npx @modelcontextprotocol/inspector node dist/index.js" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.6.1", + "dotenv": "^17.3.1", + "zod": "^3.23.8" + }, + "devDependencies": { + "@eslint/js": "^9.0.0", + "@types/node": "^22.10.0", + "eslint": "^9.0.0", + "prettier": "^3.0.0", + "tsx": "^4.19.2", + "typescript": "^5.7.2", + "typescript-eslint": "^8.0.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "keywords": [ + "mcp", + "model-context-protocol", + "solidtime", + "time-tracking", + "ai", + "claude" + ], + "repository": { + "type": "git", + "url": "https://github.com/SwamiRama/solidtime-mcp-server" + } +} diff --git a/src/api-client.ts b/src/api-client.ts new file mode 100644 index 0000000..d4ce80a --- /dev/null +++ b/src/api-client.ts @@ -0,0 +1,138 @@ +import { DEFAULT_API_URL } from "./constants.js"; + +export class SolidTimeApiError extends Error { + constructor( + public status: number, + public body: unknown + ) { + super(getErrorMessage(status, body)); + this.name = "SolidTimeApiError"; + } +} + +function getErrorMessage(status: number, body: unknown): string { + const detail = typeof body === "object" && body !== null ? JSON.stringify(body) : String(body); + + switch (status) { + case 401: + return ( + "Authentication failed. Possible causes:\n" + + " 1. Your SOLIDTIME_API_TOKEN may be expired or revoked — generate a new one in Settings > API.\n" + + " 2. Self-hosted: Passport keys may be missing — run 'php artisan passport:keys' on the server.\n" + + " 3. Self-hosted: Verify SOLIDTIME_API_URL points to the correct instance." + ); + case 403: + return "Permission denied. Your token may lack access to this organization."; + case 404: + return "Resource not found. Use the list tools (e.g. solidtime_list_projects) to find valid IDs."; + case 422: + return `Validation error: ${formatValidationErrors(body)}`; + case 429: + return "Rate limited. Wait a moment and try again."; + default: + if (status >= 500) return `SolidTime server error (${status}). Try again later.`; + return `API error ${status}: ${detail}`; + } +} + +function formatValidationErrors(body: unknown): string { + if (typeof body !== "object" || body === null) return String(body); + const obj = body as Record; + if (obj.errors && typeof obj.errors === "object") { + return Object.entries(obj.errors as Record) + .map(([field, msgs]) => `${field}: ${msgs.join(", ")}`) + .join("; "); + } + if (obj.message) return String(obj.message); + return JSON.stringify(body); +} + +export class ApiClient { + private baseUrl: string; + private token: string; + + constructor(baseUrl: string | undefined, token: string) { + this.baseUrl = (baseUrl || DEFAULT_API_URL).replace(/\/$/, ""); + this.token = token.trim(); + } + + private async request(method: string, path: string, body?: unknown): Promise { + const url = path.startsWith("/api/v1") + ? `${this.baseUrl}${path}` + : `${this.baseUrl}/api/v1${path}`; + + const headers: Record = { + Authorization: `Bearer ${this.token}`, + Accept: "application/json", + }; + + if (body !== undefined) { + headers["Content-Type"] = "application/json"; + } + + let response: Response; + try { + response = await fetch(url, { + method, + headers, + body: body !== undefined ? JSON.stringify(body) : undefined, + }); + } catch (err) { + throw new Error( + `Cannot reach SolidTime at ${this.baseUrl}. Check the SOLIDTIME_API_URL setting. (${err})` + ); + } + + if (response.status === 204) { + return undefined as T; + } + + let responseBody: unknown; + try { + responseBody = await response.json(); + } catch { + responseBody = await response.text().catch(() => ""); + } + + if (!response.ok) { + throw new SolidTimeApiError(response.status, responseBody); + } + + return responseBody as T; + } + + get(path: string): Promise { + return this.request("GET", path); + } + + async getOrNull(path: string): Promise { + try { + return await this.request("GET", path); + } catch (err) { + if (err instanceof SolidTimeApiError && err.status === 404) { + return null; + } + throw err; + } + } + + post(path: string, body: unknown): Promise { + return this.request("POST", path, body); + } + + put(path: string, body: unknown): Promise { + return this.request("PUT", path, body); + } + + patch(path: string, body: unknown): Promise { + return this.request("PATCH", path, body); + } + + delete(path: string): Promise { + return this.request("DELETE", path); + } + + deleteWithBody(path: string, body: unknown): Promise { + return this.request("DELETE", path, body); + } +} diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..b151832 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,58 @@ +export const DEFAULT_API_URL = "https://app.solidtime.io"; + +export const API_PATHS = { + me: "/users/me", + myMemberships: "/users/me/memberships", + myTimeEntries: "/users/me/time-entries", + activeTimer: "/users/me/time-entries/active", + organization: (orgId: string) => `/api/v1/organizations/${orgId}`, + members: (orgId: string) => `/api/v1/organizations/${orgId}/members`, + member: (orgId: string, memberId: string) => + `/api/v1/organizations/${orgId}/members/${memberId}`, + memberInvitePlaceholder: (orgId: string, memberId: string) => + `/api/v1/organizations/${orgId}/members/${memberId}/invite-placeholder`, + memberMakePlaceholder: (orgId: string, memberId: string) => + `/api/v1/organizations/${orgId}/members/${memberId}/make-placeholder`, + timeEntries: (orgId: string) => `/api/v1/organizations/${orgId}/time-entries`, + timeEntry: (orgId: string, id: string) => `/api/v1/organizations/${orgId}/time-entries/${id}`, + timeEntryReport: (orgId: string) => `/api/v1/organizations/${orgId}/time-entries/aggregate`, + projects: (orgId: string) => `/api/v1/organizations/${orgId}/projects`, + project: (orgId: string, id: string) => `/api/v1/organizations/${orgId}/projects/${id}`, + projectMembers: (orgId: string, projectId: string) => + `/api/v1/organizations/${orgId}/projects/${projectId}/project-members`, + projectMember: (orgId: string, projectMemberId: string) => + `/api/v1/organizations/${orgId}/project-members/${projectMemberId}`, + clients: (orgId: string) => `/api/v1/organizations/${orgId}/clients`, + client: (orgId: string, id: string) => `/api/v1/organizations/${orgId}/clients/${id}`, + tags: (orgId: string) => `/api/v1/organizations/${orgId}/tags`, + tag: (orgId: string, id: string) => `/api/v1/organizations/${orgId}/tags/${id}`, + tasks: (orgId: string) => `/api/v1/organizations/${orgId}/tasks`, + task: (orgId: string, id: string) => `/api/v1/organizations/${orgId}/tasks/${id}`, + invitations: (orgId: string) => `/api/v1/organizations/${orgId}/invitations`, + invitation: (orgId: string, invitationId: string) => + `/api/v1/organizations/${orgId}/invitations/${invitationId}`, + invitationResend: (orgId: string, invitationId: string) => + `/api/v1/organizations/${orgId}/invitations/${invitationId}/resend`, +} as const; + +export const GROUP_BY_VALUES = [ + "day", + "week", + "month", + "year", + "user", + "project", + "task", + "client", + "billable", + "description", + "tag", +] as const; + +export type GroupBy = (typeof GROUP_BY_VALUES)[number]; + +export const PAGINATION = { + defaultLimit: 50, + maxLimit: 500, + defaultPage: 1, +} as const; diff --git a/src/formatting.ts b/src/formatting.ts new file mode 100644 index 0000000..3d02522 --- /dev/null +++ b/src/formatting.ts @@ -0,0 +1,78 @@ +export function formatDuration(seconds: number): string { + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + if (h > 0 && m > 0) return `${h}h ${m}m`; + if (h > 0) return `${h}h`; + if (m > 0) return `${m}m`; + return `${seconds}s`; +} + +export function formatCurrency(cents: number): string { + const amount = cents / 100; + return amount.toLocaleString("en-US", { + style: "currency", + currency: "EUR", + minimumFractionDigits: 2, + }); +} + +export function formatDateTime(utcString: string, timezone?: string): string { + if (!timezone) { + return utcString.replace("T", " ").replace("Z", " UTC"); + } + const date = new Date(utcString); + const parts = new Intl.DateTimeFormat("en-CA", { + timeZone: timezone, + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, + timeZoneName: "short", + }).formatToParts(date); + const p: Record = {}; + for (const part of parts) { + if (part.type !== "literal") p[part.type] = part.value; + } + return `${p.year}-${p.month}-${p.day} ${p.hour}:${p.minute}:${p.second} ${p.timeZoneName}`; +} + +// Convert a time string to UTC. If the string already has a timezone suffix (Z or ±HH:MM) +// it is normalised and returned as-is. If it has no suffix it is treated as local time in +// the given timezone and converted to UTC. +export function toUTC(timeStr: string, timezone?: string): string { + if (!timeStr) return timeStr; + if (timeStr.endsWith("Z") || /[+-]\d{2}:\d{2}$/.test(timeStr)) { + return new Date(timeStr).toISOString().replace(/\.\d{3}Z$/, "Z"); + } + if (!timezone) return timeStr + "Z"; + + // Temporarily treat the naive string as UTC to get a Date object, then figure out + // what the local clock shows at that UTC moment and compute the real offset. + const naiveUTC = new Date(timeStr + "Z"); + const parts = new Intl.DateTimeFormat("en-CA", { + timeZone: timezone, + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, + }).formatToParts(naiveUTC); + const p: Record = {}; + for (const part of parts) { + if (part.type !== "literal") p[part.type] = part.value; + } + const localAtNaive = new Date( + `${p.year}-${p.month}-${p.day}T${p.hour}:${p.minute}:${p.second}Z` + ); + const offsetMs = localAtNaive.getTime() - naiveUTC.getTime(); + return new Date(naiveUTC.getTime() - offsetMs).toISOString().replace(/\.\d{3}Z$/, "Z"); +} + +export function nowUTC(): string { + return new Date().toISOString().replace(/\.\d{3}Z$/, "Z"); +} diff --git a/src/http-server.ts b/src/http-server.ts new file mode 100644 index 0000000..56544e7 --- /dev/null +++ b/src/http-server.ts @@ -0,0 +1,182 @@ +import { randomUUID } from "node:crypto"; +import http from "node:http"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"; +import { createServer } from "./server.js"; + +const PORT = parseInt(process.env.PORT || "3000", 10); +const DEFAULT_SOLIDTIME_API_URL = process.env.SOLIDTIME_API_URL; + +interface Session { + transport: StreamableHTTPServerTransport; + createdAt: number; +} + +const sessions = new Map(); + +// Clean up stale sessions every 10 minutes +setInterval( + () => { + const now = Date.now(); + const maxAge = 30 * 60 * 1000; // 30 minutes + for (const [id, session] of sessions) { + if (now - session.createdAt > maxAge) { + session.transport.close().catch(() => {}); + sessions.delete(id); + } + } + }, + 10 * 60 * 1000 +); + +function extractConfig(req: http.IncomingMessage) { + const apiToken = req.headers["x-solidtime-api-token"] as string | undefined; + const organizationId = req.headers["x-solidtime-organization-id"] as string | undefined; + const apiUrl = + (req.headers["x-solidtime-api-url"] as string | undefined) || DEFAULT_SOLIDTIME_API_URL; + const timezone = req.headers["x-solidtime-timezone"] as string | undefined; + + if (!apiToken) { + throw new Error( + "Missing required header: x-solidtime-api-token. " + + "Configure it in your MCP client settings." + ); + } + + return { apiToken, organizationId, apiUrl, timezone }; +} + +function setCorsHeaders(res: http.ServerResponse) { + res.setHeader("Access-Control-Allow-Origin", "*"); + res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS"); + res.setHeader( + "Access-Control-Allow-Headers", + "Content-Type, mcp-session-id, x-solidtime-api-token, x-solidtime-organization-id, x-solidtime-api-url, x-solidtime-timezone" + ); + res.setHeader("Access-Control-Expose-Headers", "mcp-session-id"); +} + +const httpServer = http.createServer(async (req, res) => { + setCorsHeaders(res); + + if (req.method === "OPTIONS") { + res.writeHead(204); + res.end(); + return; + } + + if (req.url === "/health") { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ status: "ok", sessions: sessions.size })); + return; + } + + if (req.url !== "/mcp") { + res.writeHead(404, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: "Not found. Use /mcp for MCP protocol or /health for health check." })); + return; + } + + const sessionId = req.headers["mcp-session-id"] as string | undefined; + + try { + // GET = SSE stream listener, DELETE = close session + if (req.method === "GET" || req.method === "DELETE") { + if (!sessionId || !sessions.has(sessionId)) { + res.writeHead(400, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: "Invalid or missing session" })); + return; + } + + const session = sessions.get(sessionId)!; + + if (req.method === "DELETE") { + await session.transport.close(); + sessions.delete(sessionId); + res.writeHead(204); + res.end(); + return; + } + + await session.transport.handleRequest(req, res); + return; + } + + if (req.method === "POST") { + const body = await new Promise((resolve, reject) => { + let data = ""; + req.on("data", (chunk: Buffer) => (data += chunk.toString())); + req.on("end", () => resolve(data)); + req.on("error", reject); + }); + const parsedBody = JSON.parse(body); + + if (!sessionId) { + // Must be an initialization request + const messages = Array.isArray(parsedBody) ? parsedBody : [parsedBody]; + const isInit = messages.some(isInitializeRequest); + + if (!isInit) { + res.writeHead(400, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + error: "Missing mcp-session-id header for non-initialization request", + }) + ); + return; + } + + // Extract credentials from headers and create a per-session MCP server + const config = extractConfig(req); + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => randomUUID(), + }); + const mcpServer = await createServer(config); + await mcpServer.connect(transport); + + const sid = transport.sessionId!; + sessions.set(sid, { transport, createdAt: Date.now() }); + + transport.onclose = () => { + sessions.delete(sid); + }; + + await transport.handleRequest(req, res, parsedBody); + } else { + // Existing session + const session = sessions.get(sessionId); + if (!session) { + res.writeHead(404, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: "Session not found. It may have expired." })); + return; + } + // Refresh session timestamp + session.createdAt = Date.now(); + await session.transport.handleRequest(req, res, parsedBody); + } + return; + } + + res.writeHead(405, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: "Method not allowed" })); + } catch (err) { + console.error("Request error:", err); + if (!res.headersSent) { + res.writeHead(500, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + error: err instanceof Error ? err.message : "Internal server error", + }) + ); + } + } +}); + +httpServer.listen(PORT, "0.0.0.0", () => { + console.log(`SolidTime MCP HTTP server listening on port ${PORT}`); + console.log(` MCP endpoint: http://0.0.0.0:${PORT}/mcp`); + console.log(` Health check: http://0.0.0.0:${PORT}/health`); + if (DEFAULT_SOLIDTIME_API_URL) { + console.log(` Default API URL: ${DEFAULT_SOLIDTIME_API_URL}`); + } +}); diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..f35b216 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,27 @@ +#!/usr/bin/env node + +import "dotenv/config"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { createServer } from "./server.js"; + +async function main() { + const apiToken = process.env.SOLIDTIME_API_TOKEN; + const organizationId = process.env.SOLIDTIME_ORGANIZATION_ID; + const apiUrl = process.env.SOLIDTIME_API_URL; + const timezone = process.env.SOLIDTIME_TIMEZONE; + + if (!apiToken) { + console.error("Error: SOLIDTIME_API_TOKEN environment variable is required."); + console.error("Get your API token from your SolidTime instance under Settings > API."); + process.exit(1); + } + + const server = await createServer({ apiToken, organizationId, apiUrl, timezone }); + const transport = new StdioServerTransport(); + await server.connect(transport); +} + +main().catch((err) => { + console.error("Fatal error:", err); + process.exit(1); +}); diff --git a/src/schemas.ts b/src/schemas.ts new file mode 100644 index 0000000..2187648 --- /dev/null +++ b/src/schemas.ts @@ -0,0 +1,20 @@ +import { z } from "zod"; + +export const coerceBoolean = z.preprocess((val) => { + if (typeof val === "string") { + if (val === "true") return true; + if (val === "false") return false; + } + return val; +}, z.boolean()); + +export const coerceUuidArray = z.preprocess((val) => { + if (typeof val === "string") { + try { + return JSON.parse(val); + } catch { + return val; + } + } + return val; +}, z.array(z.string().uuid())); diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 0000000..07c380d --- /dev/null +++ b/src/server.ts @@ -0,0 +1,107 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { ApiClient, SolidTimeApiError } from "./api-client.js"; +import { API_PATHS, DEFAULT_API_URL } from "./constants.js"; +import { registerUserTools } from "./tools/users.js"; +import { registerTimeEntryTools } from "./tools/time-entries.js"; +import { registerProjectTools } from "./tools/projects.js"; +import { registerClientTools } from "./tools/clients.js"; +import { registerTagTools } from "./tools/tags.js"; +import { registerTaskTools } from "./tools/tasks.js"; +import { registerMemberTools } from "./tools/members.js"; +import { registerOrganizationTools } from "./tools/organizations.js"; +import { registerProjectMemberTools } from "./tools/project-members.js"; +import { registerInvitationTools } from "./tools/invitations.js"; +import type { User, UserMembership } from "./types.js"; + +interface ServerConfig { + apiToken: string; + organizationId?: string; + apiUrl?: string; + timezone?: string; +} + +export async function createServer(config: ServerConfig) { + const apiUrl = config.apiUrl || DEFAULT_API_URL; + const api = new ApiClient(apiUrl, config.apiToken); + + console.error(`SolidTime MCP: Connecting to ${apiUrl}...`); + + // Fetch user and memberships in parallel + let userResponse: { data: User }; + try { + userResponse = await api.get<{ data: User }>(API_PATHS.me); + } catch (err) { + if (err instanceof SolidTimeApiError && err.status === 401) { + throw new Error( + `Authentication failed against ${apiUrl}.\n` + + ` Token: ${config.apiToken.substring(0, 10)}...(${config.apiToken.length} chars)\n` + + ` Possible fixes:\n` + + ` 1. Generate a new API token in Settings > API.\n` + + ` 2. Self-hosted: run 'php artisan passport:keys' on the server.\n` + + ` 3. Verify SOLIDTIME_API_URL is correct (currently: ${apiUrl}).` + ); + } + throw err; + } + const user = userResponse.data; + + const membershipsResponse = await api.get<{ data: UserMembership[] }>(API_PATHS.myMemberships); + const memberships = membershipsResponse.data ?? []; + + if (memberships.length === 0) { + throw new Error(`User ${user.email} has no organization memberships.`); + } + + let membership: UserMembership; + if (config.organizationId) { + const found = memberships.find((m) => m.organization.id === config.organizationId); + if (!found) { + const available = memberships + .map((m) => ` ${m.organization.id} (${m.organization.name})`) + .join("\n"); + throw new Error( + `Organization ${config.organizationId} not found. Your memberships:\n${available}\n` + + `Update your organization ID setting.` + ); + } + membership = found; + } else if (memberships.length === 1) { + membership = memberships[0]; + console.error(`SolidTime MCP: Auto-selected organization "${membership.organization.name}" (${membership.organization.id})`); + } else { + const available = memberships + .map((m) => ` ${m.organization.id} (${m.organization.name})`) + .join("\n"); + throw new Error( + `Multiple organizations found. Set organization ID to one of:\n${available}` + ); + } + + const orgId = membership.organization.id; + const memberId = membership.id; + const getMemberId = () => memberId; + const timezone = config.timezone; + + console.error( + `SolidTime MCP: Authenticated as ${user.name} (${user.email}), org "${membership.organization.name}", member ${memberId}` + + (timezone ? `, timezone ${timezone}` : "") + ); + + const server = new McpServer({ + name: "solidtime", + version: "1.0.0", + }); + + registerUserTools(server, api, getMemberId); + registerTimeEntryTools(server, api, orgId, getMemberId, timezone); + registerProjectTools(server, api, orgId); + registerClientTools(server, api, orgId); + registerTagTools(server, api, orgId); + registerTaskTools(server, api, orgId); + registerMemberTools(server, api, orgId); + registerOrganizationTools(server, api, orgId); + registerProjectMemberTools(server, api, orgId); + registerInvitationTools(server, api, orgId); + + return server; +} diff --git a/src/tools/clients.ts b/src/tools/clients.ts new file mode 100644 index 0000000..b0fd1e8 --- /dev/null +++ b/src/tools/clients.ts @@ -0,0 +1,124 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { ApiClient } from "../api-client.js"; +import { API_PATHS } from "../constants.js"; +import type { Client, PaginatedResponse } from "../types.js"; + +export function registerClientTools(server: McpServer, api: ApiClient, orgId: string) { + server.registerTool( + "solidtime_list_clients", + { + title: "List Clients", + description: "List all clients in the organization. Optionally filter by archived status.", + inputSchema: { + archived: z.enum(["true", "false"]).optional().describe("Filter by archived status"), + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true, + }, + }, + async (params) => { + const query = new URLSearchParams(); + if (params.archived) query.set("archived", params.archived); + const qs = query.toString(); + const path = qs ? `${API_PATHS.clients(orgId)}?${qs}` : API_PATHS.clients(orgId); + + const result = await api.get>(path); + const clients = result.data ?? []; + if (clients.length === 0) { + return { content: [{ type: "text", text: "No clients found." }] }; + } + + const lines = clients.map( + (c) => `ID: ${c.id}\nName: ${c.name}\nArchived: ${c.is_archived ? "Yes" : "No"}` + ); + lines.unshift(`Found ${clients.length} clients:\n`); + return { content: [{ type: "text", text: lines.join("\n\n") }] }; + } + ); + + server.registerTool( + "solidtime_create_client", + { + title: "Create Client", + description: "Create a new client.", + inputSchema: { + name: z.string().min(1).describe("Client name"), + }, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: true, + }, + }, + async (params) => { + const result = await api.post<{ data: Client }>(API_PATHS.clients(orgId), { + name: params.name, + }); + return { + content: [ + { + type: "text", + text: `Client created.\n\nID: ${result.data.id}\nName: ${result.data.name}`, + }, + ], + }; + } + ); + + server.registerTool( + "solidtime_update_client", + { + title: "Update Client", + description: "Update a client's name.", + inputSchema: { + id: z.string().uuid().describe("Client UUID to update"), + name: z.string().min(1).describe("New client name"), + }, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true, + }, + }, + async (params) => { + const result = await api.put<{ data: Client }>(API_PATHS.client(orgId, params.id), { + name: params.name, + }); + return { + content: [ + { + type: "text", + text: `Client updated.\n\nID: ${result.data.id}\nName: ${result.data.name}`, + }, + ], + }; + } + ); + + server.registerTool( + "solidtime_delete_client", + { + title: "Delete Client", + description: "Permanently delete a client. This cannot be undone.", + inputSchema: { + id: z.string().uuid().describe("Client UUID to delete"), + }, + annotations: { + readOnlyHint: false, + destructiveHint: true, + idempotentHint: true, + openWorldHint: true, + }, + }, + async (params) => { + await api.delete(API_PATHS.client(orgId, params.id)); + return { content: [{ type: "text", text: `Client ${params.id} deleted.` }] }; + } + ); +} diff --git a/src/tools/invitations.ts b/src/tools/invitations.ts new file mode 100644 index 0000000..c91cf02 --- /dev/null +++ b/src/tools/invitations.ts @@ -0,0 +1,112 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { ApiClient } from "../api-client.js"; +import { API_PATHS } from "../constants.js"; +import type { Invitation, PaginatedResponse } from "../types.js"; + +export function registerInvitationTools(server: McpServer, api: ApiClient, orgId: string) { + server.registerTool( + "solidtime_list_invitations", + { + title: "List Invitations", + description: "List all pending invitations for the organization.", + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true, + }, + }, + async () => { + const result = await api.get>( + API_PATHS.invitations(orgId) + ); + const invitations = result.data ?? []; + if (invitations.length === 0) { + return { content: [{ type: "text", text: "No pending invitations." }] }; + } + + const lines = invitations.map((inv) => formatInvitation(inv)); + lines.unshift(`Found ${invitations.length} invitations:\n`); + return { content: [{ type: "text", text: lines.join("\n\n") }] }; + } + ); + + server.registerTool( + "solidtime_create_invitation", + { + title: "Create Invitation", + description: "Invite a new user to the organization by email.", + inputSchema: { + email: z.string().email().describe("Email address to invite"), + role: z.string().describe("Role to assign (e.g. admin, manager, employee)"), + }, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: true, + }, + }, + async (params) => { + const result = await api.post<{ data: Invitation }>(API_PATHS.invitations(orgId), { + email: params.email, + role: params.role, + }); + return { + content: [ + { + type: "text", + text: `Invitation sent.\n\n${formatInvitation(result.data)}`, + }, + ], + }; + } + ); + + server.registerTool( + "solidtime_resend_invitation", + { + title: "Resend Invitation", + description: "Resend an existing invitation email.", + inputSchema: { + id: z.string().uuid().describe("Invitation UUID to resend"), + }, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: true, + }, + }, + async (params) => { + await api.post(API_PATHS.invitationResend(orgId, params.id), {}); + return { content: [{ type: "text", text: `Invitation ${params.id} resent.` }] }; + } + ); + + server.registerTool( + "solidtime_delete_invitation", + { + title: "Delete Invitation", + description: "Delete a pending invitation.", + inputSchema: { + id: z.string().uuid().describe("Invitation UUID to delete"), + }, + annotations: { + readOnlyHint: false, + destructiveHint: true, + idempotentHint: true, + openWorldHint: true, + }, + }, + async (params) => { + await api.delete(API_PATHS.invitation(orgId, params.id)); + return { content: [{ type: "text", text: `Invitation ${params.id} deleted.` }] }; + } + ); +} + +function formatInvitation(inv: Invitation): string { + return [`ID: ${inv.id}`, `Email: ${inv.email}`, `Role: ${inv.role}`].join("\n"); +} diff --git a/src/tools/members.ts b/src/tools/members.ts new file mode 100644 index 0000000..344f80e --- /dev/null +++ b/src/tools/members.ts @@ -0,0 +1,150 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { ApiClient } from "../api-client.js"; +import { API_PATHS } from "../constants.js"; +import { formatCurrency } from "../formatting.js"; +import type { Member, PaginatedResponse } from "../types.js"; + +export function registerMemberTools(server: McpServer, api: ApiClient, orgId: string) { + server.registerTool( + "solidtime_list_members", + { + title: "List Members", + description: + "List all members of the organization, including their roles and billable rates.", + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true, + }, + }, + async () => { + const result = await api.get>(API_PATHS.members(orgId)); + const members = result.data ?? []; + if (members.length === 0) { + return { content: [{ type: "text", text: "No members found." }] }; + } + + const lines = members.map((m) => formatMember(m)); + lines.unshift(`Found ${members.length} members:\n`); + return { content: [{ type: "text", text: lines.join("\n\n") }] }; + } + ); + + server.registerTool( + "solidtime_update_member", + { + title: "Update Member", + description: "Update a member's role or billable rate.", + inputSchema: { + id: z.string().uuid().describe("Member UUID to update"), + role: z.string().optional().describe("New role (e.g. admin, manager, employee, placeholder)"), + billable_rate: z + .number() + .int() + .min(0) + .optional() + .describe("New billable rate in cents (e.g. 15000 = EUR 150.00)"), + }, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true, + }, + }, + async (params) => { + const body: Record = {}; + if (params.role !== undefined) body.role = params.role; + if (params.billable_rate !== undefined) body.billable_rate = params.billable_rate; + + const result = await api.put<{ data: Member }>( + API_PATHS.member(orgId, params.id), + body + ); + return { + content: [{ type: "text", text: `Member updated.\n\n${formatMember(result.data)}` }], + }; + } + ); + + server.registerTool( + "solidtime_remove_member", + { + title: "Remove Member", + description: "Remove a member from the organization. This cannot be undone.", + inputSchema: { + id: z.string().uuid().describe("Member UUID to remove"), + }, + annotations: { + readOnlyHint: false, + destructiveHint: true, + idempotentHint: true, + openWorldHint: true, + }, + }, + async (params) => { + await api.delete(API_PATHS.member(orgId, params.id)); + return { content: [{ type: "text", text: `Member ${params.id} removed.` }] }; + } + ); + + server.registerTool( + "solidtime_invite_placeholder_member", + { + title: "Invite Placeholder Member", + description: + "Send an invitation to a placeholder member so they can create an account and take over their time entries.", + inputSchema: { + id: z.string().uuid().describe("Placeholder member UUID to invite"), + }, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: true, + }, + }, + async (params) => { + await api.post(API_PATHS.memberInvitePlaceholder(orgId, params.id), {}); + return { + content: [{ type: "text", text: `Invitation sent to placeholder member ${params.id}.` }], + }; + } + ); + + server.registerTool( + "solidtime_make_placeholder_member", + { + title: "Make Placeholder Member", + description: + "Convert a real member into a placeholder member. The user will lose access but their time entries are preserved.", + inputSchema: { + id: z.string().uuid().describe("Member UUID to convert to placeholder"), + }, + annotations: { + readOnlyHint: false, + destructiveHint: true, + idempotentHint: true, + openWorldHint: true, + }, + }, + async (params) => { + await api.post(API_PATHS.memberMakePlaceholder(orgId, params.id), {}); + return { + content: [{ type: "text", text: `Member ${params.id} converted to placeholder.` }], + }; + } + ); +} + +function formatMember(m: Member): string { + const parts = [ + `ID: ${m.id}`, + `User ID: ${m.user_id}`, + `Role: ${m.role}`, + ]; + if (m.billable_rate !== null) parts.push(`Billable Rate: ${formatCurrency(m.billable_rate)}/h`); + return parts.join("\n"); +} diff --git a/src/tools/organizations.ts b/src/tools/organizations.ts new file mode 100644 index 0000000..e41b3f0 --- /dev/null +++ b/src/tools/organizations.ts @@ -0,0 +1,80 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { ApiClient } from "../api-client.js"; +import { API_PATHS } from "../constants.js"; +import { formatCurrency } from "../formatting.js"; +import type { Organization } from "../types.js"; + +export function registerOrganizationTools(server: McpServer, api: ApiClient, orgId: string) { + server.registerTool( + "solidtime_get_organization", + { + title: "Get Organization", + description: "Get the current organization's details including name, currency, and billable rate.", + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true, + }, + }, + async () => { + const result = await api.get<{ data: Organization }>(API_PATHS.organization(orgId)); + return { + content: [{ type: "text", text: formatOrganization(result.data) }], + }; + } + ); + + server.registerTool( + "solidtime_update_organization", + { + title: "Update Organization", + description: "Update the organization's name, currency, or billable rate.", + inputSchema: { + name: z.string().min(1).optional().describe("New organization name"), + currency: z.string().min(3).max(3).optional().describe("New currency code (e.g. EUR, USD)"), + billable_rate: z + .number() + .int() + .min(0) + .optional() + .describe("New default billable rate in cents (e.g. 15000 = EUR 150.00)"), + }, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true, + }, + }, + async (params) => { + const body: Record = {}; + if (params.name !== undefined) body.name = params.name; + if (params.currency !== undefined) body.currency = params.currency; + if (params.billable_rate !== undefined) body.billable_rate = params.billable_rate; + + const result = await api.put<{ data: Organization }>( + API_PATHS.organization(orgId), + body + ); + return { + content: [ + { type: "text", text: `Organization updated.\n\n${formatOrganization(result.data)}` }, + ], + }; + } + ); +} + +function formatOrganization(org: Organization): string { + const parts = [ + `ID: ${org.id}`, + `Name: ${org.name}`, + `Currency: ${org.currency}`, + ]; + if (org.billable_rate !== null) { + parts.push(`Default Billable Rate: ${formatCurrency(org.billable_rate)}/h`); + } + return parts.join("\n"); +} diff --git a/src/tools/project-members.ts b/src/tools/project-members.ts new file mode 100644 index 0000000..11500be --- /dev/null +++ b/src/tools/project-members.ts @@ -0,0 +1,147 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { ApiClient } from "../api-client.js"; +import { API_PATHS } from "../constants.js"; +import { formatCurrency } from "../formatting.js"; +import type { ProjectMember, PaginatedResponse } from "../types.js"; + +export function registerProjectMemberTools(server: McpServer, api: ApiClient, orgId: string) { + server.registerTool( + "solidtime_list_project_members", + { + title: "List Project Members", + description: "List all members assigned to a specific project.", + inputSchema: { + project_id: z.string().uuid().describe("Project UUID"), + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true, + }, + }, + async (params) => { + const result = await api.get>( + API_PATHS.projectMembers(orgId, params.project_id) + ); + const members = result.data ?? []; + if (members.length === 0) { + return { content: [{ type: "text", text: "No project members found." }] }; + } + + const lines = members.map((m) => formatProjectMember(m)); + lines.unshift(`Found ${members.length} project members:\n`); + return { content: [{ type: "text", text: lines.join("\n\n") }] }; + } + ); + + server.registerTool( + "solidtime_add_project_member", + { + title: "Add Project Member", + description: "Add a member to a project with an optional custom billable rate.", + inputSchema: { + project_id: z.string().uuid().describe("Project UUID"), + member_id: z.string().uuid().describe("Member UUID to add"), + billable_rate: z + .number() + .int() + .min(0) + .optional() + .describe("Custom billable rate in cents for this member on this project"), + }, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: true, + }, + }, + async (params) => { + const body: Record = { + member_id: params.member_id, + }; + if (params.billable_rate !== undefined) body.billable_rate = params.billable_rate; + + const result = await api.post<{ data: ProjectMember }>( + API_PATHS.projectMembers(orgId, params.project_id), + body + ); + return { + content: [ + { type: "text", text: `Project member added.\n\n${formatProjectMember(result.data)}` }, + ], + }; + } + ); + + server.registerTool( + "solidtime_update_project_member", + { + title: "Update Project Member", + description: "Update a project member's billable rate.", + inputSchema: { + id: z.string().uuid().describe("Project member UUID to update"), + billable_rate: z + .number() + .int() + .min(0) + .describe("New billable rate in cents for this project member"), + }, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true, + }, + }, + async (params) => { + const result = await api.put<{ data: ProjectMember }>( + API_PATHS.projectMember(orgId, params.id), + { billable_rate: params.billable_rate } + ); + return { + content: [ + { + type: "text", + text: `Project member updated.\n\n${formatProjectMember(result.data)}`, + }, + ], + }; + } + ); + + server.registerTool( + "solidtime_remove_project_member", + { + title: "Remove Project Member", + description: "Remove a member from a project.", + inputSchema: { + id: z.string().uuid().describe("Project member UUID to remove"), + }, + annotations: { + readOnlyHint: false, + destructiveHint: true, + idempotentHint: true, + openWorldHint: true, + }, + }, + async (params) => { + await api.delete(API_PATHS.projectMember(orgId, params.id)); + return { content: [{ type: "text", text: `Project member ${params.id} removed.` }] }; + } + ); +} + +function formatProjectMember(pm: ProjectMember): string { + const parts = [ + `ID: ${pm.id}`, + `Member ID: ${pm.member_id}`, + `Project ID: ${pm.project_id}`, + ]; + if (pm.billable_rate !== null) { + parts.push(`Billable Rate: ${formatCurrency(pm.billable_rate)}/h`); + } + return parts.join("\n"); +} diff --git a/src/tools/projects.ts b/src/tools/projects.ts new file mode 100644 index 0000000..b44dd7d --- /dev/null +++ b/src/tools/projects.ts @@ -0,0 +1,178 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { ApiClient } from "../api-client.js"; +import { API_PATHS } from "../constants.js"; +import { coerceBoolean } from "../schemas.js"; +import { formatCurrency, formatDuration } from "../formatting.js"; +import type { Project, PaginatedResponse } from "../types.js"; + +export function registerProjectTools(server: McpServer, api: ApiClient, orgId: string) { + server.registerTool( + "solidtime_list_projects", + { + title: "List Projects", + description: "List all projects in the organization. Optionally filter by archived status.", + inputSchema: { + archived: z.enum(["true", "false"]).optional().describe("Filter by archived status"), + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true, + }, + }, + async (params) => { + const query = new URLSearchParams(); + if (params.archived) query.set("archived", params.archived); + const qs = query.toString(); + const path = qs ? `${API_PATHS.projects(orgId)}?${qs}` : API_PATHS.projects(orgId); + + const result = await api.get>(path); + const projects = result.data ?? []; + if (projects.length === 0) { + return { content: [{ type: "text", text: "No projects found." }] }; + } + + const lines = projects.map((p) => formatProject(p)); + lines.unshift(`Found ${projects.length} projects:\n`); + return { content: [{ type: "text", text: lines.join("\n\n") }] }; + } + ); + + server.registerTool( + "solidtime_create_project", + { + title: "Create Project", + description: "Create a new project.", + inputSchema: { + name: z.string().min(1).describe("Project name"), + color: z + .string() + .regex(/^#[0-9a-f]{6}$/) + .describe("Hex color, lowercase (e.g. #4caf50)"), + is_billable: coerceBoolean.describe("Whether time tracked to this project is billable"), + client_id: z.string().uuid().describe("Client UUID to assign (required)"), + billable_rate: z + .number() + .int() + .min(0) + .optional() + .describe("Billable rate in cents (e.g. 15000 = EUR 150.00)"), + estimated_time: z.number().int().min(0).optional().describe("Estimated time in seconds"), + }, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: true, + }, + }, + async (params) => { + const body: Record = { + name: params.name, + color: params.color, + is_billable: params.is_billable, + client_id: params.client_id, + }; + if (params.billable_rate !== undefined) body.billable_rate = params.billable_rate; + if (params.estimated_time !== undefined) body.estimated_time = params.estimated_time; + + const result = await api.post<{ data: Project }>(API_PATHS.projects(orgId), body); + return { + content: [{ type: "text", text: `Project created.\n\n${formatProject(result.data)}` }], + }; + } + ); + + server.registerTool( + "solidtime_update_project", + { + title: "Update Project", + description: "Update an existing project. Only provided fields will be changed.", + inputSchema: { + id: z.string().uuid().describe("Project UUID to update"), + name: z.string().min(1).optional().describe("New project name"), + color: z + .string() + .regex(/^#[0-9a-f]{6}$/) + .optional() + .describe("New hex color, lowercase (e.g. #4caf50)"), + is_billable: coerceBoolean.optional().describe("New billable status"), + is_archived: coerceBoolean.optional().describe("Archive or unarchive"), + client_id: z.string().uuid().optional().describe("New client UUID"), + billable_rate: z.number().int().min(0).optional().describe("New billable rate in cents"), + estimated_time: z + .number() + .int() + .min(0) + .optional() + .describe("New estimated time in seconds"), + }, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true, + }, + }, + async (params) => { + // SolidTime API requires all fields on PUT, so fetch current state first + const current = await api.get<{ data: Project }>(API_PATHS.project(orgId, params.id)); + const project = current.data; + + const body: Record = { + name: params.name ?? project.name, + color: params.color ?? project.color, + is_billable: params.is_billable ?? project.is_billable, + client_id: params.client_id ?? project.client_id, + }; + if (params.is_archived !== undefined) body.is_archived = params.is_archived; + if (params.billable_rate !== undefined) body.billable_rate = params.billable_rate; + else if (project.billable_rate !== null) body.billable_rate = project.billable_rate; + if (params.estimated_time !== undefined) body.estimated_time = params.estimated_time; + else if (project.estimated_time !== null) body.estimated_time = project.estimated_time; + + const result = await api.put<{ data: Project }>(API_PATHS.project(orgId, params.id), body); + return { + content: [{ type: "text", text: `Project updated.\n\n${formatProject(result.data)}` }], + }; + } + ); + + server.registerTool( + "solidtime_delete_project", + { + title: "Delete Project", + description: "Permanently delete a project. This cannot be undone.", + inputSchema: { + id: z.string().uuid().describe("Project UUID to delete"), + }, + annotations: { + readOnlyHint: false, + destructiveHint: true, + idempotentHint: true, + openWorldHint: true, + }, + }, + async (params) => { + await api.delete(API_PATHS.project(orgId, params.id)); + return { content: [{ type: "text", text: `Project ${params.id} deleted.` }] }; + } + ); +} + +function formatProject(p: Project): string { + const parts = [ + `ID: ${p.id}`, + `Name: ${p.name}`, + `Color: ${p.color}`, + `Billable: ${p.is_billable ? "Yes" : "No"}`, + `Archived: ${p.is_archived ? "Yes" : "No"}`, + ]; + if (p.billable_rate !== null) parts.push(`Rate: ${formatCurrency(p.billable_rate)}/h`); + if (p.client_id) parts.push(`Client ID: ${p.client_id}`); + if (p.estimated_time !== null) parts.push(`Estimated: ${formatDuration(p.estimated_time)}`); + if (p.spent_time > 0) parts.push(`Spent: ${formatDuration(p.spent_time)}`); + return parts.join("\n"); +} diff --git a/src/tools/tags.ts b/src/tools/tags.ts new file mode 100644 index 0000000..ede3e4a --- /dev/null +++ b/src/tools/tags.ts @@ -0,0 +1,112 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { ApiClient } from "../api-client.js"; +import { API_PATHS } from "../constants.js"; +import type { Tag, PaginatedResponse } from "../types.js"; + +export function registerTagTools(server: McpServer, api: ApiClient, orgId: string) { + server.registerTool( + "solidtime_list_tags", + { + title: "List Tags", + description: "List all tags in the organization.", + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true, + }, + }, + async () => { + const result = await api.get>(API_PATHS.tags(orgId)); + const tags = result.data ?? []; + if (tags.length === 0) { + return { content: [{ type: "text", text: "No tags found." }] }; + } + + const lines = tags.map((t) => `ID: ${t.id}\nName: ${t.name}`); + lines.unshift(`Found ${tags.length} tags:\n`); + return { content: [{ type: "text", text: lines.join("\n\n") }] }; + } + ); + + server.registerTool( + "solidtime_create_tag", + { + title: "Create Tag", + description: "Create a new tag.", + inputSchema: { + name: z.string().min(1).describe("Tag name"), + }, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: true, + }, + }, + async (params) => { + const result = await api.post<{ data: Tag }>(API_PATHS.tags(orgId), { name: params.name }); + return { + content: [ + { + type: "text", + text: `Tag created.\n\nID: ${result.data.id}\nName: ${result.data.name}`, + }, + ], + }; + } + ); + + server.registerTool( + "solidtime_update_tag", + { + title: "Update Tag", + description: "Update a tag's name.", + inputSchema: { + id: z.string().uuid().describe("Tag UUID to update"), + name: z.string().min(1).describe("New tag name"), + }, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true, + }, + }, + async (params) => { + const result = await api.put<{ data: Tag }>(API_PATHS.tag(orgId, params.id), { + name: params.name, + }); + return { + content: [ + { + type: "text", + text: `Tag updated.\n\nID: ${result.data.id}\nName: ${result.data.name}`, + }, + ], + }; + } + ); + + server.registerTool( + "solidtime_delete_tag", + { + title: "Delete Tag", + description: "Permanently delete a tag. This cannot be undone.", + inputSchema: { + id: z.string().uuid().describe("Tag UUID to delete"), + }, + annotations: { + readOnlyHint: false, + destructiveHint: true, + idempotentHint: true, + openWorldHint: true, + }, + }, + async (params) => { + await api.delete(API_PATHS.tag(orgId, params.id)); + return { content: [{ type: "text", text: `Tag ${params.id} deleted.` }] }; + } + ); +} diff --git a/src/tools/tasks.ts b/src/tools/tasks.ts new file mode 100644 index 0000000..8b235a3 --- /dev/null +++ b/src/tools/tasks.ts @@ -0,0 +1,144 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { ApiClient } from "../api-client.js"; +import { API_PATHS } from "../constants.js"; +import { coerceBoolean } from "../schemas.js"; +import { formatDuration } from "../formatting.js"; +import type { Task, PaginatedResponse } from "../types.js"; + +export function registerTaskTools(server: McpServer, api: ApiClient, orgId: string) { + server.registerTool( + "solidtime_list_tasks", + { + title: "List Tasks", + description: "List tasks. Optionally filter by project or done status.", + inputSchema: { + project_id: z.string().uuid().optional().describe("Filter by project UUID"), + done: z.enum(["true", "false"]).optional().describe("Filter by done status"), + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true, + }, + }, + async (params) => { + const query = new URLSearchParams(); + if (params.project_id) query.set("project_id", params.project_id); + if (params.done) query.set("done", params.done); + const qs = query.toString(); + const path = qs ? `${API_PATHS.tasks(orgId)}?${qs}` : API_PATHS.tasks(orgId); + + const result = await api.get>(path); + const tasks = result.data ?? []; + if (tasks.length === 0) { + return { content: [{ type: "text", text: "No tasks found." }] }; + } + + const lines = tasks.map((t) => formatTask(t)); + lines.unshift(`Found ${tasks.length} tasks:\n`); + return { content: [{ type: "text", text: lines.join("\n\n") }] }; + } + ); + + server.registerTool( + "solidtime_create_task", + { + title: "Create Task", + description: "Create a new task within a project.", + inputSchema: { + name: z.string().min(1).describe("Task name"), + project_id: z.string().uuid().describe("Project UUID this task belongs to"), + estimated_time: z.number().int().min(0).optional().describe("Estimated time in seconds"), + }, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: true, + }, + }, + async (params) => { + const body: Record = { + name: params.name, + project_id: params.project_id, + }; + if (params.estimated_time !== undefined) body.estimated_time = params.estimated_time; + + const result = await api.post<{ data: Task }>(API_PATHS.tasks(orgId), body); + return { + content: [{ type: "text", text: `Task created.\n\n${formatTask(result.data)}` }], + }; + } + ); + + server.registerTool( + "solidtime_update_task", + { + title: "Update Task", + description: "Update a task's name, done status, or estimated time.", + inputSchema: { + id: z.string().uuid().describe("Task UUID to update"), + name: z.string().min(1).optional().describe("New task name"), + is_done: coerceBoolean.optional().describe("Mark task as done or not done"), + estimated_time: z + .number() + .int() + .min(0) + .optional() + .describe("New estimated time in seconds"), + }, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true, + }, + }, + async (params) => { + const body: Record = {}; + if (params.name !== undefined) body.name = params.name; + if (params.is_done !== undefined) body.is_done = params.is_done; + if (params.estimated_time !== undefined) body.estimated_time = params.estimated_time; + + const result = await api.put<{ data: Task }>(API_PATHS.task(orgId, params.id), body); + return { + content: [{ type: "text", text: `Task updated.\n\n${formatTask(result.data)}` }], + }; + } + ); + + server.registerTool( + "solidtime_delete_task", + { + title: "Delete Task", + description: "Permanently delete a task. This cannot be undone.", + inputSchema: { + id: z.string().uuid().describe("Task UUID to delete"), + }, + annotations: { + readOnlyHint: false, + destructiveHint: true, + idempotentHint: true, + openWorldHint: true, + }, + }, + async (params) => { + await api.delete(API_PATHS.task(orgId, params.id)); + return { content: [{ type: "text", text: `Task ${params.id} deleted.` }] }; + } + ); +} + +function formatTask(t: Task): string { + const parts = [ + `ID: ${t.id}`, + `Name: ${t.name}`, + `Done: ${t.is_done ? "Yes" : "No"}`, + `Project ID: ${t.project_id}`, + ]; + if (t.estimated_time !== null) parts.push(`Estimated: ${formatDuration(t.estimated_time)}`); + if (t.spent_time > 0) parts.push(`Spent: ${formatDuration(t.spent_time)}`); + return parts.join("\n"); +} diff --git a/src/tools/time-entries.ts b/src/tools/time-entries.ts new file mode 100644 index 0000000..c0e4c84 --- /dev/null +++ b/src/tools/time-entries.ts @@ -0,0 +1,478 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { ApiClient } from "../api-client.js"; +import { API_PATHS, GROUP_BY_VALUES, PAGINATION } from "../constants.js"; +import { coerceBoolean, coerceUuidArray } from "../schemas.js"; +import { formatDuration, formatCurrency, formatDateTime, toUTC, nowUTC } from "../formatting.js"; +import type { TimeEntry, TimeEntryReport, PaginatedResponse } from "../types.js"; + +export function registerTimeEntryTools( + server: McpServer, + api: ApiClient, + orgId: string, + getMemberId: () => string, + timezone?: string +) { + const tz = timezone; + const tzNote = tz + ? ` Times are displayed and accepted in ${tz}. Provide times without a timezone suffix (e.g. 2026-03-05T11:45:00) to use local time, or append Z for UTC.` + : " Times are in UTC."; + async function getActiveTimer(): Promise { + const response = await api.getOrNull<{ data: TimeEntry }>(API_PATHS.activeTimer); + return response?.data ?? null; + } + + // --- Get Active Timer --- + server.registerTool( + "solidtime_get_active_timer", + { + title: "Get Active Timer", + description: "Get the currently running timer, or null if no timer is active.", + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true, + }, + }, + async () => { + const entry = await getActiveTimer(); + if (!entry) { + return { content: [{ type: "text", text: "No active timer." }] }; + } + return { content: [{ type: "text", text: formatTimeEntry(entry, tz) }] }; + } + ); + + // --- Start Timer --- + server.registerTool( + "solidtime_start_timer", + { + title: "Start Timer", + description: + "Start a new running timer. Checks for an existing active timer first. Use solidtime_stop_timer to stop it later.", + inputSchema: { + project_id: z.string().uuid().optional().describe("Project UUID"), + task_id: z.string().uuid().optional().describe("Task UUID"), + description: z.string().max(5000).optional().describe("What you're working on"), + billable: coerceBoolean.optional().describe("Whether this time is billable"), + tag_ids: coerceUuidArray.optional().describe("Array of tag UUIDs to attach"), + }, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: true, + }, + }, + async (params) => { + const existing = await getActiveTimer(); + if (existing) { + return { + content: [ + { + type: "text", + text: `A timer is already running (started ${formatDateTime(existing.start, tz)}). Stop it first with solidtime_stop_timer.\n\n${formatTimeEntry(existing, tz)}`, + }, + ], + }; + } + + const body: Record = { + member_id: getMemberId(), + start: nowUTC(), + billable: params.billable ?? false, + }; + if (params.project_id) body.project_id = params.project_id; + if (params.task_id) body.task_id = params.task_id; + if (params.description) body.description = params.description; + if (params.tag_ids) body.tags = params.tag_ids; + + const result = await api.post<{ data: TimeEntry }>(API_PATHS.timeEntries(orgId), body); + return { + content: [{ type: "text", text: `Timer started.\n\n${formatTimeEntry(result.data, tz)}` }], + }; + } + ); + + // --- Stop Timer --- + server.registerTool( + "solidtime_stop_timer", + { + title: "Stop Timer", + description: "Stop the currently running timer by setting end time to now.", + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true, + }, + }, + async () => { + const existing = await getActiveTimer(); + if (!existing) { + return { content: [{ type: "text", text: "No active timer to stop." }] }; + } + + const result = await api.put<{ data: TimeEntry }>(API_PATHS.timeEntry(orgId, existing.id), { + end: nowUTC(), + }); + return { + content: [{ type: "text", text: `Timer stopped.\n\n${formatTimeEntry(result.data, tz)}` }], + }; + } + ); + + // --- List Time Entries --- + server.registerTool( + "solidtime_list_time_entries", + { + title: "List Time Entries", + description: + `List time entries with optional filters. Returns newest first. Use limit/offset for pagination.${tzNote}`, + inputSchema: { + start: z + .string() + .optional() + .describe( + "Filter: only entries starting after this datetime (e.g. 2026-03-01T00:00:00 for local or 2026-03-01T00:00:00Z for UTC)" + ), + end: z.string().optional().describe("Filter: only entries ending before this datetime"), + active: z.enum(["true", "false"]).optional().describe("Filter by active/inactive"), + billable: z.enum(["true", "false"]).optional().describe("Filter by billable status"), + project_ids: coerceUuidArray.optional().describe("Filter by project UUIDs"), + client_ids: coerceUuidArray.optional().describe("Filter by client UUIDs"), + task_ids: coerceUuidArray.optional().describe("Filter by task UUIDs"), + tag_ids: coerceUuidArray.optional().describe("Filter by tag UUIDs"), + limit: z + .number() + .int() + .min(1) + .max(PAGINATION.maxLimit) + .optional() + .describe( + `Number of entries to return (1-${PAGINATION.maxLimit}, default ${PAGINATION.defaultLimit})` + ), + offset: z.number().int().min(0).optional().describe("Offset for pagination (default 0)"), + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true, + }, + }, + async (params) => { + const query = new URLSearchParams(); + if (params.start) query.set("start", toUTC(params.start, tz)); + if (params.end) query.set("end", toUTC(params.end, tz)); + if (params.active) query.set("active", params.active); + if (params.billable) query.set("billable", params.billable); + if (params.project_ids) + params.project_ids.forEach((id) => query.append("filter[project_ids][]", id)); + if (params.client_ids) + params.client_ids.forEach((id) => query.append("filter[client_ids][]", id)); + if (params.task_ids) params.task_ids.forEach((id) => query.append("filter[task_ids][]", id)); + if (params.tag_ids) params.tag_ids.forEach((id) => query.append("filter[tag_ids][]", id)); + + const limit = params.limit ?? PAGINATION.defaultLimit; + const offset = params.offset ?? 0; + query.set("limit", String(limit)); + query.set("offset", String(offset)); + + const path = `${API_PATHS.timeEntries(orgId)}?${query.toString()}`; + const result = await api.get>(path); + + const entries = result.data ?? []; + if (entries.length === 0) { + return { content: [{ type: "text", text: "No time entries found." }] }; + } + + const hasMore = entries.length === limit; + const lines = entries.map((e, i) => `${i + 1 + offset}. ${formatTimeEntry(e, tz)}`); + if (hasMore) { + lines.push( + `\n--- Showing ${entries.length} entries. Use offset=${offset + limit} to see more. ---` + ); + } + lines.unshift(`Found ${entries.length} time entries:\n`); + + return { content: [{ type: "text", text: lines.join("\n") }] }; + } + ); + + // --- Create Time Entry --- + server.registerTool( + "solidtime_create_time_entry", + { + title: "Create Time Entry", + description: + `Create a completed time entry with start and end times. For running timers, use solidtime_start_timer instead.${tzNote}`, + inputSchema: { + start: z.string().describe("Start time (e.g. 2026-03-03T09:00:00 for local or 2026-03-03T09:00:00Z for UTC)"), + end: z.string().describe("End time (e.g. 2026-03-03T10:30:00 for local or 2026-03-03T10:30:00Z for UTC)"), + project_id: z.string().uuid().optional().describe("Project UUID"), + task_id: z.string().uuid().optional().describe("Task UUID"), + description: z.string().max(5000).optional().describe("What was done"), + billable: coerceBoolean + .optional() + .describe("Whether this time is billable (default false)"), + tag_ids: coerceUuidArray.optional().describe("Array of tag UUIDs to attach"), + }, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: true, + }, + }, + async (params) => { + const body: Record = { + member_id: getMemberId(), + start: toUTC(params.start, tz), + end: toUTC(params.end, tz), + billable: params.billable ?? false, + }; + if (params.project_id) body.project_id = params.project_id; + if (params.task_id) body.task_id = params.task_id; + if (params.description) body.description = params.description; + if (params.tag_ids) body.tags = params.tag_ids; + + const result = await api.post<{ data: TimeEntry }>(API_PATHS.timeEntries(orgId), body); + return { + content: [{ type: "text", text: `Time entry created.\n\n${formatTimeEntry(result.data, tz)}` }], + }; + } + ); + + // --- Update Time Entry --- + server.registerTool( + "solidtime_update_time_entry", + { + title: "Update Time Entry", + description: `Update an existing time entry. Only provided fields will be changed.${tzNote}`, + inputSchema: { + id: z.string().uuid().describe("Time entry UUID to update"), + start: z.string().optional().describe("New start time (no suffix = local, Z suffix = UTC)"), + end: z.string().optional().describe("New end time (no suffix = local, Z suffix = UTC)"), + project_id: z.string().uuid().optional().describe("New project UUID"), + task_id: z.string().uuid().optional().describe("New task UUID"), + description: z.string().max(5000).optional().describe("New description"), + billable: coerceBoolean.optional().describe("New billable status"), + tag_ids: z + .array(z.string().uuid()) + .optional() + .describe("New tag UUIDs (replaces existing tags)"), + }, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true, + }, + }, + async (params) => { + const body: Record = {}; + if (params.start !== undefined) body.start = toUTC(params.start, tz); + if (params.end !== undefined) body.end = toUTC(params.end, tz); + if (params.project_id !== undefined) body.project_id = params.project_id; + if (params.task_id !== undefined) body.task_id = params.task_id; + if (params.description !== undefined) body.description = params.description; + if (params.billable !== undefined) body.billable = params.billable; + if (params.tag_ids !== undefined) body.tags = params.tag_ids; + + const result = await api.put<{ data: TimeEntry }>( + API_PATHS.timeEntry(orgId, params.id), + body + ); + return { + content: [{ type: "text", text: `Time entry updated.\n\n${formatTimeEntry(result.data, tz)}` }], + }; + } + ); + + // --- Delete Time Entry --- + server.registerTool( + "solidtime_delete_time_entry", + { + title: "Delete Time Entry", + description: "Permanently delete a time entry. This cannot be undone.", + inputSchema: { + id: z.string().uuid().describe("Time entry UUID to delete"), + }, + annotations: { + readOnlyHint: false, + destructiveHint: true, + idempotentHint: true, + openWorldHint: true, + }, + }, + async (params) => { + await api.delete(API_PATHS.timeEntry(orgId, params.id)); + return { content: [{ type: "text", text: `Time entry ${params.id} deleted.` }] }; + } + ); + + // --- Time Entry Report --- + server.registerTool( + "solidtime_get_time_entry_report", + { + title: "Time Entry Report", + description: + "Get aggregated time entry data grouped by a dimension (day, week, month, year, project, client, task, user, billable, description, tag). Optionally add a sub-grouping.", + inputSchema: { + group_by: z.enum(GROUP_BY_VALUES).describe("Primary grouping dimension"), + sub_group: z.enum(GROUP_BY_VALUES).optional().describe("Secondary grouping dimension"), + start: z + .string() + .optional() + .describe("Filter: only entries starting after this UTC datetime"), + end: z.string().optional().describe("Filter: only entries ending before this UTC datetime"), + project_ids: coerceUuidArray.optional().describe("Filter by project UUIDs"), + client_ids: coerceUuidArray.optional().describe("Filter by client UUIDs"), + billable: z.enum(["true", "false"]).optional().describe("Filter by billable status"), + tag_ids: coerceUuidArray.optional().describe("Filter by tag UUIDs"), + member_ids: coerceUuidArray.optional().describe("Filter by member UUIDs"), + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true, + }, + }, + async (params) => { + const query = new URLSearchParams(); + query.set("group", params.group_by); + if (params.sub_group) query.set("sub_group", params.sub_group); + if (params.start) query.set("start", toUTC(params.start, tz)); + if (params.end) query.set("end", toUTC(params.end, tz)); + if (params.billable) query.set("billable", params.billable); + if (params.project_ids) + params.project_ids.forEach((id) => query.append("filter[project_ids][]", id)); + if (params.client_ids) + params.client_ids.forEach((id) => query.append("filter[client_ids][]", id)); + if (params.tag_ids) params.tag_ids.forEach((id) => query.append("filter[tag_ids][]", id)); + if (params.member_ids) + params.member_ids.forEach((id) => query.append("filter[member_ids][]", id)); + + const path = `${API_PATHS.timeEntryReport(orgId)}?${query.toString()}`; + const response = await api.get<{ data: TimeEntryReport }>(path); + const result = response.data; + + if (!result.grouped_data || result.grouped_data.length === 0) { + return { content: [{ type: "text", text: "No data for this report." }] }; + } + + const lines = [`Report grouped by: ${result.grouped_type}\n`]; + for (const group of result.grouped_data) { + const duration = formatDuration(group.seconds); + const cost = group.cost > 0 ? ` (${formatCurrency(group.cost)})` : ""; + lines.push(`${group.key}: ${duration}${cost}`); + + if (group.grouped_data) { + for (const sub of group.grouped_data) { + const subDuration = formatDuration(sub.seconds); + const subCost = sub.cost > 0 ? ` (${formatCurrency(sub.cost)})` : ""; + lines.push(` ${sub.key}: ${subDuration}${subCost}`); + } + } + } + + return { content: [{ type: "text", text: lines.join("\n") }] }; + } + ); + + // --- Update Multiple Time Entries --- + server.registerTool( + "solidtime_update_multiple_time_entries", + { + title: "Update Multiple Time Entries", + description: + "Update multiple time entries at once. All provided fields will be applied to all specified entries.", + inputSchema: { + ids: z.array(z.string().uuid()).min(1).describe("Array of time entry UUIDs to update"), + project_id: z.string().uuid().optional().describe("New project UUID for all entries"), + task_id: z.string().uuid().optional().describe("New task UUID for all entries"), + description: z.string().max(5000).optional().describe("New description for all entries"), + billable: coerceBoolean.optional().describe("New billable status for all entries"), + tag_ids: coerceUuidArray.optional().describe("New tag UUIDs for all entries"), + }, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true, + }, + }, + async (params) => { + const body: Record = { + ids: params.ids, + changes: {}, + }; + const changes: Record = {}; + if (params.project_id !== undefined) changes.project_id = params.project_id; + if (params.task_id !== undefined) changes.task_id = params.task_id; + if (params.description !== undefined) changes.description = params.description; + if (params.billable !== undefined) changes.billable = params.billable; + if (params.tag_ids !== undefined) changes.tags = params.tag_ids; + body.changes = changes; + + await api.patch(API_PATHS.timeEntries(orgId), body); + return { + content: [ + { type: "text", text: `Updated ${params.ids.length} time entries.` }, + ], + }; + } + ); + + // --- Delete Multiple Time Entries --- + server.registerTool( + "solidtime_delete_multiple_time_entries", + { + title: "Delete Multiple Time Entries", + description: + "Permanently delete multiple time entries at once. This cannot be undone.", + inputSchema: { + ids: z + .array(z.string().uuid()) + .min(1) + .describe("Array of time entry UUIDs to delete"), + }, + annotations: { + readOnlyHint: false, + destructiveHint: true, + idempotentHint: true, + openWorldHint: true, + }, + }, + async (params) => { + await api.deleteWithBody(API_PATHS.timeEntries(orgId), { ids: params.ids }); + return { + content: [ + { type: "text", text: `Deleted ${params.ids.length} time entries.` }, + ], + }; + } + ); +} + +function formatTimeEntry(entry: TimeEntry, timezone?: string): string { + const parts: string[] = [`ID: ${entry.id}`]; + if (entry.description) parts.push(`Description: ${entry.description}`); + parts.push(`Start: ${formatDateTime(entry.start, timezone)}`); + if (entry.end) { + parts.push(`End: ${formatDateTime(entry.end, timezone)}`); + } else { + parts.push("Status: RUNNING"); + } + if (entry.duration !== null && entry.duration !== undefined) { + parts.push(`Duration: ${formatDuration(entry.duration)}`); + } + parts.push(`Billable: ${entry.billable ? "Yes" : "No"}`); + if (entry.project_id) parts.push(`Project ID: ${entry.project_id}`); + if (entry.task_id) parts.push(`Task ID: ${entry.task_id}`); + if (entry.tags && entry.tags.length > 0) parts.push(`Tags: ${entry.tags.join(", ")}`); + return parts.join("\n"); +} diff --git a/src/tools/users.ts b/src/tools/users.ts new file mode 100644 index 0000000..9217b80 --- /dev/null +++ b/src/tools/users.ts @@ -0,0 +1,35 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { ApiClient } from "../api-client.js"; +import { API_PATHS } from "../constants.js"; +import type { User } from "../types.js"; + +export function registerUserTools(server: McpServer, api: ApiClient, getMemberId: () => string) { + server.registerTool( + "solidtime_get_current_user", + { + title: "Get Current User", + description: + "Get the current user profile including name, email, timezone, and the resolved member_id for this organization.", + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true, + }, + }, + async () => { + const response = await api.get<{ data: User }>(API_PATHS.me); + const user = response.data; + const memberId = getMemberId(); + const text = [ + `Name: ${user.name}`, + `Email: ${user.email}`, + `Timezone: ${user.timezone}`, + `Week starts: ${user.week_start}`, + `Member ID: ${memberId}`, + ].join("\n"); + + return { content: [{ type: "text", text }] }; + } + ); +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..6b42e4d --- /dev/null +++ b/src/types.ts @@ -0,0 +1,117 @@ +export interface User { + id: string; + name: string; + email: string; + timezone: string; + week_start: string; + is_placeholder: boolean; +} + +export interface Member { + id: string; + user_id: string; + role: string; + billable_rate: number | null; +} + +export interface TimeEntry { + id: string; + start: string; + end: string | null; + duration: number | null; + description: string | null; + billable: boolean; + tags: string[]; + project_id: string | null; + task_id: string | null; + member_id: string; + user_id: string; +} + +export interface TimeEntryReport { + grouped_type: string; + grouped_data: + | null + | { + key: string; + seconds: number; + cost: number; + grouped_type: string | null; + grouped_data: null | { key: string; seconds: number; cost: number }[]; + }[]; +} + +export interface Project { + id: string; + name: string; + color: string; + is_billable: boolean; + is_archived: boolean; + billable_rate: number | null; + estimated_time: number | null; + spent_time: number; + client_id: string | null; +} + +export interface Client { + id: string; + name: string; + is_archived: boolean; +} + +export interface Tag { + id: string; + name: string; +} + +export interface Task { + id: string; + name: string; + is_done: boolean; + project_id: string; + estimated_time: number | null; + spent_time: number; +} + +export interface PaginatedResponse { + data: T[]; + current_page?: number; + last_page?: number; + per_page?: number; + total?: number; +} + +export interface UserMembership { + id: string; + role: string; + organization: { + id: string; + name: string; + currency: string; + }; +} + +export interface Organization { + id: string; + name: string; + currency: string; + billable_rate: number | null; +} + +export interface ProjectMember { + id: string; + member_id: string; + project_id: string; + billable_rate: number | null; +} + +export interface Invitation { + id: string; + email: string; + role: string; +} + +export interface ApiError { + message: string; + errors?: Record; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..2edf508 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +}