From 644557dafb38240aadf034133d2308a7f615fe14 Mon Sep 17 00:00:00 2001 From: userFRM Date: Wed, 24 Jun 2026 21:21:16 +0200 Subject: [PATCH] feat!: rebuild as a generic Kimi CLI wrapper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the 14-tool file/shell/web passthrough surface — which duplicated capabilities the MCP client already has — with a thin, generic wrapper over the Kimi CLI's headless print mode. Tools: kimi_run (send a prompt, optional working directory), kimi_resume (continue a session by id), kimi_status (installed binary, version, auth health). The runner classifies Kimi's documented exit codes (0 ok, 1 permanent, 75 transient), retries transient failures with backoff, enforces a wall-clock timeout via a process-group kill, and detects quota/balance failures even when the CLI exits 0. Binary resolution prefers the native installer path (~/.kimi-code/bin/kimi) and falls back to the legacy uv install and PATH. Auth detection covers OAuth credentials, kimi.json, and an api_key in config.toml. Tool auto-approval (-y) is off by default and opt-in per-call (yolo) or via KIMI_YOLO, so an unsupervised agent cannot mutate a repo or run shell without consent. BREAKING CHANGE: all previous tools are removed and replaced. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yml | 8 +- .github/workflows/publish.yml | 25 + CHANGELOG.md | 30 + CLAUDE.md | 59 -- README.md | 104 ++- package-lock.json | 1267 ++++++++++++++++++++------------ package.json | 44 +- server.json | 20 + src/__tests__/cli.test.ts | 204 ----- src/__tests__/handlers.test.ts | 138 ---- src/__tests__/mocks.ts | 49 -- src/__tests__/parser.test.ts | 138 ---- src/__tests__/schema.test.ts | 35 - src/bin-resolver.test.ts | 108 +++ src/bin-resolver.ts | 153 ++++ src/cli.ts | 159 ---- src/config.ts | 59 ++ src/handlers.ts | 287 -------- src/index.ts | 113 +-- src/parser.ts | 113 --- src/runner.test.ts | 169 +++++ src/runner.ts | 270 +++++++ src/server.test.ts | 64 ++ src/server.ts | 33 + src/stream.test.ts | 50 ++ src/stream.ts | 96 +++ src/tools/index.ts | 95 +++ src/tools/shared.ts | 45 ++ tsconfig.json | 15 +- vitest.config.ts | 4 +- 30 files changed, 2125 insertions(+), 1829 deletions(-) create mode 100644 .github/workflows/publish.yml create mode 100644 CHANGELOG.md delete mode 100644 CLAUDE.md create mode 100644 server.json delete mode 100644 src/__tests__/cli.test.ts delete mode 100644 src/__tests__/handlers.test.ts delete mode 100644 src/__tests__/mocks.ts delete mode 100644 src/__tests__/parser.test.ts delete mode 100644 src/__tests__/schema.test.ts create mode 100644 src/bin-resolver.test.ts create mode 100644 src/bin-resolver.ts delete mode 100644 src/cli.ts create mode 100644 src/config.ts delete mode 100644 src/handlers.ts delete mode 100644 src/parser.ts create mode 100644 src/runner.test.ts create mode 100644 src/runner.ts create mode 100644 src/server.test.ts create mode 100644 src/server.ts create mode 100644 src/stream.test.ts create mode 100644 src/stream.ts create mode 100644 src/tools/index.ts create mode 100644 src/tools/shared.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 67191fb..e45e4fa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,20 +9,18 @@ on: jobs: build: runs-on: ubuntu-latest - strategy: + fail-fast: false matrix: - node-version: [20.x, 22.x] - + node-version: [20.x, 22.x, 24.x] steps: - uses: actions/checkout@v4 - - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} cache: npm - - run: npm ci + - run: npm run typecheck - run: npm run build - run: npm test diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..b992c05 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,25 @@ +name: Publish + +on: + release: + types: [published] + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write # npm provenance + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22.x + registry-url: https://registry.npmjs.org + cache: npm + - run: npm ci + - run: npm run build + - run: npm test + - run: npm publish --provenance --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8c7197d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,30 @@ +# Changelog + +All notable changes to this project are documented here. The format follows +[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres +to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.1.0] + +Complete rewrite. The previous line mirrored Kimi's internal file/shell/web +tools (14 of them), duplicating capabilities the MCP client already has. This +release replaces that with a thin, generic wrapper over the Kimi CLI. + +### Added + +- `kimi_run` — send a prompt to the Kimi CLI agent (headless print mode) and + return its final answer; optionally scoped to a working directory. +- `kimi_resume` — continue a prior session by id. +- `kimi_status` — installation, binary origin, version, and auth health check. +- Native-binary detection: `~/.kimi-code/bin/kimi` (from the official + installer) preferred, with legacy `~/.local/bin/kimi` and `$PATH` fallbacks. +- Outcome classification on Kimi's documented exit codes (0/1/75), with + retry-and-backoff for transient failures and a process-group timeout kill. +- Quota/balance failure detection even when the CLI exits `0`. +- Safe-by-default tool approval: `-y`/--yolo is OFF unless opted in per-call + (`yolo`) or via `KIMI_YOLO`, so an unsupervised Kimi cannot mutate a repo or + run shell without explicit consent. + +### Removed + +- All 14 file/shell/web passthrough tools from the previous design. diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 83a81fd..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,59 +0,0 @@ -# CLAUDE.md - -## Project Overview - -`kimi-code-mcp` is an MCP (Model Context Protocol) server that wraps the [Kimi Code CLI](https://github.com/MoonshotAI/kimi-cli) (kimi-k2.5). It exposes 14 tools over stdio for filesystem, shell, web, and agent operations. - -## Architecture - -4 source files, no external runtime dependencies beyond `@modelcontextprotocol/sdk` and `zod`: - -``` -src/ - index.ts — MCP server entry point (shebang, stdio transport, zodToJsonSchema, request handlers) - handlers.ts — TOOLS array (14 tool definitions with Zod schemas), TOOL_MAP, handleToolCall, buildPrompt - cli.ts — findKimiCli, buildKimiArgs, executeKimi (spawns kimi CLI as child process) - parser.ts — parseKimiOutput, extractTextParts, extractThinkParts, extractPlainText -``` - -**Data flow:** MCP request → `index.ts` validates with Zod → `handleToolCall` in `handlers.ts` builds a natural language prompt → `executeKimi` in `cli.ts` spawns `kimi --print -y -p ` → `parser.ts` extracts TextPart/ThinkPart from CLI output → result returned as MCP text content. - -## Key Design Decisions - -- **No shell execution in spawn**: `cli.ts` uses `spawn(binary, argsArray)` with no `shell: true` — prevents command injection. -- **Single-pass regex for unescaping**: `parser.ts` uses `str.replace(/\\(\\|n|'|")/g, ...)` instead of chained `.replace()` calls to avoid ordering bugs (e.g., `\\n` must stay as literal backslash-n, not become a newline). -- **Timeout double-reject guard**: `executeKimi` uses a `killed` flag so the `close` event handler no-ops after a timeout kill. -- **`zodToJsonSchema` is inlined**: A minimal Zod-to-JSON-Schema converter lives in `index.ts` — handles string, number, boolean, enum, optional, and `.describe()`. No need for a full library. -- **`KIMI_CLI_PATH` must be absolute**: Uses `path.isAbsolute()` to reject relative paths. - -## Build & Test - -```bash -npm install -npm run build # tsc → dist/ -npm test # vitest run (68 tests) -``` - -Tests live in `src/__tests__/`. Test files are excluded from `tsconfig.json` and the npm tarball. - -## Environment Variables - -- `KIMI_CLI_PATH` — absolute path to the kimi binary (default: `kimi` from PATH) -- `MCP_KIMI_DEBUG` — set to any value to enable debug logging to stderr - -## Contributing - -- **Keep it minimal** — this repo intentionally has 4 source files. Don't add files unless strictly necessary. Prefer editing existing files over creating new ones. -- **No over-engineering** — no classes, no decorators, no abstractions for one-time operations. Plain functions and const arrays. -- **Tests required** — every new or changed export must have tests in `src/__tests__/`. Run `npm test` before submitting. -- **No new runtime dependencies** — the only runtime deps are `@modelcontextprotocol/sdk` and `zod`. Think hard before adding another. -- **Security matters** — never use `shell: true` in spawn, never interpolate user input into shell commands, validate all paths. -- **Adding a new tool** — add the tool definition to the `TOOLS` array in `handlers.ts`, add its prompt builder case to `buildPrompt` in the same file, then add tests in `src/__tests__/handlers.test.ts`. Update the tool count in `src/__tests__/schema.test.ts`. - -## Conventions - -- TypeScript strict mode, ES2022 target, NodeNext module resolution -- ESM only (`"type": "module"` in package.json, `.js` extensions in imports) -- No decorators, no classes — plain functions and const arrays -- Tests use vitest with `vi.mock` for child_process and fs mocking -- Commit messages should not mention AI authorship diff --git a/README.md b/README.md index 7086d46..d3ce59c 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,58 @@ # kimi-code-mcp [![npm version](https://img.shields.io/npm/v/kimi-code-mcp.svg)](https://www.npmjs.com/package/kimi-code-mcp) -[![CI](https://github.com/userfrm/kimi-code-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/userfrm/kimi-code-mcp/actions/workflows/ci.yml) +[![CI](https://github.com/userFRM/kimi-code-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/userFRM/kimi-code-mcp/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -MCP server wrapping [Kimi Code CLI](https://github.com/MoonshotAI/kimi-cli) (kimi-k2.5) — 14 tools for filesystem, shell, web, and agent operations. +An [MCP](https://modelcontextprotocol.io) server that exposes the [Kimi CLI](https://github.com/MoonshotAI/kimi-cli) as a tool for Claude (or any MCP client). Send a prompt to Kimi's agent and get its answer back — a thin, generic wrapper around the CLI's headless mode. No prompt engineering, no opinions: you supply the prompt, Kimi runs it. ## Prerequisites -- Node.js >= 18 -- [Kimi CLI](https://github.com/MoonshotAI/kimi-cli) installed and authenticated +- Node.js >= 20 +- The Kimi CLI, installed and authenticated: -## Usage with Claude Desktop +```bash +curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash +kimi login +``` + +The server auto-detects the binary in this order: `$KIMI_BIN` → `~/.kimi-code/bin/kimi` (native installer) → `~/.local/bin/kimi` (legacy `uv` install) → `kimi` on `$PATH`. + +## Install + +```bash +npm install -g kimi-code-mcp +``` + +Or run on demand with `npx kimi-code-mcp` (used in the configs below). -Add to your Claude Desktop configuration: +### Claude Code / Claude Desktop + +Add to `~/.claude/mcp.json` (global) or a project `.mcp.json`: ```json { "mcpServers": { "kimi": { "command": "npx", - "args": ["-y", "kimi-code-mcp@latest"] + "args": ["-y", "kimi-code-mcp"] } } } ``` -## Usage with VS Code - -Install the Kimi Code MCP server in VS Code: +Run `/mcp` in Claude Code to verify — you should see `kimi` with three tools. -[Install in VS Code](https://insiders.vscode.dev/redirect/mcp/install?name=kimi&inputs=%5B%5D&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22kimi-code-mcp%40latest%22%5D%7D) +### VS Code -Or add to `.vscode/mcp.json`: +Add to `.vscode/mcp.json`: ```json { "servers": { "kimi": { "command": "npx", - "args": ["-y", "kimi-code-mcp@latest"] + "args": ["-y", "kimi-code-mcp"] } } } @@ -47,34 +60,51 @@ Or add to `.vscode/mcp.json`: ## Tools -| Tool | Description | -|------|-------------| -| `kimi_read_file` | Read a text file (up to 1000 lines) | -| `kimi_read_media` | Analyze images and videos | -| `kimi_write_file` | Create or overwrite a file | -| `kimi_edit_file` | Find-and-replace edit in a file | -| `kimi_glob` | Find files matching a glob pattern | -| `kimi_grep` | Search for regex patterns in files | -| `kimi_shell` | Execute shell commands | -| `kimi_web_search` | Search the web (up to 20 results) | -| `kimi_fetch_url` | Fetch and extract webpage content | -| `kimi_agent` | Autonomous agent for complex tasks | -| `kimi_think` | Extended reasoning and analysis | -| `kimi_review` | Code review (bugs, security, perf, style) | -| `kimi_research` | Research with 256K context window | -| `kimi_test` | Generate or fix tests with edge cases | - -## Environment Variables - -| Variable | Description | Default | -|----------|-------------|---------| -| `KIMI_CLI_PATH` | Absolute path to kimi binary | `kimi` (from PATH) | -| `MCP_KIMI_DEBUG` | Enable debug logging to stderr | unset | +| Tool | What it does | +|------|--------------| +| `kimi_run` | Send a prompt to Kimi (headless print mode) and return its final answer. Optional working directory so Kimi can read a codebase. | +| `kimi_resume` | Continue a previous session by id (returned in earlier output) with a follow-up prompt. | +| `kimi_status` | Is Kimi installed, which binary (native vs legacy), what version, authenticated? Call this first when other tools fail. | + +### `kimi_run` parameters + +| Param | Type | Notes | +|-------|------|-------| +| `prompt` | string (required) | The prompt to send to Kimi. | +| `path` | string | Working directory Kimi runs in. Defaults to the server cwd. | +| `model` | string | Override the Kimi model. | +| `thinking` | boolean | Enable extended thinking. | +| `yolo` | boolean | Auto-approve Kimi's tool actions so it can edit files / run shell unsupervised. **Default off** — opt in per-call or via `KIMI_YOLO`. | + +> **Safety:** without `yolo`, Kimi answers and reads but will not be auto-approved to mutate your repo or run shell. Turn it on only when you want an autonomous agent acting on the working directory. + +## Environment variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `KIMI_BIN` / `KIMI_CLI_PATH` | auto-detect | Absolute path to the kimi binary. | +| `KIMI_MODEL` | CLI default | Default model passed to `kimi -m`. | +| `KIMI_TIMEOUT_MS` | `600000` | Wall-clock budget per invocation. | +| `KIMI_MAX_OUTPUT_CHARS` | `120000` | Output cap; truncated at a clean boundary beyond it. | +| `KIMI_MAX_RETRIES` | `2` | Retries for transient (exit 75 / rate-limit) failures. | +| `KIMI_RETRY_BACKOFF_MS` | `2000` | Base backoff, doubled each attempt. | +| `KIMI_YOLO` | `false` | Default auto-approval of Kimi's tool actions (per-call `yolo` overrides). | +| `MCP_KIMI_DEBUG` | unset | Mirror Kimi's stderr to this process's stderr. | + +## How it works + +``` +Claude ──tool call──▶ kimi-code-mcp ──spawn──▶ kimi --print --output-format text + --final-message-only -y -w -m + ◀──answer─── ◀──stdout── +``` + +The server spawns Kimi in print (headless) mode, classifies the outcome against Kimi's documented exit codes (`0` ok, `1` permanent, `75` transient), retries transient failures with backoff, enforces a wall-clock timeout with a process-group kill, detects quota/balance failures even when the CLI exits `0`, and returns the final assistant message (with a resume `session` id when one is available). ## Development ```bash -git clone https://github.com/userfrm/kimi-code-mcp.git +git clone https://github.com/userFRM/kimi-code-mcp.git cd kimi-code-mcp npm install npm run build diff --git a/package-lock.json b/package-lock.json index 12f4e66..627fc44 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,25 +1,25 @@ { "name": "kimi-code-mcp", - "version": "1.0.0", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "kimi-code-mcp", - "version": "1.0.0", + "version": "0.1.0", "license": "MIT", "dependencies": { - "@modelcontextprotocol/sdk": "^1.11.2", - "zod": "^3.24.4" + "@modelcontextprotocol/sdk": "^1.29.0", + "zod": "^4.4.3" }, "bin": { "kimi-code-mcp": "dist/index.js" }, "devDependencies": { - "@types/node": "^22.15.17", - "@vitest/coverage-v8": "^2.1.8", - "typescript": "^5.8.3", - "vitest": "^2.1.8" + "@types/node": "^24.0.0", + "@vitest/coverage-v8": "^3.2.4", + "typescript": "^5.9.0", + "vitest": "^3.2.4" }, "engines": { "node": ">=20.0.0" @@ -40,9 +40,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", "engines": { @@ -50,9 +50,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", "engines": { @@ -60,13 +60,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", - "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.29.0" + "@babel/types": "^7.29.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -76,30 +76,33 @@ } }, "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", "cpu": [ "ppc64" ], @@ -110,13 +113,13 @@ "aix" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", "cpu": [ "arm" ], @@ -127,13 +130,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", "cpu": [ "arm64" ], @@ -144,13 +147,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", "cpu": [ "x64" ], @@ -161,13 +164,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", "cpu": [ "arm64" ], @@ -178,13 +181,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", "cpu": [ "x64" ], @@ -195,13 +198,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", "cpu": [ "arm64" ], @@ -212,13 +215,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", "cpu": [ "x64" ], @@ -229,13 +232,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", "cpu": [ "arm" ], @@ -246,13 +249,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", "cpu": [ "arm64" ], @@ -263,13 +266,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", "cpu": [ "ia32" ], @@ -280,13 +283,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", "cpu": [ "loong64" ], @@ -297,13 +300,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", "cpu": [ "mips64el" ], @@ -314,13 +317,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", "cpu": [ "ppc64" ], @@ -331,13 +334,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", "cpu": [ "riscv64" ], @@ -348,13 +351,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", "cpu": [ "s390x" ], @@ -365,13 +368,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", "cpu": [ "x64" ], @@ -382,13 +385,30 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", "cpu": [ "x64" ], @@ -399,13 +419,30 @@ "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", "cpu": [ "x64" ], @@ -416,13 +453,30 @@ "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", "cpu": [ "x64" ], @@ -433,13 +487,13 @@ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", "cpu": [ "arm64" ], @@ -450,13 +504,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", "cpu": [ "ia32" ], @@ -467,13 +521,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", "cpu": [ "x64" ], @@ -484,13 +538,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@hono/node-server": { - "version": "1.19.9", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", - "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", "license": "MIT", "engines": { "node": ">=18.14.1" @@ -518,9 +572,9 @@ } }, "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", "dev": true, "license": "MIT", "engines": { @@ -567,9 +621,9 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.25.3", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.3.tgz", - "integrity": "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ==", + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", "license": "MIT", "dependencies": { "@hono/node-server": "^1.19.9", @@ -580,14 +634,15 @@ "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", - "express": "^5.0.1", - "express-rate-limit": "^7.5.0", - "jose": "^6.1.1", + "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.0" + "zod-to-json-schema": "^3.25.1" }, "engines": { "node": ">=18" @@ -617,9 +672,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", - "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.61.0.tgz", + "integrity": "sha512-dnxczajOqt0gesZlN5pGQ1s1imQVrsmCw5G2Ci4oM+0WvNz3pyRnlWrT7McoZIb8VlFwCawdmbWRmxRn7HI+VQ==", "cpu": [ "arm" ], @@ -631,9 +686,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", - "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.61.0.tgz", + "integrity": "sha512-Bp3JpGP00Vu3f238ivRrjf7z3xSzVPXqCmaJYA9t2c+c8vKYvOzmXF7LkkeUalTEGd6cZcSWe+PFIP3Vy48fRg==", "cpu": [ "arm64" ], @@ -645,9 +700,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", - "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.61.0.tgz", + "integrity": "sha512-zaYIpr670mUmmZ1tVzUFplbQbG7h3Gugx3L5FoqhsC2m/YnLlR1a7zVLmXNPy+iY1tFPEbNG+HHBXZGyId0G5w==", "cpu": [ "arm64" ], @@ -659,9 +714,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", - "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.61.0.tgz", + "integrity": "sha512-+P49fvkv2dSoeevUW+lgZ/I2JHSsJCK1Lyjj7Cu6E4UHG4tS9XIefzIjo5qhgELjAclnen1rLzK2PMKJdo+Dyg==", "cpu": [ "x64" ], @@ -673,9 +728,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", - "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.61.0.tgz", + "integrity": "sha512-l3FAAOyKJXH2ea6KNFN+MMgC/rnE94YGLXs2ehYqDcCoHt1DpvgWX75BhUJxN38XojP7Ul+4H8PRn7EdyqSDrw==", "cpu": [ "arm64" ], @@ -687,9 +742,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", - "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.61.0.tgz", + "integrity": "sha512-VokPN3TSctKj65cyCNPaUh4vMFA8awxOot/0sp+4J7ZlNRKQEhXhawqPwajoi8H5ZFt61i0ugZJuTKXBjGJ17Q==", "cpu": [ "x64" ], @@ -701,13 +756,16 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", - "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.61.0.tgz", + "integrity": "sha512-DxH0P3wxm+Yzs/p3zrk9dw1rURu8p0Nv5+MRK/L7OtnLNg5rLZraSBFZ8iUXOd9f2BlhJyEpIZUH/emjq4UJ4g==", "cpu": [ "arm" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -715,13 +773,16 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", - "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.61.0.tgz", + "integrity": "sha512-T6ZvMNe84kAz6TBWHC7hGAoEtzP1LWYw/AqayGWEF6uISt3Abk/st06LqRD9THd7Xz3NxzurUpzAuEAUbZf+nw==", "cpu": [ "arm" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -729,13 +790,16 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", - "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.61.0.tgz", + "integrity": "sha512-q/4hzvQkDs8b4jIBab1pnLiiM0ayTZsN2amBFPDzuyZxjEd4wDwx0UJFYM3cOZzSf5Kw8fnWSprJzIBMkcR44Q==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -743,13 +807,16 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", - "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.61.0.tgz", + "integrity": "sha512-vvYWX3akdEAY6km+9wAqFDnk6pQsbJKVnj7xawcvs/+fdlYBGp+U+Qq/lLfpIxYIZvZLHMAKD9HLdacSx/r3dw==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -757,13 +824,16 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", - "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.61.0.tgz", + "integrity": "sha512-DePa5cqOxDP/Zp0VOXpeWaGew5iIv5DXp9NYbzkX5PFQyWVX9184WCTh3hvr/7lhXo8ZVlbFLkz8+o/q1dU6gA==", "cpu": [ "loong64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -771,13 +841,16 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", - "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.61.0.tgz", + "integrity": "sha512-LV8aWMB8UChglMCEzs7RkN0GsH29RJaLLqwm9fCIjlqwxQTiWAqNcc7wjBkH31hV0PU/yVxGYvrYsgfea2qw6g==", "cpu": [ "loong64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -785,13 +858,16 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", - "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.61.0.tgz", + "integrity": "sha512-QoNSnwQtaeNu5grdBbsL0tt1uyl5EnS8DA8Mr3nluMXbhdQNyhN+G4tBax7VCdxLKj8YJ0/4OO9Ho84jMnJtKA==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -799,13 +875,16 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", - "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.61.0.tgz", + "integrity": "sha512-/zZp5MKapIIApE8trN8qLGNSiRN9TUoaUZ1cmVu4XnVdd5LQLOXTtyi+vtfUbNnT3iyjzpPqYeKXmvJ+gJGYWw==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -813,13 +892,16 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", - "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.61.0.tgz", + "integrity": "sha512-RbrzcD3aJ1k3UbtMRRBNwojdVVyXjuVAFTfn/xPa6EEl6GE9Sm/akPgFTb9aAC9pMKGJ6CtWxaGrqWcabH+ySg==", "cpu": [ "riscv64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -827,13 +909,16 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", - "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.61.0.tgz", + "integrity": "sha512-ZF+onDsBso8PJf1XaG9lB+O9RnBpKGnY6OrzC4CSHrtC1jb6jWLTKK4bRqdoCXHd22gyr2hiYmEAm8Wns/BOCw==", "cpu": [ "riscv64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -841,13 +926,16 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", - "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.61.0.tgz", + "integrity": "sha512-Atk0aSIk5Zx2Wuh9dgRQgLP0Koc8hOeYpbWryMXyk8G8/HmPkwPPkMqIIDhrXHHYqfUzSJA/I7IWSBv8xSmRBA==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -855,13 +943,16 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", - "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.61.0.tgz", + "integrity": "sha512-0uMOcf3eZ5K+K4cYHkdxShFMPlPXCOdfDFEFn9dNYAEEd2cVvmOfH7zFgRVoDgmtQ1m9k5q7qfrHzyMAubKYUA==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -869,13 +960,16 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", - "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.61.0.tgz", + "integrity": "sha512-mvFtE4A/t/7hRJ7X8Ozmu8FsIkAUat2nzl12pgU337BRmq87AQUJztwHz2Zv5/tjo9/C95E66CK03SI/ToEDJw==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -883,9 +977,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", - "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.61.0.tgz", + "integrity": "sha512-z9b9+aTxvt8n2rNltMPvyaUfB8NJ+CVyOrGK/MdIKHx7B+lXmZpm/XbRsU7Rpf3fRqJ2uS6mBJiJveCtq8LHDg==", "cpu": [ "x64" ], @@ -897,9 +991,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", - "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.61.0.tgz", + "integrity": "sha512-jXaXFqKMehsOc+g8R6oo33RRC6w07G9jDBxAE5eAKX7mOcCbZloYIPNhfG9Wl+P9O9IWHFO4OJgPi1Ml2qkt7w==", "cpu": [ "arm64" ], @@ -911,9 +1005,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", - "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.61.0.tgz", + "integrity": "sha512-OXNWVFocS2IA4+QplhTZZ2a+8hPZR7T8KuozsNmJKK8y7cp83StHvGksfHzPG3wczWTczyWHVQuqeiTUbjiyBg==", "cpu": [ "arm64" ], @@ -925,9 +1019,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", - "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.61.0.tgz", + "integrity": "sha512-AlAbNtBO637LxSldqV43z0FfXoGfl2TW1DgAg/bs7aQswFbDewz2SJm3BUhiGfbOVtW571xbc9p+REdxhyN/Eg==", "cpu": [ "ia32" ], @@ -939,9 +1033,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", - "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.61.0.tgz", + "integrity": "sha512-QRSrQXyJ1M4tjNXdR0/G/IgV6lzfQQJYBjlWIEYkY2Xs86DRl/iEpQ4blMDjJxSl7n19eDKKXMg0AmuBVYy8pQ==", "cpu": [ "x64" ], @@ -953,9 +1047,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", - "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.61.0.tgz", + "integrity": "sha512-tkuFxhvKO/HlGd0VsINF6vHSYH8AF8W0TcNxKDK6JZmrehngFj78pToc8iemtnvwilDjs2G/qSzYFhe9U8q+fw==", "cpu": [ "x64" ], @@ -966,49 +1060,68 @@ "win32" ] }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "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==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true, "license": "MIT" }, "node_modules/@types/node": { - "version": "22.19.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.8.tgz", - "integrity": "sha512-ebO/Yl+EAvVe8DnMfi+iaAyIqYdK0q/q0y0rw82INWEKJOBe6b/P3YWE8NW7oOlF/nXFNrHwhARrN/hdgDkraA==", + "version": "24.13.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz", + "integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "undici-types": "~7.18.0" } }, "node_modules/@vitest/coverage-v8": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.9.tgz", - "integrity": "sha512-Z2cOr0ksM00MpEfyVE8KXIYPEcBFxdbLSs56L8PO0QQMxt/6bDj45uQfxoc96v05KW3clk7vvgP0qfDit9DmfQ==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.6.tgz", + "integrity": "sha512-LsAdmUapA0qSN306d8+zOyawM0hFm2m2Hg9IwVNIKBm+qJV8cijiq2c+gxKZcB1HCfIWAy+0qEZDCUQA58A1cw==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.3.0", - "@bcoe/v8-coverage": "^0.2.3", - "debug": "^4.3.7", + "@bcoe/v8-coverage": "^1.0.2", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-lib-source-maps": "^5.0.6", "istanbul-reports": "^3.1.7", - "magic-string": "^0.30.12", + "magic-string": "^0.30.17", "magicast": "^0.3.5", - "std-env": "^3.8.0", + "std-env": "^3.9.0", "test-exclude": "^7.0.1", - "tinyrainbow": "^1.2.0" + "tinyrainbow": "^2.0.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "2.1.9", - "vitest": "2.1.9" + "@vitest/browser": "3.2.6", + "vitest": "3.2.6" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -1017,38 +1130,39 @@ } }, "node_modules/@vitest/expect": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", - "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.6.tgz", + "integrity": "sha512-1+7q9BtaKzEmO+fmNT3kYvoNn5Y71XWAx2Q5HRim4tTVRQVRv4uJFAQ5FbK0OPUeNP/WmVCpxYxoJdvuHVjzBQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "2.1.9", - "@vitest/utils": "2.1.9", - "chai": "^5.1.2", - "tinyrainbow": "^1.2.0" + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.6", + "@vitest/utils": "3.2.6", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/mocker": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", - "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.6.tgz", + "integrity": "sha512-EZOrpDbkKotFAP7wPAQV1UIyoGOk4oX7ynWhBhLB7v+meMHbQhU16oPpIYGTTe4oFlhpryGpgpcZP/sin3hYuw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "2.1.9", + "@vitest/spy": "3.2.6", "estree-walker": "^3.0.3", - "magic-string": "^0.30.12" + "magic-string": "^0.30.17" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^5.0.0" + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "peerDependenciesMeta": { "msw": { @@ -1060,70 +1174,71 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", - "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.6.tgz", + "integrity": "sha512-lb7XXXzmm2h2ASzFnRvQpDo6onT1NmMJA3tkGTWiBFtRJ9lxGY3d3mm/Apt36gej2bkkOVLL/yTOtufDaFa/jA==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^1.2.0" + "tinyrainbow": "^2.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", - "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.6.tgz", + "integrity": "sha512-HYcoSj1w5tcgUnzoF0HcyaAQjpA1gj9ftUJ7iSJSuipc02jW9gKkigwZbjFldAfYHA1fa8UZVRftdMY5msWM9Q==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "2.1.9", - "pathe": "^1.1.2" + "@vitest/utils": "3.2.6", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/snapshot": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", - "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.6.tgz", + "integrity": "sha512-H+ZjNTWGpObenh0YnlBctAPnJSI20P81PL8BPzWpx54YXLLTm8hEsWawtcYLMrwvpK48hGxLLbCS+1KRXhsKhw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.9", - "magic-string": "^0.30.12", - "pathe": "^1.1.2" + "@vitest/pretty-format": "3.2.6", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/spy": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", - "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.6.tgz", + "integrity": "sha512-oq6BbH68WzcWmwtBrU9nqLeaXTR4XwJF7FSLkKEZo4i6eoXcrxjcwSuTvWBIRUTC6VC72nXYunzqgZA+IKdtxg==", "dev": true, "license": "MIT", "dependencies": { - "tinyspy": "^3.0.2" + "tinyspy": "^4.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", - "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.6.tgz", + "integrity": "sha512-lI23nIs4bnT3T8NIoh+vFaz5s2/DdP0Jgt2jxwgWljvwn82cLJtyi/If+fjFyoLMGIOz0U/fKvWE0d4jsNQEfg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.9", - "loupe": "^3.1.2", - "tinyrainbow": "^1.2.0" + "@vitest/pretty-format": "3.2.6", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -1143,9 +1258,9 @@ } }, "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -1211,28 +1326,43 @@ "node": ">=12" } }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz", + "integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.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==", + "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" + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } }, "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==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", "license": "MIT", "dependencies": { "bytes": "^3.1.2", - "content-type": "^1.0.5", + "content-type": "^2.0.0", "debug": "^4.4.3", - "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", - "qs": "^6.14.1", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" }, "engines": { "node": ">=18" @@ -1242,14 +1372,30 @@ "url": "https://opencollective.com/express" } }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/bytes": { @@ -1348,9 +1494,9 @@ "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==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", "license": "MIT", "engines": { "node": ">=18" @@ -1523,9 +1669,9 @@ "license": "MIT" }, "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==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -1535,9 +1681,9 @@ } }, "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1545,32 +1691,35 @@ "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" } }, "node_modules/escape-html": { @@ -1611,9 +1760,9 @@ } }, "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==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", + "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -1673,10 +1822,13 @@ } }, "node_modules/express-rate-limit": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", - "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", "license": "MIT", + "dependencies": { + "ip-address": "^10.2.0" + }, "engines": { "node": ">= 16" }, @@ -1694,9 +1846,9 @@ "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==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", "funding": [ { "type": "github", @@ -1709,6 +1861,24 @@ ], "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/finalhandler": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", @@ -1848,6 +2018,39 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/glob/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/glob/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -1883,9 +2086,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -1895,11 +2098,10 @@ } }, "node_modules/hono": { - "version": "4.11.7", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.7.tgz", - "integrity": "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==", + "version": "4.12.25", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.25.tgz", + "integrity": "sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=16.9.0" } @@ -1953,6 +2155,15 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -2055,14 +2266,21 @@ } }, "node_modules/jose": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", - "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" } }, + "node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "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", @@ -2183,27 +2401,27 @@ } }, "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^5.0.5" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" } @@ -2215,9 +2433,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", "dev": true, "funding": [ { @@ -2327,9 +2545,9 @@ } }, "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==", + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", "license": "MIT", "funding": { "type": "opencollective", @@ -2337,9 +2555,9 @@ } }, "node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "dev": true, "license": "MIT" }, @@ -2360,6 +2578,19 @@ "dev": true, "license": "ISC" }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "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", @@ -2370,9 +2601,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "dev": true, "funding": [ { @@ -2390,7 +2621,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -2412,9 +2643,9 @@ } }, "node_modules/qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -2460,13 +2691,13 @@ } }, "node_modules/rollup": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", - "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.61.0.tgz", + "integrity": "sha512-T9mWdbWfQtp0B5lv/HX+wrhYsmXRlcWnXXmJbXqKJhlRaoS6KMhq0gpyzW4UJfclcxrEdLnTgjT2NjruLONu0g==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" + "@types/estree": "1.0.9" }, "bin": { "rollup": "dist/bin/rollup" @@ -2476,31 +2707,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.57.1", - "@rollup/rollup-android-arm64": "4.57.1", - "@rollup/rollup-darwin-arm64": "4.57.1", - "@rollup/rollup-darwin-x64": "4.57.1", - "@rollup/rollup-freebsd-arm64": "4.57.1", - "@rollup/rollup-freebsd-x64": "4.57.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", - "@rollup/rollup-linux-arm-musleabihf": "4.57.1", - "@rollup/rollup-linux-arm64-gnu": "4.57.1", - "@rollup/rollup-linux-arm64-musl": "4.57.1", - "@rollup/rollup-linux-loong64-gnu": "4.57.1", - "@rollup/rollup-linux-loong64-musl": "4.57.1", - "@rollup/rollup-linux-ppc64-gnu": "4.57.1", - "@rollup/rollup-linux-ppc64-musl": "4.57.1", - "@rollup/rollup-linux-riscv64-gnu": "4.57.1", - "@rollup/rollup-linux-riscv64-musl": "4.57.1", - "@rollup/rollup-linux-s390x-gnu": "4.57.1", - "@rollup/rollup-linux-x64-gnu": "4.57.1", - "@rollup/rollup-linux-x64-musl": "4.57.1", - "@rollup/rollup-openbsd-x64": "4.57.1", - "@rollup/rollup-openharmony-arm64": "4.57.1", - "@rollup/rollup-win32-arm64-msvc": "4.57.1", - "@rollup/rollup-win32-ia32-msvc": "4.57.1", - "@rollup/rollup-win32-x64-gnu": "4.57.1", - "@rollup/rollup-win32-x64-msvc": "4.57.1", + "@rollup/rollup-android-arm-eabi": "4.61.0", + "@rollup/rollup-android-arm64": "4.61.0", + "@rollup/rollup-darwin-arm64": "4.61.0", + "@rollup/rollup-darwin-x64": "4.61.0", + "@rollup/rollup-freebsd-arm64": "4.61.0", + "@rollup/rollup-freebsd-x64": "4.61.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.61.0", + "@rollup/rollup-linux-arm-musleabihf": "4.61.0", + "@rollup/rollup-linux-arm64-gnu": "4.61.0", + "@rollup/rollup-linux-arm64-musl": "4.61.0", + "@rollup/rollup-linux-loong64-gnu": "4.61.0", + "@rollup/rollup-linux-loong64-musl": "4.61.0", + "@rollup/rollup-linux-ppc64-gnu": "4.61.0", + "@rollup/rollup-linux-ppc64-musl": "4.61.0", + "@rollup/rollup-linux-riscv64-gnu": "4.61.0", + "@rollup/rollup-linux-riscv64-musl": "4.61.0", + "@rollup/rollup-linux-s390x-gnu": "4.61.0", + "@rollup/rollup-linux-x64-gnu": "4.61.0", + "@rollup/rollup-linux-x64-musl": "4.61.0", + "@rollup/rollup-openbsd-x64": "4.61.0", + "@rollup/rollup-openharmony-arm64": "4.61.0", + "@rollup/rollup-win32-arm64-msvc": "4.61.0", + "@rollup/rollup-win32-ia32-msvc": "4.61.0", + "@rollup/rollup-win32-x64-gnu": "4.61.0", + "@rollup/rollup-win32-x64-msvc": "4.61.0", "fsevents": "~2.3.2" } }, @@ -2527,9 +2758,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", "dev": true, "license": "ISC", "bin": { @@ -2612,14 +2843,14 @@ } }, "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" }, @@ -2631,13 +2862,13 @@ } }, "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==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -2801,13 +3032,13 @@ } }, "node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^6.2.2" }, "engines": { "node": ">=12" @@ -2840,6 +3071,26 @@ "node": ">=8" } }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -2854,15 +3105,15 @@ } }, "node_modules/test-exclude": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", - "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", + "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", "dev": true, "license": "ISC", "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^10.4.1", - "minimatch": "^9.0.4" + "minimatch": "^10.2.2" }, "engines": { "node": ">=18" @@ -2882,6 +3133,23 @@ "dev": true, "license": "MIT" }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, "node_modules/tinypool": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", @@ -2893,9 +3161,9 @@ } }, "node_modules/tinyrainbow": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", - "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", "dev": true, "license": "MIT", "engines": { @@ -2903,9 +3171,9 @@ } }, "node_modules/tinyspy": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", - "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", "dev": true, "license": "MIT", "engines": { @@ -2922,17 +3190,34 @@ } }, "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==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", "license": "MIT", "dependencies": { - "content-type": "^1.0.5", + "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" }, "engines": { - "node": ">= 0.6" + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/typescript": { @@ -2950,9 +3235,9 @@ } }, "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==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "dev": true, "license": "MIT" }, @@ -2975,21 +3260,24 @@ } }, "node_modules/vite": { - "version": "5.4.21", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", - "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.5.tgz", + "integrity": "sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -2998,19 +3286,25 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { "@types/node": { "optional": true }, + "jiti": { + "optional": true + }, "less": { "optional": true }, @@ -3031,74 +3325,84 @@ }, "terser": { "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true } } }, "node_modules/vite-node": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", - "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", "dev": true, "license": "MIT", "dependencies": { "cac": "^6.7.14", - "debug": "^4.3.7", - "es-module-lexer": "^1.5.4", - "pathe": "^1.1.2", - "vite": "^5.0.0" + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/vitest": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", - "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "2.1.9", - "@vitest/mocker": "2.1.9", - "@vitest/pretty-format": "^2.1.9", - "@vitest/runner": "2.1.9", - "@vitest/snapshot": "2.1.9", - "@vitest/spy": "2.1.9", - "@vitest/utils": "2.1.9", - "chai": "^5.1.2", - "debug": "^4.3.7", - "expect-type": "^1.1.0", - "magic-string": "^0.30.12", - "pathe": "^1.1.2", - "std-env": "^3.8.0", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.6.tgz", + "integrity": "sha512-xejya+bT/j/+R/AGa1XOfRxLmNUlLtlwjRsFUILF+xHfzElmGcmFydy2gqqIrd62ptIEfwVMofd19uNWD9L7Nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.6", + "@vitest/mocker": "3.2.6", + "@vitest/pretty-format": "^3.2.6", + "@vitest/runner": "3.2.6", + "@vitest/snapshot": "3.2.6", + "@vitest/spy": "3.2.6", + "@vitest/utils": "3.2.6", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", "tinybench": "^2.9.0", - "tinyexec": "^0.3.1", - "tinypool": "^1.0.1", - "tinyrainbow": "^1.2.0", - "vite": "^5.0.0", - "vite-node": "2.1.9", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "bin": { "vitest": "vitest.mjs" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "@edge-runtime/vm": "*", - "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "2.1.9", - "@vitest/ui": "2.1.9", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.6", + "@vitest/ui": "3.2.6", "happy-dom": "*", "jsdom": "*" }, @@ -3106,6 +3410,9 @@ "@edge-runtime/vm": { "optional": true }, + "@types/debug": { + "optional": true + }, "@types/node": { "optional": true }, @@ -3260,21 +3567,21 @@ "license": "ISC" }, "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "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==", + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", "license": "ISC", "peerDependencies": { - "zod": "^3.25 || ^4" + "zod": "^3.25.28 || ^4" } } } diff --git a/package.json b/package.json index 13ce5b7..a932edb 100644 --- a/package.json +++ b/package.json @@ -1,20 +1,28 @@ { "name": "kimi-code-mcp", - "version": "1.0.0", - "description": "MCP server wrapping Kimi Code CLI (kimi-k2.5) — filesystem, shell, web, and agent tools", + "version": "0.1.0", + "description": "MCP server that turns the Kimi CLI into a delegatable engine for Claude — deep code audits, bulk codebase analysis, and one-shot queries over a 256K-context agent.", "type": "module", "main": "dist/index.js", "bin": { "kimi-code-mcp": "dist/index.js" }, "files": [ - "dist" + "dist", + "server.json", + "README.md", + "CHANGELOG.md", + "LICENSE" ], "scripts": { "build": "tsc", - "prepare": "npm run build", + "prepublishOnly": "npm run build", "watch": "tsc --watch", - "test": "vitest run" + "typecheck": "tsc --noEmit", + "test": "vitest run", + "test:watch": "vitest", + "coverage": "vitest run --coverage", + "start": "node dist/index.js" }, "keywords": [ "mcp", @@ -23,28 +31,32 @@ "kimi-code", "kimi-cli", "moonshot", - "coding-agent" + "code-audit", + "code-review", + "coding-agent", + "claude" ], "license": "MIT", + "author": "thetagamma", "repository": { "type": "git", - "url": "https://github.com/userfrm/kimi-code-mcp.git" + "url": "git+https://github.com/userFRM/kimi-code-mcp.git" }, - "homepage": "https://github.com/userfrm/kimi-code-mcp", + "homepage": "https://github.com/userFRM/kimi-code-mcp#readme", "bugs": { - "url": "https://github.com/userfrm/kimi-code-mcp/issues" + "url": "https://github.com/userFRM/kimi-code-mcp/issues" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.11.2", - "zod": "^3.24.4" + "@modelcontextprotocol/sdk": "^1.29.0", + "zod": "^4.4.3" }, "devDependencies": { - "@types/node": "^22.15.17", - "@vitest/coverage-v8": "^2.1.8", - "typescript": "^5.8.3", - "vitest": "^2.1.8" + "@types/node": "^24.0.0", + "@vitest/coverage-v8": "^3.2.4", + "typescript": "^5.9.0", + "vitest": "^3.2.4" } } diff --git a/server.json b/server.json new file mode 100644 index 0000000..2b985ea --- /dev/null +++ b/server.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json", + "name": "io.github.userFRM/kimi-code-mcp", + "description": "Delegate deep code audits, bulk codebase analysis, and one-shot queries to the Kimi CLI agent.", + "version": "0.1.0", + "repository": { + "url": "https://github.com/userFRM/kimi-code-mcp", + "source": "github" + }, + "packages": [ + { + "registryType": "npm", + "identifier": "kimi-code-mcp", + "version": "0.1.0", + "transport": { + "type": "stdio" + } + } + ] +} diff --git a/src/__tests__/cli.test.ts b/src/__tests__/cli.test.ts deleted file mode 100644 index 0586df4..0000000 --- a/src/__tests__/cli.test.ts +++ /dev/null @@ -1,204 +0,0 @@ -import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest'; -import { findKimiCli, buildKimiArgs, executeKimi } from '../cli.js'; -import { EventEmitter } from 'node:events'; -import type { ChildProcess } from 'node:child_process'; - -describe('findKimiCli', () => { - afterEach(() => { - vi.unstubAllEnvs(); - }); - - it('returns "kimi" when no env var is set', () => { - vi.stubEnv('KIMI_CLI_PATH', ''); - expect(findKimiCli()).toBe('kimi'); - }); - - it('returns the env var value when KIMI_CLI_PATH is set', () => { - vi.stubEnv('KIMI_CLI_PATH', '/usr/local/bin/kimi'); - expect(findKimiCli()).toBe('/usr/local/bin/kimi'); - }); - - it('throws for relative paths in KIMI_CLI_PATH', () => { - vi.stubEnv('KIMI_CLI_PATH', './bin/kimi'); - expect(() => findKimiCli()).toThrow('must be an absolute path'); - }); - - it('throws for parent-relative paths in KIMI_CLI_PATH', () => { - vi.stubEnv('KIMI_CLI_PATH', '../bin/kimi'); - expect(() => findKimiCli()).toThrow('must be an absolute path'); - }); - - it('throws for bare relative paths like foo/bar in KIMI_CLI_PATH', () => { - vi.stubEnv('KIMI_CLI_PATH', 'foo/bar'); - expect(() => findKimiCli()).toThrow('must be an absolute path'); - }); - - it('throws for bare name without slashes in KIMI_CLI_PATH', () => { - vi.stubEnv('KIMI_CLI_PATH', 'kimi-custom'); - expect(() => findKimiCli()).toThrow('must be an absolute path'); - }); -}); - -describe('buildKimiArgs', () => { - it('always includes --print and -y', () => { - const args = buildKimiArgs({ prompt: 'test' }); - expect(args).toContain('--print'); - expect(args).toContain('-y'); - }); - - it('adds --thinking when thinking=true', () => { - const args = buildKimiArgs({ prompt: 'test', thinking: true }); - expect(args).toContain('--thinking'); - expect(args).not.toContain('--no-thinking'); - }); - - it('adds --no-thinking when thinking=false', () => { - const args = buildKimiArgs({ prompt: 'test', thinking: false }); - expect(args).toContain('--no-thinking'); - expect(args).not.toContain('--thinking'); - }); - - it('adds --no-thinking when thinking is undefined', () => { - const args = buildKimiArgs({ prompt: 'test' }); - expect(args).toContain('--no-thinking'); - }); - - it('adds --work-dir when workDir is provided', () => { - const args = buildKimiArgs({ prompt: 'test', workDir: '/tmp' }); - expect(args).toContain('--work-dir'); - expect(args).toContain('/tmp'); - }); - - it('adds --session when session is provided', () => { - const args = buildKimiArgs({ prompt: 'test', session: 'abc123' }); - expect(args).toContain('--session'); - expect(args).toContain('abc123'); - }); - - it('adds --continue when continueSession=true', () => { - const args = buildKimiArgs({ prompt: 'test', continueSession: true }); - expect(args).toContain('--continue'); - }); - - it('does not add --continue when continueSession is undefined', () => { - const args = buildKimiArgs({ prompt: 'test' }); - expect(args).not.toContain('--continue'); - }); - - it('adds --model when model is provided', () => { - const args = buildKimiArgs({ prompt: 'test', model: 'moonshot-v1' }); - expect(args).toContain('--model'); - expect(args).toContain('moonshot-v1'); - }); - - it('always ends with -p and the prompt', () => { - const args = buildKimiArgs({ - prompt: 'hello world', - thinking: true, - workDir: '/tmp', - session: 'sess1', - model: 'moonshot-v1', - }); - const lastTwo = args.slice(-2); - expect(lastTwo).toEqual(['-p', 'hello world']); - }); -}); - -// Helper to create a mock child process -function createMockChild(): ChildProcess & { stdout: EventEmitter; stderr: EventEmitter } { - const child = new EventEmitter() as any; - child.stdout = new EventEmitter(); - child.stderr = new EventEmitter(); - child.kill = vi.fn(); - return child; -} - -vi.mock('node:child_process', () => ({ - spawn: vi.fn(), -})); - -describe('executeKimi', () => { - let spawnMock: ReturnType; - - beforeEach(async () => { - vi.stubEnv('KIMI_CLI_PATH', '/usr/bin/kimi'); - const cp = await import('node:child_process'); - spawnMock = cp.spawn as unknown as ReturnType; - }); - - afterEach(() => { - vi.unstubAllEnvs(); - vi.restoreAllMocks(); - }); - - it('resolves with parsed output on success', async () => { - const child = createMockChild(); - spawnMock.mockReturnValue(child); - - const promise = executeKimi({ prompt: 'test' }); - - // Simulate kimi CLI output - child.stdout.emit('data', Buffer.from("TextPart(type='text', text='Hello')")); - child.emit('close', 0); - - const result = await promise; - expect(result.text).toBe('Hello'); - }); - - it('rejects on non-zero exit code', async () => { - const child = createMockChild(); - spawnMock.mockReturnValue(child); - - const promise = executeKimi({ prompt: 'test' }); - - child.stderr.emit('data', Buffer.from('something went wrong')); - child.emit('close', 1); - - await expect(promise).rejects.toThrow('Kimi CLI exited with code 1'); - }); - - it('rejects on spawn error (binary not found)', async () => { - const child = createMockChild(); - spawnMock.mockReturnValue(child); - - const promise = executeKimi({ prompt: 'test' }); - - child.emit('error', new Error('spawn ENOENT')); - - await expect(promise).rejects.toThrow('Failed to execute Kimi CLI: spawn ENOENT'); - }); - - it('rejects on timeout', async () => { - vi.useFakeTimers(); - const child = createMockChild(); - spawnMock.mockReturnValue(child); - - const promise = executeKimi({ prompt: 'test', timeout: 1 }); - - // Advance past the 1-second timeout - vi.advanceTimersByTime(1100); - - await expect(promise).rejects.toThrow('timed out after 1 seconds'); - expect(child.kill).toHaveBeenCalledWith('SIGTERM'); - - vi.useRealTimers(); - }); - - it('does not double-reject after timeout when close fires', async () => { - vi.useFakeTimers(); - const child = createMockChild(); - spawnMock.mockReturnValue(child); - - const promise = executeKimi({ prompt: 'test', timeout: 1 }); - - // Trigger timeout - vi.advanceTimersByTime(1100); - - // Now close fires after kill — should not cause unhandled rejection - child.emit('close', null); - - await expect(promise).rejects.toThrow('timed out'); - - vi.useRealTimers(); - }); -}); diff --git a/src/__tests__/handlers.test.ts b/src/__tests__/handlers.test.ts deleted file mode 100644 index 32db6b6..0000000 --- a/src/__tests__/handlers.test.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { buildPrompt } from '../handlers.js'; - -describe('buildPrompt', () => { - it('builds kimi_read_file prompt with path only', () => { - const prompt = buildPrompt('kimi_read_file', { path: '/foo/bar.ts' }); - expect(prompt).toContain('/foo/bar.ts'); - expect(prompt).toContain('Read the file'); - }); - - it('builds kimi_read_file prompt with offset and limit', () => { - const prompt = buildPrompt('kimi_read_file', { path: '/foo.ts', offset: 10, limit: 50 }); - expect(prompt).toContain('from line 10'); - expect(prompt).toContain('up to 50 lines'); - }); - - it('builds kimi_read_media prompt with default description', () => { - const prompt = buildPrompt('kimi_read_media', { path: '/img.png' }); - expect(prompt).toContain('/img.png'); - expect(prompt).toContain('Describe this media file'); - }); - - it('builds kimi_read_media prompt with custom prompt', () => { - const prompt = buildPrompt('kimi_read_media', { path: '/img.png', prompt: 'Count the cats' }); - expect(prompt).toContain('Count the cats'); - }); - - it('builds kimi_write_file prompt', () => { - const prompt = buildPrompt('kimi_write_file', { path: '/out.txt', content: 'hello' }); - expect(prompt).toContain('/out.txt'); - expect(prompt).toContain('hello'); - }); - - it('builds kimi_edit_file prompt', () => { - const prompt = buildPrompt('kimi_edit_file', { path: '/f.ts', old_string: 'old', new_string: 'new' }); - expect(prompt).toContain('old'); - expect(prompt).toContain('new'); - expect(prompt).toContain('/f.ts'); - }); - - it('builds kimi_glob prompt', () => { - const prompt = buildPrompt('kimi_glob', { pattern: '**/*.ts' }); - expect(prompt).toContain('**/*.ts'); - }); - - it('builds kimi_glob prompt with path', () => { - const prompt = buildPrompt('kimi_glob', { pattern: '*.js', path: '/src' }); - expect(prompt).toContain('/src'); - }); - - it('builds kimi_grep prompt', () => { - const prompt = buildPrompt('kimi_grep', { pattern: 'TODO' }); - expect(prompt).toContain('TODO'); - }); - - it('builds kimi_grep prompt with include', () => { - const prompt = buildPrompt('kimi_grep', { pattern: 'TODO', include: '*.ts' }); - expect(prompt).toContain('*.ts'); - }); - - it('builds kimi_shell prompt', () => { - const prompt = buildPrompt('kimi_shell', { command: 'ls -la' }); - expect(prompt).toContain('ls -la'); - }); - - it('builds kimi_web_search prompt', () => { - const prompt = buildPrompt('kimi_web_search', { query: 'vitest mocking' }); - expect(prompt).toContain('vitest mocking'); - }); - - it('builds kimi_web_search prompt with include_content', () => { - const prompt = buildPrompt('kimi_web_search', { query: 'test', include_content: true }); - expect(prompt).toContain('Include page content'); - }); - - it('builds kimi_fetch_url prompt', () => { - const prompt = buildPrompt('kimi_fetch_url', { url: 'https://example.com' }); - expect(prompt).toContain('https://example.com'); - }); - - it('builds kimi_fetch_url prompt with custom prompt', () => { - const prompt = buildPrompt('kimi_fetch_url', { url: 'https://example.com', prompt: 'Get title' }); - expect(prompt).toContain('Get title'); - }); - - it('builds kimi_agent prompt', () => { - const prompt = buildPrompt('kimi_agent', { prompt: 'Do the thing' }); - expect(prompt).toBe('Do the thing'); - }); - - it('builds kimi_think prompt', () => { - const prompt = buildPrompt('kimi_think', { problem: 'Why is X slow?' }); - expect(prompt).toContain('Why is X slow?'); - expect(prompt).toContain('Think deeply'); - }); - - it('builds kimi_think prompt with context', () => { - const prompt = buildPrompt('kimi_think', { problem: 'X', context: 'extra info' }); - expect(prompt).toContain('extra info'); - }); - - it('builds kimi_review prompt for code snippet', () => { - const prompt = buildPrompt('kimi_review', { code_or_path: 'const x = 1;' }); - expect(prompt).toContain('const x = 1;'); - expect(prompt).toContain('bugs'); - }); - - it('builds kimi_review prompt with focus', () => { - const prompt = buildPrompt('kimi_review', { code_or_path: 'const x = 1;', focus: 'security' }); - expect(prompt).toContain('security'); - expect(prompt).not.toContain('bugs, security, performance'); - }); - - it('builds kimi_research prompt', () => { - const prompt = buildPrompt('kimi_research', { question: 'How does X work?' }); - expect(prompt).toContain('How does X work?'); - }); - - it('builds kimi_research prompt with context', () => { - const prompt = buildPrompt('kimi_research', { question: 'Q', context: 'background' }); - expect(prompt).toContain('background'); - }); - - it('builds kimi_test prompt', () => { - const prompt = buildPrompt('kimi_test', { target: 'src/cli.ts' }); - expect(prompt).toContain('src/cli.ts'); - expect(prompt).toContain('edge cases'); - }); - - it('builds kimi_test prompt with instructions', () => { - const prompt = buildPrompt('kimi_test', { target: 'src/cli.ts', instructions: 'Use vitest' }); - expect(prompt).toContain('Use vitest'); - }); - - it('throws for unknown tool', () => { - expect(() => buildPrompt('kimi_nonexistent', {})).toThrow('Unknown tool: kimi_nonexistent'); - }); -}); diff --git a/src/__tests__/mocks.ts b/src/__tests__/mocks.ts deleted file mode 100644 index 09fa86f..0000000 --- a/src/__tests__/mocks.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Fixture strings representing real kimi CLI output for testing. - */ - -/** A basic kimi response with a single TextPart */ -export const SIMPLE_TEXT_OUTPUT = `TurnBegin(turn_index=0) -StepBegin(step_index=0, step_type='text_response') -TextPart(type='text', text='Hello, world!') -StepEnd(step_index=0) -TurnEnd(turn_index=0)`; - -/** Response with multiple TextParts */ -export const MULTI_TEXT_OUTPUT = `TurnBegin(turn_index=0) -StepBegin(step_index=0, step_type='text_response') -TextPart(type='text', text='First part. ') -TextPart(type='text', text='Second part. ') -TextPart(type='text', text='Third part.') -StepEnd(step_index=0) -TurnEnd(turn_index=0)`; - -/** Response with both ThinkPart and TextPart */ -export const THINKING_OUTPUT = `TurnBegin(turn_index=0) -StepBegin(step_index=0, step_type='text_response') -ThinkPart( - type='think', - think='Let me analyze this step by step...', - encrypted=False -) -TextPart(type='text', text='The answer is 42.') -StepEnd(step_index=0) -TurnEnd(turn_index=0)`; - -/** Raw text without TextPart wrappers (fallback case) */ -export const PLAIN_TEXT_OUTPUT = `TurnBegin(turn_index=0) -StepBegin(step_index=0, step_type='text_response') -This is plain text output -that spans multiple lines. -StepEnd(step_index=0) -TurnEnd(turn_index=0)`; - -/** Empty string */ -export const EMPTY_OUTPUT = ''; - -/** TextPart with escaped characters (\n, \', \") */ -export const ESCAPED_OUTPUT = `TurnBegin(turn_index=0) -StepBegin(step_index=0, step_type='text_response') -TextPart(type='text', text='Line one\\nLine two\\nHe said \\\'hello\\\' and \\"goodbye\\"') -StepEnd(step_index=0) -TurnEnd(turn_index=0)`; diff --git a/src/__tests__/parser.test.ts b/src/__tests__/parser.test.ts deleted file mode 100644 index 0effa56..0000000 --- a/src/__tests__/parser.test.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { extractTextParts, extractThinkParts, extractPlainText, parseKimiOutput } from '../parser.js'; -import { - SIMPLE_TEXT_OUTPUT, - MULTI_TEXT_OUTPUT, - THINKING_OUTPUT, - PLAIN_TEXT_OUTPUT, - EMPTY_OUTPUT, - ESCAPED_OUTPUT, -} from './mocks.js'; - -describe('extractTextParts', () => { - it('extracts text from a single TextPart block', () => { - const parts = extractTextParts(SIMPLE_TEXT_OUTPUT); - expect(parts).toEqual(['Hello, world!']); - }); - - it('extracts text from multiple TextPart blocks', () => { - const parts = extractTextParts(MULTI_TEXT_OUTPUT); - expect(parts).toEqual(['First part. ', 'Second part. ', 'Third part.']); - }); - - it('handles escaped characters in TextPart', () => { - const parts = extractTextParts(ESCAPED_OUTPUT); - expect(parts).toHaveLength(1); - expect(parts[0]).toContain('\n'); - expect(parts[0]).toContain("'"); - expect(parts[0]).toContain('"'); - }); - - it('returns empty array when no TextParts are present', () => { - const parts = extractTextParts(PLAIN_TEXT_OUTPUT); - expect(parts).toEqual([]); - }); - - it('returns empty array for empty input', () => { - const parts = extractTextParts(EMPTY_OUTPUT); - expect(parts).toEqual([]); - }); - - it('preserves literal backslash-n (\\\\n) as two characters', () => { - // Input: \\n in the raw CLI output represents a literal backslash followed by 'n' - const raw = `TextPart(type='text', text='path\\\\nname')`; - const parts = extractTextParts(raw); - expect(parts).toEqual(['path\\nname']); // backslash + 'n', not a newline - }); -}); - -describe('extractThinkParts', () => { - it('extracts thinking from ThinkPart blocks', () => { - const parts = extractThinkParts(THINKING_OUTPUT); - expect(parts).toHaveLength(1); - expect(parts[0]).toBe('Let me analyze this step by step...'); - }); - - it('returns empty array for non-thinking output', () => { - const parts = extractThinkParts(SIMPLE_TEXT_OUTPUT); - expect(parts).toEqual([]); - }); - - it('returns empty array for empty input', () => { - const parts = extractThinkParts(EMPTY_OUTPUT); - expect(parts).toEqual([]); - }); -}); - -describe('extractPlainText', () => { - it('filters out structured lines and keeps real content', () => { - const lines = extractPlainText(PLAIN_TEXT_OUTPUT); - expect(lines).toEqual([ - 'This is plain text output', - 'that spans multiple lines.', - ]); - }); - - it('filters out TurnBegin, StepBegin, StepEnd, TurnEnd lines', () => { - const lines = extractPlainText(SIMPLE_TEXT_OUTPUT); - // TextPart line is not in skipPrefixes, so it remains - // but TurnBegin, StepBegin, StepEnd, TurnEnd are filtered - for (const line of lines) { - expect(line).not.toMatch(/^TurnBegin/); - expect(line).not.toMatch(/^StepBegin/); - expect(line).not.toMatch(/^StepEnd/); - expect(line).not.toMatch(/^TurnEnd/); - } - }); - - it('filters out ThinkPart and indented key= lines', () => { - const lines = extractPlainText(THINKING_OUTPUT); - for (const line of lines) { - expect(line).not.toMatch(/^ThinkPart/); - expect(line).not.toMatch(/^\s+type='/); - expect(line).not.toMatch(/^\s+think='/); - expect(line).not.toMatch(/^\s+encrypted=/); - } - }); - - it('returns empty array for empty input', () => { - const lines = extractPlainText(EMPTY_OUTPUT); - expect(lines).toEqual([]); - }); -}); - -describe('parseKimiOutput', () => { - it('parses output with both text and thinking', () => { - const result = parseKimiOutput(THINKING_OUTPUT); - expect(result.text).toBe('The answer is 42.'); - expect(result.thinking).toBe('Let me analyze this step by step...'); - expect(result.raw).toBe(THINKING_OUTPUT); - }); - - it('parses text-only output', () => { - const result = parseKimiOutput(SIMPLE_TEXT_OUTPUT); - expect(result.text).toBe('Hello, world!'); - expect(result.thinking).toBeUndefined(); - expect(result.raw).toBe(SIMPLE_TEXT_OUTPUT); - }); - - it('concatenates multiple text parts', () => { - const result = parseKimiOutput(MULTI_TEXT_OUTPUT); - expect(result.text).toBe('First part. Second part. Third part.'); - expect(result.thinking).toBeUndefined(); - }); - - it('falls back to plain text when no TextParts found', () => { - const result = parseKimiOutput(PLAIN_TEXT_OUTPUT); - expect(result.text).toContain('This is plain text output'); - expect(result.text).toContain('that spans multiple lines.'); - expect(result.thinking).toBeUndefined(); - }); - - it('returns empty text for empty input', () => { - const result = parseKimiOutput(EMPTY_OUTPUT); - expect(result.text).toBe(''); - expect(result.thinking).toBeUndefined(); - expect(result.raw).toBe(''); - }); -}); diff --git a/src/__tests__/schema.test.ts b/src/__tests__/schema.test.ts deleted file mode 100644 index e8655f2..0000000 --- a/src/__tests__/schema.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { TOOLS, TOOL_MAP } from '../handlers.js'; - -describe('TOOLS', () => { - it('has exactly 14 tools', () => { - expect(TOOLS).toHaveLength(14); - }); - - it('all tools have name, description, and inputSchema', () => { - for (const tool of TOOLS) { - expect(tool.name).toBeDefined(); - expect(typeof tool.name).toBe('string'); - expect(tool.name.length).toBeGreaterThan(0); - - expect(tool.description).toBeDefined(); - expect(typeof tool.description).toBe('string'); - expect(tool.description.length).toBeGreaterThan(0); - - expect(tool.inputSchema).toBeDefined(); - } - }); -}); - -describe('TOOL_MAP', () => { - it('contains all 14 tools', () => { - expect(TOOL_MAP.size).toBe(14); - }); - - it('maps each tool name to the correct tool', () => { - for (const tool of TOOLS) { - expect(TOOL_MAP.has(tool.name)).toBe(true); - expect(TOOL_MAP.get(tool.name)).toBe(tool); - } - }); -}); diff --git a/src/bin-resolver.test.ts b/src/bin-resolver.test.ts new file mode 100644 index 0000000..44576c9 --- /dev/null +++ b/src/bin-resolver.test.ts @@ -0,0 +1,108 @@ +import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { afterEach, describe, expect, it } from 'vitest'; +import { detectAuth, INSTALL_HINT, resolveKimiBin } from './bin-resolver.js'; + +const dirs: string[] = []; +function tmp(): string { + const d = mkdtempSync(join(tmpdir(), 'kimi-test-')); + dirs.push(d); + return d; +} + +afterEach(() => { + while (dirs.length) rmSync(dirs.pop()!, { recursive: true, force: true }); +}); + +describe('resolveKimiBin', () => { + it('honours an explicit override above all else', () => { + const r = resolveKimiBin('/custom/kimi'); + expect(r).toEqual({ path: '/custom/kimi', origin: 'override' }); + }); + + it('returns null when nothing resolves', () => { + // Empty override + empty PATH so PATH lookup fails; native/legacy may or may + // not exist on the host, so only assert the null contract via a clean env. + const savedPath = process.env.PATH; + process.env.PATH = ''; + try { + const r = resolveKimiBin(''); + // On a host with ~/.kimi-code or ~/.local/bin/kimi this is non-null; the + // contract we can always assert is that override wins and PATH is honoured. + expect(r === null || typeof r.path === 'string').toBe(true); + } finally { + process.env.PATH = savedPath; + } + }); + + it('finds a binary on PATH when no install dir exists', () => { + const dir = tmp(); + writeFileSync(join(dir, 'kimi'), '#!/bin/sh\n'); + const savedPath = process.env.PATH; + process.env.PATH = dir; + try { + const r = resolveKimiBin(''); + // Native/legacy take precedence if present on host; otherwise PATH hit. + expect(r).not.toBeNull(); + expect(r!.path.endsWith('kimi')).toBe(true); + } finally { + process.env.PATH = savedPath; + } + }); +}); + +describe('detectAuth', () => { + it('detects an env API key first', () => { + expect(detectAuth('/no/home', { KIMICODE_API_KEY: 'sk-x' }).authenticated).toBe(true); + }); + + it('detects native OAuth credentials', () => { + const home = tmp(); + mkdirSync(join(home, '.kimi', 'credentials'), { recursive: true }); + writeFileSync(join(home, '.kimi', 'credentials', 'kimi-code.json'), JSON.stringify({ access_token: 't' })); + const auth = detectAuth(home, {}); + expect(auth.authenticated).toBe(true); + expect(auth.source).toContain('kimi-code.json'); + }); + + it('falls back to legacy kimi.json', () => { + const home = tmp(); + mkdirSync(join(home, '.kimi'), { recursive: true }); + writeFileSync(join(home, '.kimi', 'kimi.json'), JSON.stringify({ api_key: 'k' })); + expect(detectAuth(home, {}).authenticated).toBe(true); + }); + + it('detects an api_key in config.toml without reading its value', () => { + const home = tmp(); + mkdirSync(join(home, '.kimi'), { recursive: true }); + writeFileSync(join(home, '.kimi', 'config.toml'), 'api_key = "sk-secret-value"\n'); + const auth = detectAuth(home, {}); + expect(auth.authenticated).toBe(true); + expect(auth.source).toContain('config.toml'); + }); + + it('treats an empty api_key in config.toml as unauthenticated', () => { + const home = tmp(); + mkdirSync(join(home, '.kimi'), { recursive: true }); + writeFileSync(join(home, '.kimi', 'config.toml'), 'api_key = ""\n'); + expect(detectAuth(home, {}).authenticated).toBe(false); + }); + + it('returns unauthenticated when no credentials exist', () => { + expect(detectAuth(tmp(), {}).authenticated).toBe(false); + }); + + it('treats a malformed credential file as unauthenticated', () => { + const home = tmp(); + mkdirSync(join(home, '.kimi', 'credentials'), { recursive: true }); + writeFileSync(join(home, '.kimi', 'credentials', 'kimi-code.json'), 'not json'); + expect(detectAuth(home, {}).authenticated).toBe(false); + }); +}); + +describe('INSTALL_HINT', () => { + it('points at the native installer', () => { + expect(INSTALL_HINT).toContain('code.kimi.com/kimi-code/install.sh'); + }); +}); diff --git a/src/bin-resolver.ts b/src/bin-resolver.ts new file mode 100644 index 0000000..dd5ed50 --- /dev/null +++ b/src/bin-resolver.ts @@ -0,0 +1,153 @@ +/** + * Locate the Kimi binary and report its health. + * + * Resolution order, most explicit first: + * 1. `$KIMI_BIN` / `$KIMI_CLI_PATH` — operator override + * 2. `~/.kimi-code/bin/kimi` — native installer (code.kimi.com/kimi-code/install.sh) + * 3. `~/.local/bin/kimi` — legacy `uv tool install kimi-cli` + * 4. `kimi` on `$PATH` + * + * The native installer renames any prior Python CLI to `kimi-legacy`, so the + * native path is preferred over the legacy one when both exist. + */ + +import { execFile } from 'node:child_process'; +import { existsSync, readFileSync } from 'node:fs'; +import { homedir } from 'node:os'; +import { delimiter, join } from 'node:path'; +import { promisify } from 'node:util'; + +const execFileAsync = promisify(execFile); + +export interface KimiAuth { + /** True when OAuth credentials or an API key are present on disk. */ + authenticated: boolean; + /** Where the credential was found, for diagnostics. */ + source?: string; +} + +export interface KimiStatus { + installed: boolean; + /** Resolved absolute path, or the bare name if only found on PATH. */ + binPath: string | null; + /** How the binary was resolved: override | native | legacy | path. */ + origin: 'override' | 'native' | 'legacy' | 'path' | 'none'; + version?: string; + auth: KimiAuth; + error?: string; +} + +const NATIVE_BIN = join(homedir(), '.kimi-code', 'bin', 'kimi'); +const LEGACY_BIN = join(homedir(), '.local', 'bin', 'kimi'); + +/** Search `$PATH` for an executable named `name`. Returns the first hit. */ +function findOnPath(name: string): string | null { + const path = process.env.PATH ?? ''; + for (const dir of path.split(delimiter)) { + if (dir === '') continue; + const candidate = join(dir, name); + if (existsSync(candidate)) return candidate; + } + return null; +} + +export interface ResolvedBin { + path: string; + origin: KimiStatus['origin']; +} + +/** + * Resolve the binary to run. Returns `null` only when nothing is found + * anywhere; callers surface an actionable install hint in that case. + */ +export function resolveKimiBin(binOverride = ''): ResolvedBin | null { + if (binOverride !== '') { + return { path: binOverride, origin: 'override' }; + } + if (existsSync(NATIVE_BIN)) return { path: NATIVE_BIN, origin: 'native' }; + if (existsSync(LEGACY_BIN)) return { path: LEGACY_BIN, origin: 'legacy' }; + const onPath = findOnPath('kimi'); + if (onPath) return { path: onPath, origin: 'path' }; + return null; +} + +/** + * Detect whether the user has logged in. We never read token *values* — only + * their presence — so this is safe to surface in diagnostics. + */ +export function detectAuth(home = homedir(), env: NodeJS.ProcessEnv = process.env): KimiAuth { + if ((env.KIMICODE_API_KEY ?? env.KIMI_API_KEY ?? '').trim() !== '') { + return { authenticated: true, source: 'env:api-key' }; + } + const kimiDir = (env.KIMI_SHARE_DIR ?? '').trim() || join(home, '.kimi'); + const oauthPath = join(kimiDir, 'credentials', 'kimi-code.json'); + const legacyPath = join(kimiDir, 'kimi.json'); + const configPath = join(kimiDir, 'config.toml'); + try { + if (existsSync(oauthPath)) { + const creds = JSON.parse(readFileSync(oauthPath, 'utf-8')) as Record; + if (creds.access_token || creds.refresh_token) { + return { authenticated: true, source: oauthPath }; + } + } + if (existsSync(legacyPath)) { + const cfg = JSON.parse(readFileSync(legacyPath, 'utf-8')) as Record; + if (cfg.access_token || cfg.auth_token || cfg.api_key) { + return { authenticated: true, source: legacyPath }; + } + } + // The CLI stores an API key (or login tokens) in config.toml. Detect by + // presence of a non-empty key — we never read the value itself. + if (existsSync(configPath)) { + const toml = readFileSync(configPath, 'utf-8'); + const re = /^\s*(?:api_key|access_token|refresh_token)\s*=\s*(.+?)\s*$/gm; + for (const m of toml.matchAll(re)) { + const value = (m[1] ?? '').replace(/^["']|["']$/g, '').trim(); + if (value !== '') { + return { authenticated: true, source: configPath }; + } + } + } + } catch { + // Malformed credential file — treat as unauthenticated rather than crash. + } + return { authenticated: false }; +} + +export const INSTALL_HINT = + 'Kimi CLI not found. Install the native binary with: ' + + 'curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash — then run `kimi login`.'; + +/** Full health probe: resolution + `--version` + credential presence. */ +export async function getKimiStatus(binOverride = ''): Promise { + const resolved = resolveKimiBin(binOverride); + if (!resolved) { + return { + installed: false, + binPath: null, + origin: 'none', + auth: { authenticated: false }, + error: INSTALL_HINT, + }; + } + + const auth = detectAuth(); + const status: KimiStatus = { + installed: true, + binPath: resolved.path, + origin: resolved.origin, + auth, + }; + + try { + const { stdout } = await execFileAsync(resolved.path, ['--version'], { timeout: 5_000 }); + status.version = stdout.trim(); + } catch { + status.version = '(unable to detect)'; + } + + if (!auth.authenticated) { + status.error = 'Not authenticated. Run `kimi login` (or set KIMICODE_API_KEY).'; + } + return status; +} diff --git a/src/cli.ts b/src/cli.ts deleted file mode 100644 index 5d09924..0000000 --- a/src/cli.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { spawn } from 'node:child_process'; -import { existsSync } from 'node:fs'; -import { isAbsolute } from 'node:path'; -import { parseKimiOutput } from './parser.js'; - -export interface KimiOutput { - text: string; - thinking?: string; - raw: string; -} - -export interface KimiCliOptions { - prompt: string; - workDir?: string; - thinking?: boolean; - session?: string; - continueSession?: boolean; - model?: string; - timeout?: number; -} - -/** - * Helper function to log debug messages to stderr when MCP_KIMI_DEBUG is set - */ -function debug(...args: unknown[]): void { - if (process.env.MCP_KIMI_DEBUG) { - console.error('[kimi-mcp debug]', ...args); - } -} - -/** - * Finds the Kimi CLI binary path - * @returns The path to the kimi binary - * @throws Error if KIMI_CLI_PATH is set to a relative path - */ -export function findKimiCli(): string { - const envPath = process.env.KIMI_CLI_PATH; - - if (envPath) { - // Validate it's an absolute path - if (!isAbsolute(envPath)) { - throw new Error('KIMI_CLI_PATH must be an absolute path, not a relative path'); - } - debug('Using Kimi CLI from KIMI_CLI_PATH:', envPath); - return envPath; - } - - debug('Using Kimi CLI from PATH: kimi'); - return 'kimi'; -} - -/** - * Builds the command-line arguments array for the Kimi CLI - * @param options - CLI options - * @returns Array of command-line arguments - * @throws Error if workDir is specified but doesn't exist - */ -export function buildKimiArgs(options: KimiCliOptions): string[] { - const args: string[] = ['--print', '-y']; - - // Add thinking flag - if (options.thinking) { - args.push('--thinking'); - } else { - args.push('--no-thinking'); - } - - // Add work directory if specified - if (options.workDir) { - if (!existsSync(options.workDir)) { - throw new Error(`Work directory does not exist: ${options.workDir}`); - } - args.push('--work-dir', options.workDir); - } - - // Add session ID if specified - if (options.session) { - args.push('--session', options.session); - } - - // Add continue flag if specified - if (options.continueSession) { - args.push('--continue'); - } - - // Add model if specified - if (options.model) { - args.push('--model', options.model); - } - - // Add prompt - args.push('-p', options.prompt); - - return args; -} - -/** - * Executes the Kimi CLI with the given options - * @param options - CLI options - * @returns Promise resolving to parsed Kimi output - * @throws Error on execution failure or timeout - */ -export function executeKimi(options: KimiCliOptions): Promise { - return new Promise((resolve, reject) => { - const binaryPath = findKimiCli(); - const args = buildKimiArgs(options); - - debug('Executing Kimi CLI:', binaryPath, args.join(' ')); - - const child = spawn(binaryPath, args); - - let stdout = ''; - let stderr = ''; - let killed = false; - - child.stdout.on('data', (data) => { - stdout += data.toString(); - }); - - child.stderr.on('data', (data) => { - stderr += data.toString(); - }); - - // Set up timeout (default 300 seconds, override from options) - const timeoutMs = options.timeout ? options.timeout * 1000 : 300_000; - const timeoutHandle = setTimeout(() => { - killed = true; - child.kill('SIGTERM'); - reject(new Error(`Kimi CLI execution timed out after ${timeoutMs / 1000} seconds`)); - }, timeoutMs); - - child.on('error', (error) => { - clearTimeout(timeoutHandle); - reject(new Error(`Failed to execute Kimi CLI: ${error.message}`)); - }); - - child.on('close', (code) => { - clearTimeout(timeoutHandle); - if (killed) return; - - if (code !== 0) { - debug('Kimi CLI exited with code:', code); - debug('stderr:', stderr); - reject(new Error(`Kimi CLI exited with code ${code}: ${stderr || 'No error message'}`)); - return; - } - - debug('Kimi CLI output length:', stdout.length); - debug('Kimi CLI stderr:', stderr); - - try { - const output = parseKimiOutput(stdout); - resolve(output); - } catch (error) { - reject(new Error(`Failed to parse Kimi output: ${error instanceof Error ? error.message : String(error)}`)); - } - }); - }); -} diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..242d540 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,59 @@ +/** + * Runtime configuration, resolved once from the environment at startup. + * + * Every value has a safe default so the server runs with zero configuration. + * Operators override behaviour through environment variables documented in the + * README; nothing here reaches out to the network or the Kimi account. + */ + +function intFromEnv(name: string, fallback: number): number { + const raw = process.env[name]; + if (raw === undefined || raw.trim() === '') return fallback; + const value = Number.parseInt(raw, 10); + return Number.isFinite(value) && value > 0 ? value : fallback; +} + +function boolFromEnv(name: string): boolean { + const raw = process.env[name]; + if (raw === undefined) return false; + return /^(1|true|yes|on)$/i.test(raw.trim()); +} + +export interface Config { + /** Explicit override for the kimi binary path. Empty when unset. */ + readonly binOverride: string; + /** Model passed to `kimi -m`. Empty means "let the CLI pick its default". */ + readonly defaultModel: string; + /** Wall-clock budget for a single Kimi invocation, in milliseconds. */ + readonly timeoutMs: number; + /** Hard cap on characters returned to the client; output is truncated at a clean boundary beyond it. */ + readonly maxOutputChars: number; + /** How many times to retry a transient (exit 75 / rate-limit) failure. */ + readonly maxRetries: number; + /** Base backoff between retries, in milliseconds (doubled each attempt). */ + readonly retryBackoffMs: number; + /** Mirror Kimi's stderr to this process's stderr for debugging. */ + readonly debug: boolean; + /** + * Auto-approve Kimi's tool actions (`-y`/--yolo). OFF by default: an + * unsupervised Kimi must not edit files or run shell unless explicitly + * opted in, per-call or via `KIMI_YOLO`. + */ + readonly yolo: boolean; +} + +export function loadConfig(env: NodeJS.ProcessEnv = process.env): Config { + return { + binOverride: (env.KIMI_BIN ?? env.KIMI_CLI_PATH ?? '').trim(), + defaultModel: (env.KIMI_MODEL ?? '').trim(), + timeoutMs: intFromEnv('KIMI_TIMEOUT_MS', 600_000), + maxOutputChars: intFromEnv('KIMI_MAX_OUTPUT_CHARS', 120_000), + maxRetries: intFromEnv('KIMI_MAX_RETRIES', 2), + retryBackoffMs: intFromEnv('KIMI_RETRY_BACKOFF_MS', 2_000), + debug: boolFromEnv('MCP_KIMI_DEBUG') || boolFromEnv('KIMI_DEBUG'), + yolo: boolFromEnv('KIMI_YOLO'), + }; +} + +export const SERVER_NAME = 'kimi-code-mcp'; +export const SERVER_VERSION = '0.1.0'; diff --git a/src/handlers.ts b/src/handlers.ts deleted file mode 100644 index 76cb15b..0000000 --- a/src/handlers.ts +++ /dev/null @@ -1,287 +0,0 @@ -import { z } from 'zod'; -import { executeKimi } from './cli.js'; -import { existsSync } from 'node:fs'; - -export const TOOLS = [ - { - name: 'kimi_read_file', - description: 'Read a text file. Returns file content up to 1000 lines.', - inputSchema: z.object({ - path: z.string().describe('Absolute file path to read'), - offset: z.number().optional().describe('Line offset to start from (0-based)'), - limit: z.number().optional().describe('Max lines to read (default: 1000)'), - workFolder: z.string().optional().describe('Working directory (absolute path)'), - }), - }, - { - name: 'kimi_read_media', - description: 'Analyze images and videos using multimodal capabilities.', - inputSchema: z.object({ - path: z.string().describe('Absolute path to image or video file'), - prompt: z.string().optional().describe('Analysis prompt for the media'), - workFolder: z.string().optional().describe('Working directory (absolute path)'), - }), - }, - { - name: 'kimi_write_file', - description: 'Create or overwrite a file with specified content.', - inputSchema: z.object({ - path: z.string().describe('Absolute file path to write'), - content: z.string().describe('Content to write to the file'), - workFolder: z.string().optional().describe('Working directory (absolute path)'), - }), - }, - { - name: 'kimi_edit_file', - description: 'Edit a file by replacing an exact string with new content.', - inputSchema: z.object({ - path: z.string().describe('Absolute file path to edit'), - old_string: z.string().describe('Exact string to find and replace'), - new_string: z.string().describe('New string to replace with'), - workFolder: z.string().optional().describe('Working directory (absolute path)'), - }), - }, - { - name: 'kimi_glob', - description: 'Find files matching a glob pattern.', - inputSchema: z.object({ - pattern: z.string().describe('Glob pattern to match (e.g., "**/*.ts")'), - path: z.string().optional().describe('Directory to search in (default: current directory)'), - workFolder: z.string().optional().describe('Working directory (absolute path)'), - }), - }, - { - name: 'kimi_grep', - description: 'Search for regex patterns in files using ripgrep.', - inputSchema: z.object({ - pattern: z.string().describe('Regex pattern to search for'), - path: z.string().optional().describe('Directory or file to search in (default: current directory)'), - include: z.string().optional().describe('File pattern to include (e.g., "*.ts")'), - workFolder: z.string().optional().describe('Working directory (absolute path)'), - }), - }, - { - name: 'kimi_shell', - description: 'Execute shell commands and return output.', - inputSchema: z.object({ - command: z.string().describe('Shell command to execute'), - workFolder: z.string().optional().describe('Working directory (absolute path)'), - timeout: z.number().optional().describe('Timeout in seconds (default: 120)'), - }), - }, - { - name: 'kimi_web_search', - description: 'Search the web and return up to 20 results.', - inputSchema: z.object({ - query: z.string().describe('Search query'), - include_content: z.boolean().optional().describe('Include page content in results'), - }), - }, - { - name: 'kimi_fetch_url', - description: 'Fetch and extract text content from a webpage.', - inputSchema: z.object({ - url: z.string().describe('URL to fetch'), - prompt: z.string().optional().describe('Extraction prompt for the webpage'), - }), - }, - { - name: 'kimi_agent', - description: 'Full autonomous agent for complex multi-step tasks.', - inputSchema: z.object({ - prompt: z.string().describe('Task description for the agent'), - workFolder: z.string().optional().describe('Working directory (absolute path)'), - timeout: z.number().optional().describe('Timeout in seconds (default: 300)'), - }), - }, - { - name: 'kimi_think', - description: 'Extended reasoning and analysis without taking actions.', - inputSchema: z.object({ - problem: z.string().describe('Problem or question to analyze'), - context: z.string().optional().describe('Additional context for reasoning'), - }), - }, - { - name: 'kimi_review', - description: 'Code review analyzing bugs, security, performance, and style.', - inputSchema: z.object({ - code_or_path: z.string().describe('Code snippet or file path to review'), - focus: z.enum(['bugs', 'security', 'performance', 'style', 'all']).optional().describe('Review focus area (default: all)'), - workFolder: z.string().optional().describe('Working directory (absolute path)'), - }), - }, - { - name: 'kimi_research', - description: 'Research and analysis with 256K context window.', - inputSchema: z.object({ - question: z.string().describe('Research question or topic'), - context: z.string().optional().describe('Additional context or background'), - workFolder: z.string().optional().describe('Working directory (absolute path)'), - }), - }, - { - name: 'kimi_test', - description: 'Generate or fix comprehensive tests with edge cases.', - inputSchema: z.object({ - target: z.string().describe('Code or file path to generate tests for'), - instructions: z.string().optional().describe('Specific testing instructions'), - workFolder: z.string().optional().describe('Working directory (absolute path)'), - }), - }, -] as const; - -export const TOOL_MAP = new Map(TOOLS.map(t => [t.name, t])); - -export async function handleToolCall(toolName: string, args: Record): Promise { - const prompt = buildPrompt(toolName, args); - const thinking = toolName === 'kimi_think'; - const timeout = (typeof args.timeout === 'number' ? args.timeout : (toolName === 'kimi_agent' ? 300 : 120)); - - const output = await executeKimi({ - prompt, - workDir: args.workFolder as string | undefined, - thinking, - timeout, - }); - - if (toolName === 'kimi_think' && output.thinking) { - return `## Reasoning\n\n${output.thinking}\n\n## Response\n\n${output.text}`; - } - - return output.text || '(no output)'; -} - -export function buildPrompt(toolName: string, args: Record): string { - switch (toolName) { - case 'kimi_read_file': { - const path = args.path as string; - const offset = args.offset as number | undefined; - const limit = args.limit as number | undefined; - let prompt = `Read the file at ${path}`; - if (offset !== undefined) { - prompt += ` from line ${offset}`; - } - if (limit !== undefined) { - prompt += `, up to ${limit} lines`; - } - prompt += '. Output the file content.'; - return prompt; - } - - case 'kimi_read_media': { - const path = args.path as string; - const userPrompt = args.prompt as string | undefined; - return `Read the media file at ${path}. ${userPrompt || 'Describe this media file in detail.'}`; - } - - case 'kimi_write_file': { - const path = args.path as string; - const content = args.content as string; - return `Write the following content to the file ${path}:\n\n${content}`; - } - - case 'kimi_edit_file': { - const path = args.path as string; - const oldString = args.old_string as string; - const newString = args.new_string as string; - return `In the file ${path}, find the exact string:\n\`\`\`\n${oldString}\n\`\`\`\n\nAnd replace it with:\n\`\`\`\n${newString}\n\`\`\``; - } - - case 'kimi_glob': { - const pattern = args.pattern as string; - const path = args.path as string | undefined; - return `Find all files matching the glob pattern '${pattern}' under ${path || '.'}. List the matching file paths.`; - } - - case 'kimi_grep': { - const pattern = args.pattern as string; - const path = args.path as string | undefined; - const include = args.include as string | undefined; - let prompt = `Search for the regex pattern '${pattern}' in files under ${path || '.'}.`; - if (include) { - prompt += ` Only search in files matching '${include}'.`; - } - prompt += ' Show matching lines with file paths and line numbers.'; - return prompt; - } - - case 'kimi_shell': { - const command = args.command as string; - return `Execute this shell command and show the output:\n\`\`\`\n${command}\n\`\`\``; - } - - case 'kimi_web_search': { - const query = args.query as string; - const includeContent = args.include_content as boolean | undefined; - let prompt = `Search the web for: ${query}`; - if (includeContent) { - prompt += '\n\nInclude page content in the results.'; - } - return prompt; - } - - case 'kimi_fetch_url': { - const url = args.url as string; - const userPrompt = args.prompt as string | undefined; - return `Fetch the webpage at ${url}. ${userPrompt || 'Extract and return the main text content.'}`; - } - - case 'kimi_agent': { - return args.prompt as string; - } - - case 'kimi_think': { - const problem = args.problem as string; - const context = args.context as string | undefined; - let prompt = 'Think deeply about this problem. Do NOT execute any file operations or commands. Only reason and analyze.\n\n'; - prompt += `Problem: ${problem}`; - if (context) { - prompt += `\n\nContext: ${context}`; - } - return prompt; - } - - case 'kimi_review': { - const codeOrPath = args.code_or_path as string; - const focus = (args.focus as string | undefined) || 'all'; - const isFile = existsSync(codeOrPath); - - let prompt = isFile - ? `Review the code in file ${codeOrPath}` - : `Review the following code:\n\n\`\`\`\n${codeOrPath}\n\`\`\``; - - if (focus === 'all') { - prompt += '\n\nAnalyze for bugs, security issues, performance problems, and style concerns.'; - } else { - prompt += `\n\nFocus specifically on ${focus}.`; - } - - return prompt; - } - - case 'kimi_research': { - const question = args.question as string; - const context = args.context as string | undefined; - let prompt = `Research thoroughly:\n\n${question}`; - if (context) { - prompt += `\n\nAdditional context:\n${context}`; - } - return prompt; - } - - case 'kimi_test': { - const target = args.target as string; - const instructions = args.instructions as string | undefined; - let prompt = `Write comprehensive tests for: ${target}`; - if (instructions) { - prompt += `\n\nInstructions: ${instructions}`; - } - prompt += '\n\nInclude edge cases and error scenarios.'; - return prompt; - } - - default: - throw new Error(`Unknown tool: ${toolName}`); - } -} diff --git a/src/index.ts b/src/index.ts index ccb626a..c2755ad 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,114 +1,21 @@ #!/usr/bin/env node +/** + * Entrypoint: run the MCP server over stdio. This is the binary referenced by + * the `kimi-code-mcp` command and by MCP client configs (`npx kimi-code-mcp`). + */ -import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; -import { - CallToolRequestSchema, - ListToolsRequestSchema, -} from '@modelcontextprotocol/sdk/types.js'; -import type { z } from 'zod'; -import { TOOLS, TOOL_MAP, handleToolCall } from './handlers.js'; - -const server = new Server( - { name: 'kimi-code-mcp', version: '1.0.0' }, - { capabilities: { tools: {} } }, -); - -function zodToJsonSchema(schema: z.ZodObject): Record { - const shape = schema.shape; - const properties: Record = {}; - const required: string[] = []; - - for (const [key, value] of Object.entries(shape)) { - const zodValue = value as z.ZodTypeAny; - const prop: Record = {}; - - let inner = zodValue; - let isOptional = false; - if (inner._def.typeName === 'ZodOptional') { - isOptional = true; - inner = inner._def.innerType; - } - - switch (inner._def.typeName) { - case 'ZodString': - prop.type = 'string'; - break; - case 'ZodNumber': - prop.type = 'number'; - break; - case 'ZodBoolean': - prop.type = 'boolean'; - break; - case 'ZodEnum': - prop.type = 'string'; - prop.enum = inner._def.values; - break; - default: - prop.type = 'string'; - } - - if (inner._def.description) { - prop.description = inner._def.description; - } else if (zodValue._def.description) { - prop.description = zodValue._def.description; - } - - properties[key] = prop; - - if (!isOptional) { - required.push(key); - } - } - - return { - type: 'object', - properties, - ...(required.length > 0 ? { required } : {}), - }; -} - -server.setRequestHandler(ListToolsRequestSchema, async () => ({ - tools: TOOLS.map((tool) => ({ - name: tool.name, - description: tool.description, - inputSchema: zodToJsonSchema(tool.inputSchema), - })), -})); - -server.setRequestHandler(CallToolRequestSchema, async (request) => { - const { name, arguments: rawArgs } = request.params; - - const tool = TOOL_MAP.get(name); - if (!tool) { - return { - content: [{ type: 'text' as const, text: `Unknown tool: ${name}` }], - isError: true, - }; - } - - try { - const args = tool.inputSchema.parse(rawArgs ?? {}); - const result = await handleToolCall(name, args as Record); - return { - content: [{ type: 'text' as const, text: result }], - }; - } catch (error) { - const msg = error instanceof Error ? error.message : String(error); - return { - content: [{ type: 'text' as const, text: `Error: ${msg}` }], - isError: true, - }; - } -}); +import { createServer } from './server.js'; async function main(): Promise { + const server = createServer(); const transport = new StdioServerTransport(); await server.connect(transport); - console.error('kimi-code-mcp v1.0.0 started'); + // Never log to stdout — it is the MCP transport. Diagnostics go to stderr. + process.stderr.write('kimi-code-mcp ready (stdio)\n'); } -main().catch((error) => { - console.error('Fatal:', error); +main().catch((err: unknown) => { + process.stderr.write(`kimi-code-mcp fatal: ${(err as Error).message}\n`); process.exit(1); }); diff --git a/src/parser.ts b/src/parser.ts deleted file mode 100644 index 7bbd27f..0000000 --- a/src/parser.ts +++ /dev/null @@ -1,113 +0,0 @@ -import type { KimiOutput } from './cli.js'; - -/** - * Unescapes Python string literals found in kimi CLI output - */ -function unescapeString(str: string): string { - return str.replace(/\\(\\|n|'|")/g, (_, ch) => { - switch (ch) { - case '\\': return '\\'; - case 'n': return '\n'; - case "'": return "'"; - case '"': return '"'; - default: return ch; - } - }); -} - -/** - * Extracts text from TextPart(...) blocks in kimi CLI output - */ -export function extractTextParts(raw: string): string[] { - const textPartRegex = /TextPart\(type='text',\s*text='((?:[^'\\]|\\.)*)'\)/gs; - const parts: string[] = []; - - let match; - while ((match = textPartRegex.exec(raw)) !== null) { - parts.push(unescapeString(match[1])); - } - - return parts; -} - -/** - * Extracts thinking from ThinkPart(...) blocks in kimi CLI output - */ -export function extractThinkParts(raw: string): string[] { - const thinkPartRegex = /ThinkPart\(\s*type='think',\s*think='((?:[^'\\]|\\.)*)',\s*encrypted=/gs; - const parts: string[] = []; - - let match; - while ((match = thinkPartRegex.exec(raw)) !== null) { - parts.push(unescapeString(match[1])); - } - - return parts; -} - -/** - * Extracts plain text by filtering out structured kimi CLI lines - */ -export function extractPlainText(raw: string): string[] { - const lines = raw.split('\n'); - const filtered: string[] = []; - - const skipPrefixes = [ - 'TurnBegin', - 'TurnEnd', - 'StepBegin', - 'StepEnd', - 'StatusUpdate', - 'TokenUsage', - 'ToolUseBegin', - 'ToolUseEnd', - 'ToolResultPart', - 'ErrorPart', - 'ThinkPart', - ]; - - for (const line of lines) { - const trimmed = line.trim(); - - // Skip empty lines - if (!trimmed) continue; - - // Skip lines starting with known prefixes - if (skipPrefixes.some(prefix => trimmed.startsWith(prefix))) continue; - - // Skip lines starting with ( or ) - if (trimmed.startsWith('(') || trimmed.startsWith(')')) continue; - - // Skip 4-space-indented key= patterns - if (line.startsWith(' ') && /^\s+\w+=/.test(line)) continue; - - filtered.push(line); - } - - return filtered; -} - -/** - * Parses kimi CLI verbose output into structured format - */ -export function parseKimiOutput(raw: string): KimiOutput { - // Extract text parts - let textParts = extractTextParts(raw); - - // Fallback to plain text if no TextParts found - if (textParts.length === 0) { - textParts = extractPlainText(raw); - } - - const text = textParts.join(''); - - // Extract thinking parts - const thinkParts = extractThinkParts(raw); - const thinking = thinkParts.length > 0 ? thinkParts.join('\n\n') : undefined; - - return { - text, - thinking, - raw, - }; -} diff --git a/src/runner.test.ts b/src/runner.test.ts new file mode 100644 index 0000000..a363d60 --- /dev/null +++ b/src/runner.test.ts @@ -0,0 +1,169 @@ +import { describe, expect, it, vi } from 'vitest'; +import { loadConfig, type Config } from './config.js'; +import { + buildArgs, + capOutput, + captureSessionId, + classify, + detectQuota, + runKimi, + type ExecFn, + type RawRun, +} from './runner.js'; + +const cfg: Config = { + ...loadConfig({}), + maxRetries: 2, + retryBackoffMs: 1, // keep tests fast + timeoutMs: 1000, + maxOutputChars: 100, +}; + +function raw(over: Partial): RawRun { + return { stdout: '', stderr: '', exitCode: 0, signal: null, timedOut: false, ...over }; +} + +describe('buildArgs', () => { + it('builds text print mode with final-message-only and NO yolo by default', () => { + const args = buildArgs({ prompt: 'hi' }, cfg); + expect(args).toEqual(['--print', '--output-format', 'text', '-p', 'hi', '--final-message-only']); + expect(args).not.toContain('-y'); + }); + + it('adds -y only when yolo is opted in (per-call or config)', () => { + expect(buildArgs({ prompt: 'hi', yolo: true }, cfg)).toContain('-y'); + expect(buildArgs({ prompt: 'hi' }, { ...cfg, yolo: true })).toContain('-y'); + // Per-call override wins over config. + expect(buildArgs({ prompt: 'hi', yolo: false }, { ...cfg, yolo: true })).not.toContain('-y'); + }); + + it('omits final-message-only for stream-json and threads optional flags', () => { + const args = buildArgs( + { prompt: 'hi', format: 'stream-json', workDir: '/repo', model: 'k2', sessionId: 'abc', thinking: true }, + cfg, + ); + expect(args).toContain('stream-json'); + expect(args).not.toContain('--final-message-only'); + expect(args).toEqual(expect.arrayContaining(['-w', '/repo', '-m', 'k2', '-r', 'abc', '--thinking'])); + }); + + it('falls back to the configured default model', () => { + const withModel: Config = { ...cfg, defaultModel: 'kimi-for-coding' }; + expect(buildArgs({ prompt: 'x' }, withModel)).toEqual(expect.arrayContaining(['-m', 'kimi-for-coding'])); + }); +}); + +describe('detectQuota', () => { + it('detects the documented quota markers case-insensitively', () => { + expect(detectQuota('Error code: 429 - insufficient balance')).toBe(true); + expect(detectQuota('exceeded_current_quota_error')).toBe(true); + expect(detectQuota('all good here')).toBe(false); + }); +}); + +describe('classify', () => { + it('maps documented exit codes', () => { + expect(classify(raw({ exitCode: 0 }))).toBe('ok'); + expect(classify(raw({ exitCode: 75 }))).toBe('transient'); + expect(classify(raw({ exitCode: 1 }))).toBe('permanent'); + expect(classify(raw({ exitCode: 137, signal: 'SIGKILL' }))).toBe('permanent'); + }); + + it('treats a timeout as transient', () => { + expect(classify(raw({ exitCode: null, timedOut: true }))).toBe('transient'); + }); + + it('catches quota failures even when the CLI exits 0', () => { + // Observed in the wild: kimi prints the 429 and still returns 0. + expect(classify(raw({ exitCode: 0, stdout: 'Error code: 429 - insufficient balance' }))).toBe('quota'); + }); +}); + +describe('captureSessionId', () => { + it('extracts the resume id from the CLI hint', () => { + expect(captureSessionId('To resume this session: kimi -r 09c3f464-44ac-4e4a-9889-7d670197b2bb')).toBe( + '09c3f464-44ac-4e4a-9889-7d670197b2bb', + ); + expect(captureSessionId('no session here')).toBeUndefined(); + }); +}); + +describe('capOutput', () => { + it('passes short output through untouched', () => { + const { text, truncated } = capOutput('short', 100); + expect(truncated).toBe(false); + expect(text).toBe('short'); + }); + + it('truncates long output and flags it', () => { + const long = 'x'.repeat(500); + const { text, truncated } = capOutput(long, 100); + expect(truncated).toBe(true); + expect(text).toContain('output truncated'); + }); +}); + +describe('runKimi orchestration', () => { + it('returns parsed text on success', async () => { + const exec: ExecFn = vi.fn(async () => + raw({ exitCode: 0, stdout: 'PONG', stderr: 'To resume this session: kimi -r abc123de' }), + ); + const res = await runKimi('/bin/kimi', { prompt: 'ping' }, cfg, exec); + expect(res.ok).toBe(true); + expect(res.text).toBe('PONG'); + expect(res.sessionId).toBe('abc123de'); + expect(res.attempts).toBe(1); + }); + + it('retries a transient failure then succeeds', async () => { + let calls = 0; + const exec: ExecFn = vi.fn(async () => { + calls += 1; + return calls < 2 ? raw({ exitCode: 75 }) : raw({ exitCode: 0, stdout: 'ok' }); + }); + const res = await runKimi('/bin/kimi', { prompt: 'x' }, cfg, exec); + expect(res.ok).toBe(true); + expect(res.attempts).toBe(2); + expect(exec).toHaveBeenCalledTimes(2); + }); + + it('does not retry a quota failure and surfaces a recharge hint', async () => { + const exec: ExecFn = vi.fn(async () => raw({ exitCode: 0, stdout: 'exceeded_current_quota_error' })); + const res = await runKimi('/bin/kimi', { prompt: 'x' }, cfg, exec); + expect(res.ok).toBe(false); + expect(res.classification).toBe('quota'); + expect(res.error).toMatch(/recharge/i); + expect(exec).toHaveBeenCalledTimes(1); + }); + + it('gives up after exhausting retries on persistent transient errors', async () => { + const exec: ExecFn = vi.fn(async () => raw({ exitCode: 75 })); + const res = await runKimi('/bin/kimi', { prompt: 'x' }, cfg, exec); + expect(res.ok).toBe(false); + expect(res.classification).toBe('transient'); + expect(exec).toHaveBeenCalledTimes(cfg.maxRetries + 1); + }); + + it('reports a spawn failure as permanent without retrying', async () => { + const exec: ExecFn = vi.fn(async () => { + throw new Error('spawn ENOENT'); + }); + const res = await runKimi('/bin/kimi', { prompt: 'x' }, cfg, exec); + expect(res.ok).toBe(false); + expect(res.classification).toBe('permanent'); + expect(res.error).toMatch(/ENOENT/); + expect(exec).toHaveBeenCalledTimes(1); + }); + + it('parses stream-json output into final text and events', async () => { + const jsonl = + JSON.stringify({ role: 'user', content: 'ping' }) + + '\n' + + JSON.stringify({ role: 'assistant', content: 'PONG' }); + const exec: ExecFn = vi.fn(async () => raw({ exitCode: 0, stdout: jsonl })); + const res = await runKimi('/bin/kimi', { prompt: 'ping', format: 'stream-json' }, cfg, exec); + expect(res.ok).toBe(true); + expect(res.text).toBe('PONG'); + expect(res.events).toHaveLength(2); + }); +}); diff --git a/src/runner.ts b/src/runner.ts new file mode 100644 index 0000000..e216c1b --- /dev/null +++ b/src/runner.ts @@ -0,0 +1,270 @@ +/** + * The bridge between an MCP tool call and a headless Kimi invocation. + * + * Responsibilities, each isolated and unit-tested: + * - assemble the print-mode argument vector + * - classify the outcome (Kimi's documented exit codes: 0 ok, 1 permanent, + * 75 transient) AND catch failures that still exit 0 (quota / balance + * errors are printed as a normal message — observed in the wild) + * - retry transient failures with exponential backoff + * - enforce a wall-clock timeout with a process-group kill + * - capture the resume session id and cap oversized output + * + * The actual process spawn is injected (`exec`) so the orchestration logic can + * be tested without launching a real CLI. + */ + +import { spawn } from 'node:child_process'; +import type { Config } from './config.js'; +import { parseStreamJson, type StreamEvent } from './stream.js'; + +export type OutputFormat = 'text' | 'stream-json'; + +export interface RunRequest { + prompt: string; + workDir?: string; + model?: string; + sessionId?: string; + thinking?: boolean; + format?: OutputFormat; + /** Auto-approve Kimi's tool actions. Overrides the config default when set. */ + yolo?: boolean; +} + +/** Raw result of one process launch, before classification. */ +export interface RawRun { + stdout: string; + stderr: string; + exitCode: number | null; + signal: NodeJS.Signals | null; + timedOut: boolean; +} + +export type Classification = 'ok' | 'quota' | 'transient' | 'permanent'; + +export interface KimiResult { + ok: boolean; + classification: Classification; + text: string; + events?: StreamEvent[]; + sessionId?: string; + error?: string; + attempts: number; + truncated: boolean; +} + +export type ExecFn = ( + bin: string, + args: string[], + opts: { cwd?: string; timeoutMs: number; debug: boolean }, +) => Promise; + +const QUOTA_MARKERS = [ + 'exceeded_current_quota_error', + 'insufficient balance', + 'insufficient_quota', + 'Error code: 429', + 'check your plan and billing', +]; + +const SESSION_RE = /kimi\s+-r\s+([0-9a-f-]{8,})/i; + +/** Build the print-mode argv. `text` mode adds `--final-message-only` for a clean answer. */ +export function buildArgs(req: RunRequest, cfg: Config): string[] { + const format: OutputFormat = req.format ?? 'text'; + const args = ['--print', '--output-format', format, '-p', req.prompt]; + + // Auto-approve tool actions only when explicitly opted in (per-call or config). + if (req.yolo ?? cfg.yolo) args.push('-y'); + + if (format === 'text') args.push('--final-message-only'); + + const model = req.model?.trim() || cfg.defaultModel; + if (model) args.push('-m', model); + + if (req.workDir) args.push('-w', req.workDir); + if (req.sessionId) args.push('-r', req.sessionId); + if (req.thinking) args.push('--thinking'); + + return args; +} + +export function detectQuota(text: string): boolean { + const lower = text.toLowerCase(); + return QUOTA_MARKERS.some((m) => lower.includes(m.toLowerCase())); +} + +export function captureSessionId(text: string): string | undefined { + const match = SESSION_RE.exec(text); + return match?.[1]; +} + +/** Map a raw run to a classification, accounting for exit-0 quota failures. */ +export function classify(run: RawRun): Classification { + if (run.timedOut) return 'transient'; + if (detectQuota(`${run.stdout}\n${run.stderr}`)) return 'quota'; + if (run.exitCode === 0) return 'ok'; + if (run.exitCode === 75) return 'transient'; + // 1 = config/auth (permanent); anything else (incl. signal kill) is permanent too. + return 'permanent'; +} + +/** Truncate at a newline boundary near the limit so output never splits mid-line. */ +export function capOutput(text: string, max: number): { text: string; truncated: boolean } { + if (text.length <= max) return { text, truncated: false }; + const slice = text.slice(0, max); + const lastNl = slice.lastIndexOf('\n'); + const boundary = lastNl > max * 0.8 ? lastNl : slice.length; + return { + text: `${slice.slice(0, boundary)}\n\n…[output truncated at ${boundary} of ${text.length} chars]`, + truncated: true, + }; +} + +const sleep = (ms: number): Promise => new Promise((r) => setTimeout(r, ms)); + +/** Default executor: spawn the real binary, capture output, kill the group on timeout. */ +export const defaultExec: ExecFn = (bin, args, opts) => + new Promise((resolve, reject) => { + const env = { ...process.env }; + const child = spawn(bin, args, { + cwd: opts.cwd, + env, + detached: true, // own process group, so we can group-kill descendants + stdio: ['ignore', 'pipe', 'pipe'], + }); + + let stdout = ''; + let stderr = ''; + let timedOut = false; + let settled = false; + + const timer = setTimeout(() => { + timedOut = true; + if (child.pid) { + try { + process.kill(-child.pid, 'SIGKILL'); + } catch { + child.kill('SIGKILL'); + } + } + }, opts.timeoutMs); + + child.stdout.on('data', (d: Buffer) => { + stdout += d.toString(); + }); + child.stderr.on('data', (d: Buffer) => { + stderr += d.toString(); + if (opts.debug) process.stderr.write(d); + }); + + child.on('error', (err) => { + if (settled) return; + settled = true; + clearTimeout(timer); + reject(err); + }); + + child.on('close', (code, signal) => { + if (settled) return; + settled = true; + clearTimeout(timer); + resolve({ stdout, stderr, exitCode: code, signal, timedOut }); + }); + }); + +/** Run Kimi with retry-on-transient. `exec` is injectable for tests. */ +export async function runKimi( + bin: string, + req: RunRequest, + cfg: Config, + exec: ExecFn = defaultExec, +): Promise { + const args = buildArgs(req, cfg); + const format: OutputFormat = req.format ?? 'text'; + let lastRun: RawRun | undefined; + let lastError: string | undefined; + + for (let attempt = 1; attempt <= cfg.maxRetries + 1; attempt++) { + let run: RawRun; + try { + run = await exec(bin, args, { + cwd: req.workDir, + timeoutMs: cfg.timeoutMs, + debug: cfg.debug, + }); + } catch (err) { + // Spawn-level failure (e.g. ENOENT): not retryable. + return { + ok: false, + classification: 'permanent', + text: '', + error: `Failed to launch Kimi: ${(err as Error).message}`, + attempts: attempt, + truncated: false, + }; + } + + lastRun = run; + const kind = classify(run); + + if (kind === 'ok') { + const combined = `${run.stdout}\n${run.stderr}`; + const sessionId = captureSessionId(combined); + let text: string; + let events: StreamEvent[] | undefined; + + if (format === 'stream-json') { + const parsed = parseStreamJson(run.stdout); + text = parsed.finalText || run.stdout.trim(); + events = parsed.events; + } else { + text = run.stdout.trim(); + } + + const capped = capOutput(text, cfg.maxOutputChars); + const result: KimiResult = { + ok: true, + classification: 'ok', + text: capped.text, + attempts: attempt, + truncated: capped.truncated, + }; + if (events) result.events = events; + if (sessionId) result.sessionId = sessionId; + return result; + } + + lastError = errorMessage(kind, run); + + if (kind === 'transient' && attempt <= cfg.maxRetries) { + await sleep(cfg.retryBackoffMs * 2 ** (attempt - 1)); + continue; + } + // Permanent or quota, or retries exhausted. + break; + } + + return { + ok: false, + classification: lastRun ? classify(lastRun) : 'permanent', + text: lastRun ? capOutput(lastRun.stdout.trim(), cfg.maxOutputChars).text : '', + error: lastError ?? 'Unknown failure', + attempts: cfg.maxRetries + 1, + truncated: false, + }; +} + +function errorMessage(kind: Classification, run: RawRun): string { + const tail = (run.stderr || run.stdout).trim().slice(-600); + switch (kind) { + case 'quota': + return `Kimi account out of balance / quota exceeded. Recharge at https://www.kimi.com/code, then retry.\n\n${tail}`; + case 'transient': + return run.timedOut + ? `Kimi timed out after the configured budget.` + : `Kimi hit a transient error (rate limit / exit 75).\n\n${tail}`; + default: + return `Kimi failed (exit ${run.exitCode ?? 'signal:' + run.signal}). Often a config or auth problem — try \`kimi login\`.\n\n${tail}`; + } +} diff --git a/src/server.test.ts b/src/server.test.ts new file mode 100644 index 0000000..51e3e09 --- /dev/null +++ b/src/server.test.ts @@ -0,0 +1,64 @@ +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'; +import { describe, expect, it } from 'vitest'; +import { loadConfig } from './config.js'; +import type { ExecFn, RawRun } from './runner.js'; +import { createServer } from './server.js'; + +function fakeExec(over: Partial): ExecFn { + return async () => ({ stdout: '', stderr: '', exitCode: 0, signal: null, timedOut: false, ...over }); +} + +async function connect(exec: ExecFn) { + const server = createServer({ config: { ...loadConfig({}), binOverride: '/fake/kimi' }, exec }); + const [clientT, serverT] = InMemoryTransport.createLinkedPair(); + await server.connect(serverT); + const client = new Client({ name: 'test', version: '0.0.0' }); + await client.connect(clientT); + return { client, server }; +} + +describe('MCP server', () => { + it('exposes exactly the three generic tools', async () => { + const { client, server } = await connect(fakeExec({ stdout: 'ok' })); + const { tools } = await client.listTools(); + const names = tools.map((t) => t.name).sort(); + expect(names).toEqual(['kimi_resume', 'kimi_run', 'kimi_status']); + await server.close(); + }); + + it('kimi_run returns Kimi output as text', async () => { + const { client, server } = await connect(fakeExec({ stdout: 'the answer' })); + const res = await client.callTool({ name: 'kimi_run', arguments: { prompt: 'q' } }); + const content = res.content as Array<{ type: string; text: string }>; + expect(content[0]?.text).toContain('the answer'); + expect(res.isError).toBeFalsy(); + await server.close(); + }); + + it('kimi_run surfaces a quota failure as an error', async () => { + const { client, server } = await connect(fakeExec({ stdout: 'exceeded_current_quota_error' })); + const res = await client.callTool({ name: 'kimi_run', arguments: { prompt: 'q' } }); + expect(res.isError).toBe(true); + const content = res.content as Array<{ type: string; text: string }>; + expect(content[0]?.text).toMatch(/recharge/i); + await server.close(); + }); + + it('kimi_query reports a missing binary with an install hint', async () => { + // No binOverride and a binary that cannot resolve via an impossible PATH. + const server = createServer({ + config: { ...loadConfig({}), binOverride: '' }, + exec: fakeExec({ stdout: 'x' }), + }); + const [clientT, serverT] = InMemoryTransport.createLinkedPair(); + await server.connect(serverT); + const client = new Client({ name: 'test', version: '0.0.0' }); + await client.connect(clientT); + // This host may or may not have kimi installed; just assert the call resolves + // to a well-formed result either way. + const res = await client.callTool({ name: 'kimi_run', arguments: { prompt: 'q' } }); + expect(Array.isArray(res.content)).toBe(true); + await server.close(); + }); +}); diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 0000000..552c50c --- /dev/null +++ b/src/server.ts @@ -0,0 +1,33 @@ +/** + * Assemble the MCP server: construct it, register the tool surface, and expose + * a factory so both the entrypoint and tests build it the same way. + */ + +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { loadConfig, SERVER_NAME, SERVER_VERSION, type Config } from './config.js'; +import { registerTools } from './tools/index.js'; +import type { ExecFn } from './runner.js'; + +export interface CreateServerOptions { + config?: Config; + /** Injectable executor, for tests. */ + exec?: ExecFn; +} + +export function createServer(opts: CreateServerOptions = {}): McpServer { + const cfg = opts.config ?? loadConfig(); + const server = new McpServer( + { name: SERVER_NAME, version: SERVER_VERSION }, + { + capabilities: { tools: {} }, + instructions: + 'Delegate work to the Kimi CLI agent: kimi_run sends a prompt (optionally scoped to a working directory) ' + + 'and returns Kimi’s answer, kimi_resume continues a prior session, kimi_status checks the CLI is installed ' + + 'and authenticated.', + }, + ); + + const ctx = opts.exec ? { cfg, exec: opts.exec } : { cfg }; + registerTools(server, ctx); + return server; +} diff --git a/src/stream.test.ts b/src/stream.test.ts new file mode 100644 index 0000000..8663193 --- /dev/null +++ b/src/stream.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from 'vitest'; +import { parseStreamJson } from './stream.js'; + +describe('parseStreamJson', () => { + it('accumulates assistant text across lines', () => { + const raw = [ + JSON.stringify({ role: 'user', content: 'hi' }), + JSON.stringify({ role: 'assistant', content: 'Hello ' }), + JSON.stringify({ role: 'assistant', content: 'world' }), + ].join('\n'); + const res = parseStreamJson(raw); + expect(res.finalText).toBe('Hello world'); + expect(res.events).toHaveLength(3); + }); + + it('flattens array-form content parts', () => { + const raw = JSON.stringify({ + role: 'assistant', + content: [ + { type: 'text', text: 'a' }, + { type: 'text', text: 'b' }, + ], + }); + expect(parseStreamJson(raw).finalText).toBe('ab'); + }); + + it('captures tool calls', () => { + const raw = JSON.stringify({ + role: 'assistant', + content: '', + tool_calls: [{ function: { name: 'read_file', arguments: '{"path":"x"}' } }], + }); + const res = parseStreamJson(raw); + expect(res.events[0]?.toolCalls[0]).toEqual({ name: 'read_file', arguments: '{"path":"x"}' }); + }); + + it('survives malformed and banner lines without throwing', () => { + const raw = ['not json at all', '{"role":"assistant","content":"ok"}', ' ', '{bad'].join('\n'); + const res = parseStreamJson(raw); + expect(res.finalText).toBe('ok'); + expect(res.unparsed).toContain('not json at all'); + expect(res.unparsed).toContain('{bad'); + }); + + it('ignores objects without a role', () => { + const res = parseStreamJson('{"foo":"bar"}'); + expect(res.events).toHaveLength(0); + expect(res.unparsed).toHaveLength(1); + }); +}); diff --git a/src/stream.ts b/src/stream.ts new file mode 100644 index 0000000..3c910ac --- /dev/null +++ b/src/stream.ts @@ -0,0 +1,96 @@ +/** + * Parser for Kimi's `--output-format stream-json` mode (JSONL: one JSON object + * per line). Each line is a chat message; we accumulate assistant text and a + * lightweight tool-call trace so callers can show what the agent actually did. + * + * The parser is deliberately defensive: Kimi mixes machine lines with the + * occasional human-readable banner, and a single malformed line must never + * sink an otherwise-good run. + */ + +export interface ToolCall { + name: string; + arguments?: string; +} + +export interface StreamEvent { + role: 'user' | 'assistant' | 'tool' | string; + text: string; + toolCalls: ToolCall[]; +} + +export interface StreamResult { + /** Concatenated assistant text — the answer the user cares about. */ + finalText: string; + events: StreamEvent[]; + /** Lines that were not valid JSON (kept for debugging only). */ + unparsed: string[]; +} + +interface RawMessage { + role?: string; + content?: unknown; + tool_calls?: Array<{ function?: { name?: string; arguments?: string } }>; +} + +/** Flatten Kimi's content field — a string, or an array of `{type,text}` parts. */ +function extractText(content: unknown): string { + if (typeof content === 'string') return content; + if (Array.isArray(content)) { + return content + .map((part) => { + if (typeof part === 'string') return part; + if (part && typeof part === 'object' && 'text' in part) { + const t = (part as { text?: unknown }).text; + return typeof t === 'string' ? t : ''; + } + return ''; + }) + .join(''); + } + return ''; +} + +export function parseStreamJson(raw: string): StreamResult { + const events: StreamEvent[] = []; + const unparsed: string[] = []; + const assistantChunks: string[] = []; + + for (const line of raw.split('\n')) { + const trimmed = line.trim(); + if (trimmed === '') continue; + + let msg: RawMessage; + try { + msg = JSON.parse(trimmed) as RawMessage; + } catch { + unparsed.push(trimmed); + continue; + } + if (typeof msg !== 'object' || msg === null || typeof msg.role !== 'string') { + unparsed.push(trimmed); + continue; + } + + const text = extractText(msg.content); + const toolCalls: ToolCall[] = Array.isArray(msg.tool_calls) + ? msg.tool_calls + .map((tc) => tc.function) + .filter((fn): fn is { name?: string; arguments?: string } => fn != null) + .map((fn) => { + const call: ToolCall = { name: fn.name ?? '(unknown)' }; + if (fn.arguments !== undefined) call.arguments = fn.arguments; + return call; + }) + : []; + + events.push({ role: msg.role, text, toolCalls }); + if (msg.role === 'assistant' && text !== '') assistantChunks.push(text); + } + + return { + finalText: assistantChunks.join('').trim(), + events, + unparsed, + }; +} diff --git a/src/tools/index.ts b/src/tools/index.ts new file mode 100644 index 0000000..b1b278c --- /dev/null +++ b/src/tools/index.ts @@ -0,0 +1,95 @@ +/** + * Tool surface — a thin, generic wrapper over the Kimi CLI's headless + * (print) mode. The caller supplies the prompt verbatim; this server adds no + * prompt engineering of its own. Three tools: run, resume, status. + */ + +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { z } from 'zod'; +import { getKimiStatus } from '../bin-resolver.js'; +import { delegate, type ToolContext } from './shared.js'; + +export function registerTools(server: McpServer, ctx: ToolContext): void { + server.registerTool( + 'kimi_run', + { + title: 'Kimi: run a prompt', + description: + 'Send a prompt to the Kimi CLI agent (headless print mode) and return its final answer. ' + + 'Optionally scope it to a working directory so Kimi can read that codebase.', + inputSchema: { + prompt: z.string().min(1).describe('The prompt to send to Kimi.'), + path: z + .string() + .optional() + .describe('Absolute working directory Kimi runs in. Defaults to the server process cwd.'), + model: z.string().optional().describe('Override the Kimi model for this run.'), + thinking: z.boolean().optional().describe('Enable extended thinking mode.'), + yolo: z + .boolean() + .optional() + .describe('Auto-approve Kimi’s tool actions so it can edit files and run shell unsupervised. Default off.'), + }, + }, + async (args) => { + const req: Parameters[1] = { prompt: args.prompt }; + if (args.path !== undefined) req.workDir = args.path; + if (args.model !== undefined) req.model = args.model; + if (args.thinking !== undefined) req.thinking = args.thinking; + if (args.yolo !== undefined) req.yolo = args.yolo; + return delegate(ctx, req); + }, + ); + + server.registerTool( + 'kimi_resume', + { + title: 'Kimi: resume a session', + description: + 'Continue a previous Kimi session by id (returned in earlier tool output) with a follow-up prompt, ' + + 'preserving the agent’s prior context.', + inputSchema: { + sessionId: z.string().min(1).describe('The session id from a previous run.'), + prompt: z.string().min(1).describe('The follow-up prompt.'), + path: z.string().optional().describe('Absolute working directory for the run.'), + model: z.string().optional().describe('Override the Kimi model for this run.'), + yolo: z + .boolean() + .optional() + .describe('Auto-approve Kimi’s tool actions so it can edit files and run shell unsupervised. Default off.'), + }, + }, + async (args) => { + const req: Parameters[1] = { prompt: args.prompt, sessionId: args.sessionId }; + if (args.path !== undefined) req.workDir = args.path; + if (args.model !== undefined) req.model = args.model; + if (args.yolo !== undefined) req.yolo = args.yolo; + return delegate(ctx, req); + }, + ); + + server.registerTool( + 'kimi_status', + { + title: 'Kimi: health check', + description: + 'Report whether the Kimi CLI is installed, which binary is in use (native vs legacy), its version, and ' + + 'whether you are authenticated. Call this first when other tools fail.', + inputSchema: {}, + }, + async () => { + const s = await getKimiStatus(ctx.cfg.binOverride); + const lines = [ + `installed: ${s.installed ? 'yes' : 'no'}`, + `binary: ${s.binPath ?? '(not found)'} [${s.origin}]`, + `version: ${s.version ?? '(unknown)'}`, + `authenticated: ${s.auth.authenticated ? 'yes' : 'no'}${s.auth.source ? ` (${s.auth.source})` : ''}`, + ]; + if (s.error) lines.push('', `note: ${s.error}`); + return { + content: [{ type: 'text', text: lines.join('\n') }], + isError: !s.installed, + }; + }, + ); +} diff --git a/src/tools/shared.ts b/src/tools/shared.ts new file mode 100644 index 0000000..7d094ec --- /dev/null +++ b/src/tools/shared.ts @@ -0,0 +1,45 @@ +/** + * Shared plumbing for every tool: resolve the binary just-in-time, run Kimi, + * and render the result as an MCP tool response. Resolving per-call (not at + * startup) means the server recovers automatically once a user installs or + * logs in to Kimi, with no restart. + */ + +import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import { INSTALL_HINT, resolveKimiBin } from '../bin-resolver.js'; +import type { Config } from '../config.js'; +import { runKimi, type ExecFn, type RunRequest } from '../runner.js'; + +export interface ToolContext { + cfg: Config; + /** Injectable for tests; defaults to the real spawn inside runKimi. */ + exec?: ExecFn; +} + +function textResult(text: string, isError = false): CallToolResult { + return { content: [{ type: 'text', text }], isError }; +} + +/** Run a delegated Kimi request and format the outcome for the MCP client. */ +export async function delegate(ctx: ToolContext, req: RunRequest): Promise { + const resolved = resolveKimiBin(ctx.cfg.binOverride); + if (!resolved) { + return textResult(INSTALL_HINT, true); + } + + const result = await runKimi(resolved.path, req, ctx.cfg, ctx.exec); + + if (!result.ok) { + return textResult(result.error ?? 'Kimi failed with no error message.', true); + } + + const footer: string[] = []; + if (result.sessionId) { + footer.push(`session: ${result.sessionId} (resume with kimi_resume)`); + } + if (result.attempts > 1) footer.push(`attempts: ${result.attempts}`); + if (result.truncated) footer.push('output truncated'); + + const body = footer.length > 0 ? `${result.text}\n\n---\n${footer.join(' · ')}` : result.text; + return textResult(body); +} diff --git a/tsconfig.json b/tsconfig.json index 2c90ef2..cb2dc66 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,17 +1,24 @@ { "compilerOptions": { - "target": "ES2022", + "target": "ES2023", "module": "NodeNext", "moduleResolution": "NodeNext", - "lib": ["ES2022"], + "lib": ["ES2023"], "outDir": "./dist", "rootDir": "./src", "strict": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "exactOptionalPropertyTypes": false, "esModuleInterop": true, "skipLibCheck": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true + "resolveJsonModule": true, + "verbatimModuleSyntax": true }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "src/**/*.test.ts", "src/__tests__/**"] + "exclude": ["node_modules", "dist", "src/**/*.test.ts"] } diff --git a/vitest.config.ts b/vitest.config.ts index a88bca3..9bd0422 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -2,13 +2,13 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { - globals: true, environment: 'node', include: ['src/**/*.test.ts'], coverage: { provider: 'v8', reporter: ['text', 'html', 'lcov'], - exclude: ['**/node_modules/**', '**/dist/**', '**/*.test.ts', '**/mocks.ts'], + include: ['src/**/*.ts'], + exclude: ['src/**/*.test.ts', 'src/index.ts'], }, }, });