diff --git a/mcp/.dockerignore b/mcp/.dockerignore new file mode 100644 index 0000000000..d6da748a59 --- /dev/null +++ b/mcp/.dockerignore @@ -0,0 +1,19 @@ +# Rebuilt inside the image +node_modules/ +dist/ + +# Local-only / secrets / caches — never bake into image +.cache/ +models/ +config.local.json +*.db +*.db-wal +*.db-shm +*.db.tmp +*.log + +# Not needed at runtime +.git/ +examples/ +*.test.ts +vitest*.ts diff --git a/mcp/.gitignore b/mcp/.gitignore new file mode 100644 index 0000000000..b908ed1bfb --- /dev/null +++ b/mcp/.gitignore @@ -0,0 +1,17 @@ +node_modules/ +dist/ +*.log + +# Built knowledge index (SQLite) — DO NOT commit +*.db +*.db-wal +*.db-shm +*.db.tmp + +# Downloaded embedding models / cache +/models/ +.cache/ + +# Local config with secrets +config.local.json +config.*.local.json diff --git a/mcp/Dockerfile b/mcp/Dockerfile new file mode 100644 index 0000000000..e2a595c923 --- /dev/null +++ b/mcp/Dockerfile @@ -0,0 +1,62 @@ +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Eclipse Public License 2.0 which is available at +# http://www.eclipse.org/legal/epl-2.0 +# +# SPDX-License-Identifier: EPL-2.0 + +# ---- builder: compile native deps + tsc, then drop devDeps ---- +# node:22-slim = Debian (glibc). Do NOT use alpine: onnxruntime-node +# (pulled by @huggingface/transformers) ships no musl prebuild. +FROM node:22-slim AS builder +WORKDIR /app + +# Toolchain so better-sqlite3 can build from source if no prebuilt +# binary matches this platform/ABI. Builder is throwaway — not shipped. +RUN apt-get update && apt-get install -y --no-install-recommends \ + python3 make g++ ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# Install with full lockfile first (better layer caching). +COPY package.json package-lock.json ./ +RUN npm ci + +# Build TypeScript -> dist/ +COPY tsconfig.json ./ +COPY src ./src +RUN npm run build + +# Strip devDeps but keep the compiled native .node binaries. +RUN npm prune --omit=dev + +# ---- runtime: slim image, prod deps + dist only, non-root ---- +FROM node:22-slim AS runtime +WORKDIR /app +ENV NODE_ENV=production + +# Same base/arch as builder -> copying node_modules keeps native bindings valid. +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/package.json ./package.json + +# Baked default config (http, 0.0.0.0). Override by bind-mounting a file +# and pointing DITTO_MCP_CONFIG at it. +COPY docker/config.docker.json ./config.docker.json +ENV DITTO_MCP_CONFIG=/app/config.docker.json + +# Writable dir for the sqlite knowledge index (see config.docker.json). +# Mount a volume here to persist across restarts. +RUN mkdir -p /app/data + +# Run unprivileged. +RUN useradd --system --uid 10001 --home-dir /app ditto \ + && chown -R ditto:ditto /app +USER ditto +VOLUME ["/app/data"] + +EXPOSE 3000 +CMD ["node", "dist/bin/http.js"] diff --git a/mcp/README.md b/mcp/README.md new file mode 100644 index 0000000000..6572686eb6 --- /dev/null +++ b/mcp/README.md @@ -0,0 +1,493 @@ +# Ditto MCP Server + +Model Context Protocol (MCP) server for Eclipse Ditto. Provides two classes of tools: + +1. **Knowledge tools** (`search`, `get_chunk`) — semantic/keyword search over Ditto documentation (public llms.txt + optional local markdown corpus) +2. **Action tools** (dynamically generated from OpenAPI) — query and manage Things, Policies, Connections, and other Ditto resources + +## Requirements + +- Node >= 22 + +## Quickstart + +### Install & Build + +```bash +cd mcp/ +npm install +npm run build +``` + +### Run (stdio transport) + +The MCP server runs in stdio mode by default (for Claude Desktop, Cline, and other MCP clients): + +```bash +# Development (with tsx) +npm run dev:stdio + +# Production (built) +node dist/bin/stdio.js +``` + +### Connect to Claude Desktop + +Add the server to Claude Desktop's MCP config: + +```bash +claude mcp add ditto \ + -e DITTO_MCP_CONFIG=/absolute/path/to/config.json \ + -- node /absolute/path/to/ditto/mcp/dist/bin/stdio.js +``` + +Or edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) / `%APPDATA%\Claude\claude_desktop_config.json` (Windows) directly: + +```json +{ + "mcpServers": { + "ditto": { + "command": "node", + "args": ["/absolute/path/to/ditto/mcp/dist/bin/stdio.js"], + "env": { + "DITTO_MCP_CONFIG": "/absolute/path/to/config.json" + } + } + } +} +``` + +### Run (HTTP transport) + +The server can also run over HTTP (streamable SSE transport): + +```bash +# Development +npm run dev:http + +# Production +node dist/bin/http.js +``` + +By default, the HTTP server binds to `127.0.0.1:3000` and serves at `/mcp`. Configure via `server.http` in the config (see below). + +## Docker Deployment + +The HTTP transport is the intended production deployment: one long-lived container that MCP clients reach over a URL. The image bundles all native dependencies (`better-sqlite3`, `sqlite-vec`, `onnxruntime-node`) so consumers need no build toolchain. (stdio transport is for local development only.) + +The repo ships a multi-stage `Dockerfile` and a baked default config at `docker/config.docker.json` (binds `0.0.0.0:3000`, sqlite index under `/app/data`). + +### Build & run + +```bash +docker build -t ditto-mcp-server:0.1.0 . + +docker run -d -p 3000:3000 \ + -v ditto-data:/app/data \ + ditto-mcp-server:0.1.0 +``` + +Point your MCP client at the HTTP endpoint: + +```json +{ "mcpServers": { "ditto": { "url": "http://your-host:3000/mcp" } } } +``` + +### ⚠️ Required: set `allowedHosts` for your hostname + +The baked config keeps DNS rebinding protection **on**, with `allowedHosts` limited to `localhost:3000` / `127.0.0.1:3000`. A request whose `Host` header is not on that list is rejected with **HTTP 403 `Invalid Host header`** — so a container reached via a service name, public hostname, or reverse proxy is unreachable until you add that host. + +Override the config with your real hostname (see the [Server HTTP options](#server-http-transport-options) table): + +```json +{ + "server": { + "http": { + "host": "0.0.0.0", + "port": 3000, + "enableDnsRebindingProtection": true, + "allowedHosts": ["ditto-mcp.internal:3000", "mcp.example.com"] + } + } +} +``` + +Mount it and point the server at it: + +```bash +docker run -d -p 3000:3000 \ + -v ditto-data:/app/data \ + -v /path/to/config.json:/app/config.docker.json:ro \ + ditto-mcp-server:0.1.0 +``` + +Disabling `enableDnsRebindingProtection` is only appropriate when the container sits behind a proxy or network boundary that validates the `Host` header for you — do not ship it disabled on a directly exposed service. + +### Publishing to a private registry (e.g. Artifactory) + +Artifactory (and most registries) host Docker images directly — no npm publish needed. + +```bash +docker login your.artifactory.com # user + API token +docker build -t your.artifactory.com/docker-local/ditto-mcp-server:0.1.0 . +docker push your.artifactory.com/docker-local/ditto-mcp-server:0.1.0 +``` + +Consumers then `docker run your.artifactory.com/docker-local/ditto-mcp-server:0.1.0`. + +### Vector / hybrid retrieval in containers + +The default `retriever: fts` needs no embedding model. If you switch to `vector` or `hybrid`, the bge model is downloaded at runtime from the Hugging Face Hub — which fails in air-gapped networks. For offline use, either bake the model into the image and set `knowledge.embedding.modelPath`, or mount a pre-populated cache and set `knowledge.embedding.cacheDir`. Persist the built index with the ingest process (see below) so the container doesn't rebuild in memory on every start. + +## Two Processes: Ingest vs. Server + +The MCP server supports **persistent knowledge indexes** (SQLite or Postgres). The index must be built **before** the server starts (or the server falls back to building it in memory at startup). + +- **Ingest process** (`ditto-mcp-ingest` or `npm run dev:ingest`) — builds and persists the knowledge index (fetch → chunk → embed → write to store). Run this once, or whenever your corpus changes. +- **Server process** (`ditto-mcp-stdio` / `ditto-mcp-http`) — serves MCP tools. Reads the prebuilt index if it exists and is valid; otherwise builds in memory (SQLite) or refuses to start (Postgres). + +**When to run ingest:** + +- After changing `knowledge.retriever`, `knowledge.embedding.model`, or `knowledge.embedding.dim` (metadata mismatch requires a rebuild). +- After adding/removing sources (`publicSource`, `localDir`). +- When you want to persist the index to disk (SQLite) or Postgres (pgvector). + +**Ingest command:** + +```bash +DITTO_MCP_CONFIG=/path/to/config.json npm run dev:ingest # development +DITTO_MCP_CONFIG=/path/to/config.json ditto-mcp-ingest # production +``` + +The ingest command reads `knowledge.store` from your config and writes the index to the configured location (SQLite file path or Postgres connection). If the config specifies `kind: "sqlite"` but no `sqlite.path`, ingest will error (it requires an explicit path to write to). + +## Configuration + +All configuration is optional. The server uses sensible defaults when no config is provided. Pass a JSON config file via the `DITTO_MCP_CONFIG` environment variable. + +See `examples/` for reference configs. All examples are parse-tested in CI and won't rot. + +### Server (HTTP transport options) + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `server.name` | `string` | `"ditto-mcp"` | Server name exposed to MCP clients | +| `server.http.port` | `number` | `3000` | HTTP server port | +| `server.http.host` | `string` | `"127.0.0.1"` | Bind address (loopback by default) | +| `server.http.enableDnsRebindingProtection` | `boolean` | `true` | Enable DNS rebinding protection (rejects requests with invalid Host/Origin headers) | +| `server.http.allowedHosts` | `string[]?` | `undefined` | Allowed Host header values (e.g., `["mcp.example.com:3000"]`). When undefined and protection is enabled, a loopback allowlist is derived: `["127.0.0.1:port", "localhost:port", "[::1]:port", "host:port"]` | +| `server.http.allowedOrigins` | `string[]?` | `undefined` | Allowed Origin header values (optional) | + +**Remote deployments:** When binding a non-loopback host (e.g., `0.0.0.0` or a public IP), you **MUST** set `allowedHosts` explicitly. The SDK matches the full `Host` header (e.g., `mcp.example.com:3000`), so include the exact `host:port` values your clients will send. + +Example: +```json +{ + "server": { + "http": { + "host": "0.0.0.0", + "port": 3000, + "allowedHosts": ["mcp.example.com:3000"] + } + } +} +``` + +### Knowledge Tools + +The server exposes `search` and `get_chunk` tools backed by a pluggable knowledge core. You can index: +- **Public Ditto docs** (fetched from `llms.txt` at startup, enabled by default) +- **Local markdown directory** (disabled by default) +- **Both** (merged corpus) + +Choose a retriever mode: +- `fts` (default) — fast keyword search, no model download, works offline +- `vector` — semantic vector search (downloads BGE embedding model ~80MB on first run) +- `hybrid` — RRF fusion of FTS + vector (best recall) + +The index can be stored in **SQLite** (file-based, default) or **Postgres** (pgvector + FTS). + +#### Knowledge Config + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `knowledge.enabled` | `boolean` | `true` | Enable knowledge tools | +| `knowledge.retriever` | `"fts" \| "vector" \| "hybrid"` | `"fts"` | Retriever mode | + +#### Sources + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `knowledge.publicSource.enabled` | `boolean` | `true` | Enable PublicSource (Ditto llms.txt) | +| `knowledge.publicSource.url` | `string` | `"https://eclipse.dev/ditto/llms.txt"` | llms.txt URL | +| `knowledge.publicSource.maxDocs` | `number?` | `undefined` | Optional doc limit | +| `knowledge.localDir.enabled` | `boolean` | `false` | Enable LocalDirSource (index a local markdown directory) | +| `knowledge.localDir.path` | `string?` | `undefined` | Path to local markdown directory | +| `knowledge.localDir.id` | `string` | `"local"` | Source ID for local chunks | + +#### Chunking + +Controls how source markdown is split into indexed chunks. Applies to all sources (public + local). Re-run `ditto-mcp-ingest` after changing these to rebuild the index. + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `knowledge.chunk.maxChars` | `number` | `1000` | Max characters per chunk. Larger = more contiguous context per hit, fewer split-across-boundary gaps; coarser ranking precision and more tokens returned per result. | +| `knowledge.chunk.overlap` | `number` | `150` | Characters carried between adjacent pieces when a paragraph is hard-split. Must be `< maxChars`. Raise to reduce boundary loss without growing chunks much. | + +**⚠️ Ceiling for `vector`/`hybrid`:** the embedding model bounds useful chunk size. `bge-small-en-v1.5` has a **512-token (~1500–2000 char) window** — text beyond it is silently truncated before embedding, so a chunk larger than the window loses semantic recall on its tail. Keep `maxChars` at/under the model's window for vector search, or switch to a longer-context embedding model. For `fts` (no embeddings) there is no such limit; larger chunks are safe. + +#### Search (query-time) + +Applies at search time — no re-ingest needed. The `search` tool accepts per-call `limit` and `context` args that override these defaults. + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `knowledge.search.limit` | `number` | `5` | Default number of **matches (anchors)** returned. Max 20. Neighbors from `context` do not count against it. | +| `knowledge.search.context` | `number` | `1` | **Neighbor expansion**: adjacent same-document chunks pulled in on each side of every match, for surrounding context. Max 5. `0` = matches only. | + +**Neighbor expansion** decouples *retrieval granularity* from *context delivery*. The retriever finds the relevant chunk (anchor); expansion then fetches its positional neighbors (same document, adjacent ordinal) **by id — never re-scored or re-embedded**. This is retriever-agnostic: an `fts`, `vector`, or `hybrid` match all get the same neighbors, since neighbors are defined by document layout, not by how the anchor was found. Results are emitted as contiguous spans in reading order; neighbors are tagged `context` (vs. `matched: …`) so their relevance isn't over-weighted. + +Why it matters for `vector`/`hybrid`: keep chunks **small** (sharp embeddings, under the 512-token ceiling) and recover surrounding context via `context` instead of fat chunks. Because neighbors are fetched by id, expansion never touches the embedding ceiling. + +**Interaction with `chunk.overlap`:** overlap and neighbor expansion both fight boundary loss, so they partly overlap in purpose. With `context ≥ 1`, adjacent chunks are already returned — so a positive `overlap` duplicates the seam text. When using expansion, set `chunk.overlap` low or `0`. + +Recommended for `hybrid`: small chunks + expansion, no overlap: + +```json +{ "knowledge": { + "retriever": "hybrid", + "chunk": { "maxChars": 1000, "overlap": 0 }, + "search": { "limit": 5, "context": 1 } +} } +``` + +#### Embedding (for vector/hybrid) + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `knowledge.embedding.model` | `string` | `"Xenova/bge-small-en-v1.5"` | Hugging Face model ID | +| `knowledge.embedding.dim` | `number` | `384` | Embedding dimension (must match the model) | +| `knowledge.embedding.modelPath` | `string?` | `undefined` | Local path to ONNX model (offline mode) | +| `knowledge.embedding.allowRemoteModels` | `boolean` | `true` | Allow model download from Hugging Face | +| `knowledge.embedding.cacheDir` | `string?` | `undefined` | Custom cache directory for downloaded models | +| `knowledge.embedding.batchSize` | `number` | `32` | Embedding batch size | + +**Offline vector search:** Set `allowRemoteModels: false` and provide `modelPath` pointing to a pre-downloaded ONNX model directory. + +#### Store (persistence) + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `knowledge.store.kind` | `"sqlite" \| "pgvector"` | `"sqlite"` | Store backend | +| `knowledge.store.sqlite.path` | `string?` | `undefined` | Path to SQLite file. If unset, the server builds an in-memory index at startup. | +| `knowledge.store.pgvector.connectionString` | `string?` | `undefined` | Postgres connection string (required when `kind: "pgvector"`) | +| `knowledge.store.pgvector.table` | `string` | `"ditto_kn"` | Table name prefix for Postgres tables | + +**SQLite (default):** File-based index. If `sqlite.path` is set and the file exists, the server loads it instantly (no fetch/embed). If missing/corrupt/mismatched, the server builds the index in memory (fallback mode). The server never writes the file automatically — use `ditto-mcp-ingest` to persist. + +**Postgres (pgvector):** Requires the `vector` extension. If the index exists and metadata matches, the server loads instantly. If missing/mismatched, the server **refuses to start** (no in-memory fallback). You **must** run `ditto-mcp-ingest` to populate Postgres before starting the server. + +**Postgres setup (AWS RDS example):** +1. Create a Postgres database. +2. Enable the `vector` extension: + - Create a custom parameter group with `rds.extensions = 'vector'` + - Apply it to your instance + - Connect and run `CREATE EXTENSION IF NOT EXISTS vector;` +3. Configure `knowledge.store.pgvector.connectionString` in your config. +4. Run `ditto-mcp-ingest` to build the index. + +**Testing Postgres:** Run `npm run test:pg` (requires Docker + testcontainers). These tests are excluded from the default `npm test` suite to keep the default test run hermetic. + +### Ditto Action Tools + +The server can expose action tools dynamically generated from a Ditto OpenAPI spec. Each tool makes HTTP calls to a Ditto instance. Credentials are passed through to Ditto (the MCP never decides authorization beyond policy gating). The default policy is **read-only** (`GET` only). + +#### Ditto Config + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `ditto.enabled` | `boolean` | `false` | Enable action tools | +| `ditto.baseUrl` | `string` | (required if enabled) | Base URL to Ditto instance (e.g., `http://localhost:8080`) | +| `ditto.openApi.path` | `string?` | `undefined` | Path to a local OpenAPI spec file. | +| `ditto.openApi.url` | `string?` | `undefined` | URL to fetch the OpenAPI spec from. | +| `ditto.openApi.version` | `string?` | `undefined` | Ditto git tag/ref (e.g. `3.6.0`) to fetch the matching spec for, via `versionUrlTemplate`. | +| `ditto.openApi.versionUrlTemplate` | `string` | eclipse-ditto raw URL | URL template with a `${version}` placeholder; override for forks/mirrors. | + +Spec resolution precedence (first match wins): `path` > `url` > `version` > the in-repo canonical spec (`documentation/src/main/resources/openapi/ditto-api-2.yml`), which matches the checked-out Ditto version and works offline. Set `version` to target a different Ditto release at runtime (requires network). + +#### Credentials + +Action tools support two credential modes: + +| Kind | Description | +|------|-------------| +| `basic` | Username + password (sent as `Authorization: Basic `) | +| `oidc` | OAuth2 client-credentials flow: requests a token from `tokenUrl` using `clientId` + `clientSecret`, sends it as `Authorization: Bearer `, auto-refreshes ~30s before expiry | + +**OIDC credential fields:** +- `tokenUrl` (required) — OAuth2 token endpoint +- `clientId` (required) — OAuth2 client identifier +- `clientSecret` (required) — OAuth2 client secret (never logged) +- `scope` (optional) — OAuth2 scopes (space-separated) + +Example: +```json +{ + "ditto": { + "enabled": true, + "baseUrl": "http://localhost:8080", + "credential": { + "kind": "oidc", + "tokenUrl": "https://auth.example.com/oauth/token", + "clientId": "my-client-id", + "clientSecret": "my-client-secret", + "scope": "ditto:read ditto:write" + } + } +} +``` + +**Standard vs devops credentials:** + +`ditto.credential` authenticates standard operations. `ditto.devopsCredential` (optional, same shape) authenticates **sudo** operations — `/devops/*`, `sudo*`, and the secret-bearing connectivity API (`/api/2/connections*`). + +- Sudo operations use `devopsCredential` **exclusively**. If `devopsCredential` is not set, every sudo tool is refused at the MCP layer (even if `credential` could reach it). +- `devopsCredential` may be `basic` (Ditto `DevOpsBasic`: a devops user's username/password) or `oidc` (Ditto `DevOpsBearer`: OAuth2). For a separate devops OIDC identity, give it its own `clientId`/`clientSecret`. +- A per-session `Authorization` header overrides whichever credential the operation selected (standard for normal ops, devops for sudo ops). + +Example (separate OIDC identities): + +```json +{ + "ditto": { + "enabled": true, + "baseUrl": "http://localhost:8080", + "credential": { "kind": "oidc", "tokenUrl": "https://idp/token", "clientId": "app", "clientSecret": "..." }, + "devopsCredential": { "kind": "oidc", "tokenUrl": "https://idp/token", "clientId": "devops", "clientSecret": "..." }, + "policy": { "sudoAllowlist": ["GET /api/2/connections"] } + } +} +``` + +**Migration from earlier configs:** the `devops` credential kind and the `devops: true` flag are removed. Move a devops credential into `ditto.devopsCredential` (use `kind: "basic"` for a devops username/password). + +**Authorization enforcement:** The MCP never decides authorization. It forwards the credential (or `Authorization` header) and lets Ditto enforce access control. Credentials are never logged. + +#### Policy + +By default, action tools only expose **read** (`GET`) operations. Write and privileged operations require explicit allowlisting: + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `ditto.policy.allowMethods` | `string[]` | `["GET"]` | Wholesale HTTP method allowlist (applies to all non-sudo operations) | +| `ditto.policy.writeAllowlist` | `string[]` | `[]` | Per-operation granular allowlist for enabling specific write operations (see key format below) | +| `ditto.policy.sudoAllowlist` | `string[]` | `[]` | Per-operation allowlist for sudo/devops-privileged operations (see key format below) | + +**Allowlist key format:** each entry is either the OpenAPI `operationId` (e.g. `putThing`) **or** a `METHOD path` key (e.g. `PUT /api/2/things/{thingId}`). Ditto's OpenAPI spec does not declare `operationId`s, so use the `METHOD path` form — the `path` is the raw spec path, keeping the `{param}` braces. `allowMethods` (wholesale, non-sudo) is unaffected. + +**Sudo operations & devops credential:** + +Ditto secures `/api/2/connections*` (secret-bearing) with `DevOpsBasic`/`DevOpsBearer` security, and `/devops/*` paths are devops-privileged. These operations are classified as "sudo" and: +- Must be explicitly listed in `sudoAllowlist` (by `METHOD path` key, or operationId if the spec has one) +- Require `ditto.devopsCredential` to be configured. +- Are NOT auto-allowed even if the method is `GET` and in `allowMethods`. + +Connectivity is always devops-gated (classified sudo regardless of the OpenAPI spec's declared security), but policy granularity is unchanged — `sudoAllowlist` is a per-operation opt-in (default `[]` = all sudo blocked). Allow connections while blocking direct-actor/devops commands by listing only the connection operations: + +- Connections read only: `"sudoAllowlist": ["GET /api/2/connections", "GET /api/2/connections/{connectionId}"]` +- Full connections CRUD, still blocking piggyback/devops: `"sudoAllowlist": ["GET /api/2/connections","GET /api/2/connections/{connectionId}","POST /api/2/connections","PUT /api/2/connections/{connectionId}","DELETE /api/2/connections/{connectionId}"]` + +All sudo operations still require `ditto.devopsCredential` to be set. + +**Examples:** + +- **Read-only (default):** `{ "allowMethods": ["GET"] }` — only non-sudo GET operations are allowed. +- **Enable specific writes:** `{ "allowMethods": ["GET"], "writeAllowlist": ["PUT /api/2/things/{thingId}", "PATCH /api/2/things/{thingId}"] }` — enables specific write operations. +- **Enable sudo:** `{ "allowMethods": ["GET"], "sudoAllowlist": ["GET /api/2/connections", "GET /devops/logging"] }` — enables specific devops-privileged operations (requires devops credential). + +### Tools Exposed + +When running, the server exposes: + +**Core tools:** +- `ping` — health check (enabled by default via `tools.ping`; independent of `knowledge`/`ditto`) + +**Knowledge tools (if `knowledge.enabled: true`):** +- `search` — semantic/keyword search over the knowledge corpus +- `get_chunk` — retrieve a specific chunk by ID + +**Action tools (if `ditto.enabled: true`):** + +Dynamically generated from the Ditto OpenAPI spec. Examples: +- `getThing`, `putThing`, `modifyThing`, `deleteThing` +- `getPolicy`, `putPolicy`, `modifyPolicy`, `deletePolicy` +- `getConnections`, `createConnection`, `modifyConnection`, `deleteConnection` (sudo, requires devops credential + `sudoAllowlist`) +- `sudoRetrieveThing`, `piggybackSend` (sudo) + +Write tools (POST, PATCH, PUT) expose their top-level request body fields in the tool schema, allowing clients to discover and validate the shape of the request. Nested objects are passed through as freeform JSON. + +## Security & OSS Notes + +**Commit the engine, NOT your corpus/built index/secrets:** + +- The built knowledge index (`.db` files) is gitignored. Commit the code, not the index. +- Local corpus directories (markdown files) may contain proprietary content — do NOT commit them to public repos unless intended. +- Credentials in `config.local.json` or other configs may contain secrets — do NOT commit them. Use environment variables or secret managers in production. +- Downloaded embedding models (`models/`, `.cache/`) are gitignored. + +**Credential passthrough:** + +The MCP forwards credentials to Ditto, which enforces authorization. The MCP never decides access beyond policy gating (allowMethods, writeAllowlist, sudoAllowlist). Credentials are never logged by the server. + +**Read-only default:** + +The default policy (`allowMethods: ["GET"]`) ensures action tools are read-only unless you explicitly enable writes. Sudo operations (connections, devops) require explicit `sudoAllowlist` + devops credential. + +**Sudo/devops/connections gating:** + +Connections and devops endpoints are gated at the MCP layer (sudo policy) to prevent accidental exposure of secret-bearing APIs. Ditto still enforces the real authorization. + +## Testing + +```bash +npm test # hermetic unit tests (no Docker, no network) +npm run test:pg # Postgres integration tests (requires Docker + testcontainers) +npm run typecheck # TypeScript type check +``` + +The examples are parse-tested in `src/config/examples.test.ts` to ensure they stay valid. + +## Examples + +See `examples/` for reference configs: + +- `public-fts.json` — simplest public quickstart (FTS, SQLite, llms.txt only) +- `hybrid-local.json` — hybrid retriever + local markdown directory + llms.txt +- `pgvector.json` — Postgres (pgvector) store for persistent index +- `ditto-readonly.json` — Ditto action tools with read-only policy (basic auth) +- `ditto-oidc-write.json` — Ditto action tools with OIDC client-credentials + write/sudo operations + +All examples use placeholders (e.g., `REPLACE_ME`, `/path/to/...`) for secrets and paths. Replace these with your own values before use. + +## Project Layout + +- `src/core/` — shared types (`ToolDef`, `RequestCtx`) +- `src/registry/` — `ToolRegistry` +- `src/config/` — zod schema + loader +- `src/tools/` — tool implementations (`ping`, `search`, `get_chunk`) + wiring +- `src/knowledge/` — corpus/retrieval core (`KnowledgeSource`, `Retriever`, `PublicSource`, `LocalDirSource`, `FtsRetriever`, `VectorRetriever`, `HybridRetriever`) +- `src/ditto/` — action tools (OpenAPI → MCP tool schema, credential handling, policy enforcement) +- `src/server/` — `buildServer`, `createHttpApp` +- `src/bin/` — `stdio`, `http`, `ingest` entrypoints +- `examples/` — reference configs (parse-tested) + +## Dependencies + +- `@modelcontextprotocol/sdk` — MCP protocol +- `express` — HTTP server +- `zod` — config validation +- `better-sqlite3` — SQLite store +- `pg` — Postgres client (pgvector store) +- `sqlite-vec` — SQLite vector extension +- `@huggingface/transformers` — ONNX embedding models +- `yaml` — OpenAPI spec parsing diff --git a/mcp/docker/config.docker.json b/mcp/docker/config.docker.json new file mode 100644 index 0000000000..62c740a776 --- /dev/null +++ b/mcp/docker/config.docker.json @@ -0,0 +1,16 @@ +{ + "server": { + "name": "ditto-mcp", + "http": { + "host": "0.0.0.0", + "port": 3000, + "enableDnsRebindingProtection": true, + "allowedHosts": ["localhost:3000", "127.0.0.1:3000"] + } + }, + "knowledge": { + "enabled": true, + "retriever": "fts", + "store": { "kind": "sqlite", "sqlite": { "path": "/app/data/ditto_kn.db" } } + } +} diff --git a/mcp/examples/ditto-oidc-write.json b/mcp/examples/ditto-oidc-write.json new file mode 100644 index 0000000000..30938f16e1 --- /dev/null +++ b/mcp/examples/ditto-oidc-write.json @@ -0,0 +1,23 @@ +{ + "ditto": { + "enabled": true, + "baseUrl": "http://localhost:8080", + "credential": { + "kind": "oidc", + "tokenUrl": "https://idp.example/token", + "clientId": "REPLACE_ME_APP_CLIENT", + "clientSecret": "REPLACE_ME" + }, + "devopsCredential": { + "kind": "oidc", + "tokenUrl": "https://idp.example/token", + "clientId": "REPLACE_ME_DEVOPS_CLIENT", + "clientSecret": "REPLACE_ME" + }, + "policy": { + "allowMethods": ["GET"], + "writeAllowlist": ["PUT /api/2/things/{thingId}"], + "sudoAllowlist": ["GET /api/2/connections"] + } + } +} diff --git a/mcp/examples/ditto-readonly.json b/mcp/examples/ditto-readonly.json new file mode 100644 index 0000000000..4070b1f681 --- /dev/null +++ b/mcp/examples/ditto-readonly.json @@ -0,0 +1,18 @@ +{ + "knowledge": { + "enabled": true, + "retriever": "fts" + }, + "ditto": { + "enabled": true, + "baseUrl": "http://localhost:8080", + "credential": { + "kind": "basic", + "username": "ditto", + "password": "REPLACE_ME" + }, + "policy": { + "allowMethods": ["GET"] + } + } +} diff --git a/mcp/examples/hybrid-local.json b/mcp/examples/hybrid-local.json new file mode 100644 index 0000000000..edf2b83307 --- /dev/null +++ b/mcp/examples/hybrid-local.json @@ -0,0 +1,24 @@ +{ + "knowledge": { + "retriever": "hybrid", + "publicSource": { + "enabled": true + }, + "localDir": { + "enabled": true, + "path": "/path/to/your/markdown" + }, + "embedding": { + "model": "Xenova/bge-small-en-v1.5", + "dim": 384, + "allowRemoteModels": true, + "batchSize": 32 + }, + "store": { + "kind": "sqlite", + "sqlite": { + "path": "./ditto-index.db" + } + } + } +} diff --git a/mcp/examples/pgvector.json b/mcp/examples/pgvector.json new file mode 100644 index 0000000000..0b2d6847f7 --- /dev/null +++ b/mcp/examples/pgvector.json @@ -0,0 +1,16 @@ +{ + "knowledge": { + "retriever": "hybrid", + "embedding": { + "model": "Xenova/bge-small-en-v1.5", + "dim": 384 + }, + "store": { + "kind": "pgvector", + "pgvector": { + "connectionString": "postgres://USER:PASSWORD@HOST:5432/ditto", + "table": "ditto_kn" + } + } + } +} diff --git a/mcp/examples/public-fts.json b/mcp/examples/public-fts.json new file mode 100644 index 0000000000..336f1ed2e4 --- /dev/null +++ b/mcp/examples/public-fts.json @@ -0,0 +1,14 @@ +{ + "knowledge": { + "retriever": "fts", + "publicSource": { + "enabled": true + }, + "store": { + "kind": "sqlite", + "sqlite": { + "path": "./ditto-index.db" + } + } + } +} diff --git a/mcp/package-lock.json b/mcp/package-lock.json new file mode 100644 index 0000000000..f497e383f4 --- /dev/null +++ b/mcp/package-lock.json @@ -0,0 +1,7155 @@ +{ + "name": "ditto-mcp-server", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ditto-mcp-server", + "version": "0.1.0", + "dependencies": { + "@huggingface/transformers": "^3.8.1", + "@modelcontextprotocol/sdk": "^1", + "better-sqlite3": "^11.10.0", + "express": "^4.21.2", + "pg": "^8.22.0", + "sqlite-vec": "^0.1.9", + "yaml": "^2.9.0", + "zod": "^3.23.8" + }, + "bin": { + "ditto-mcp-http": "dist/bin/http.js", + "ditto-mcp-ingest": "dist/bin/ingest.js", + "ditto-mcp-stdio": "dist/bin/stdio.js" + }, + "devDependencies": { + "@testcontainers/postgresql": "^12.1.0", + "@types/better-sqlite3": "^7.6.13", + "@types/express": "^4.17.21", + "@types/node": "^22.10.0", + "@types/pg": "^8.20.4", + "tsx": "^4.19.2", + "typescript": "^5.7.0", + "vitest": "^2.1.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@balena/dockerignore": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz", + "integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.4.tgz", + "integrity": "sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.1.tgz", + "integrity": "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@huggingface/jinja": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.5.9.tgz", + "integrity": "sha512-uWTG+l3VJRsl7EXxYizuL3P+cCPoc3cRqbWWRcQN0FhejRfbdq0RNhCmbY/YDtnTcz9icdLYuLDjsnz4d8JMuw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@huggingface/transformers": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@huggingface/transformers/-/transformers-3.8.1.tgz", + "integrity": "sha512-tsTk4zVjImqdqjS8/AOZg2yNLd1z9S5v+7oUPpXaasDRwEDhB+xnglK1k5cad26lL5/ZIaeREgWWy0bs9y9pPA==", + "license": "Apache-2.0", + "dependencies": { + "@huggingface/jinja": "^0.5.3", + "onnxruntime-node": "1.21.0", + "onnxruntime-web": "1.22.0-dev.20250409-89f8206ba4", + "sharp": "^0.34.1" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@kwsites/file-exists": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", + "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1" + } + }, + "node_modules/@kwsites/file-exists/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@kwsites/file-exists/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/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/@modelcontextprotocol/sdk/node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/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/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", + "license": "BSD-3-Clause" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@testcontainers/postgresql": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-12.1.0.tgz", + "integrity": "sha512-Pjf2VSVNirEPfz36nidyrVAnZvc2YhajOznY4VgyEsvfTd5qiMNOuPq96drREvxAUtXl5SFLX7vXj7sSq4aTcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "testcontainers": "^12.1.0" + } + }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/docker-modem": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz", + "integrity": "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/ssh2": "*" + } + }, + "node_modules/@types/dockerode": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-4.0.1.tgz", + "integrity": "sha512-cmUpB+dPN955PxBEuXE3f6lKO1hHiIGYJA46IVF3BJpNsZGvtBDcRnlrHYHtOH/B6vtDOyl2kZ2ShAu3mgc27Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/docker-modem": "*", + "@types/node": "*", + "@types/ssh2": "*" + } + }, + "node_modules/@types/estree": { + "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/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.9", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.9.tgz", + "integrity": "sha512-QP2ESEe/ImWY0HDwNAnK9PvEffUyhLTnWkk7KXzHfyeWAnlrDe1fN77bXl6ia8KT3wPlmA7t9/VPRpnf4Ex9sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/pg": { + "version": "8.20.4", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.4.tgz", + "integrity": "sha512-Jz7UDOlIiFJuacC0TlBoLyNtmwlA/wpIyPDd3tvUqlRM+HzkWy2xUgpFpaXtbfTAFF6sIGq5lsCDBdJnhky1Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/ssh2": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.5.tgz", + "integrity": "sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^18.11.18" + } + }, + "node_modules/@types/ssh2-streams": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/@types/ssh2-streams/-/ssh2-streams-0.1.13.tgz", + "integrity": "sha512-faHyY3brO9oLEA0QlcO8N2wT7R0+1sHWZvQ+y3rMLwdY1ZyS1z0W3t65j9PqT4HmQ6ALzNe7RZlNuCNE0wBSWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ssh2/node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/ssh2/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/archiver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.2", + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^10.0.0", + "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", + "lazystream": "^1.0.0", + "lodash": "^4.17.15", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/archiver/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/archiver/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/archiver/node_modules/tar-stream": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz", + "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-lock": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz", + "integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/b4a": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", + "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "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/bare-events": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.1.tgz", + "integrity": "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.8.0.tgz", + "integrity": "sha512-fM+MhCvdQhZ7NV6S95a07gPSqjIYKn6mFaXfx266wN3ajZGl/+1AzH+ubkXQ0fFZvOe2nk9VHkzdYkQE5zMV3Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.28.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.1.tgz", + "integrity": "sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/bare-stream": { + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.3.tgz", + "integrity": "sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.8.1", + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.4.7", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.7.tgz", + "integrity": "sha512-o8CRCiJtib+ycO3mE4A5UChtGX4dDP2XxsWVu9P+Zc3H8tcmKwNVEDoDTXmwN+uuMhfKeT7/i7Y26xS8W7ohoA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/better-sqlite3": { + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", + "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", + "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/buildcheck": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.7.tgz", + "integrity": "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==", + "dev": true, + "optional": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/byline": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", + "integrity": "sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/compress-commons": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/compress-commons/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/compress-commons/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cpu-features": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", + "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "dependencies": { + "buildcheck": "~0.0.6", + "nan": "^2.19.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", + "dev": true, + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/crc32-stream/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/crc32-stream/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "license": "MIT" + }, + "node_modules/docker-compose": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/docker-compose/-/docker-compose-1.4.2.tgz", + "integrity": "sha512-rPHigTKGaEHpkUmfd69QgaOp+Os5vGJwG/Ry8lcr8W/382AmI+z/D7qoa9BybKIkqNppaIbs8RYeHSevdQjWww==", + "dev": true, + "license": "MIT", + "dependencies": { + "yaml": "^2.2.2" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/docker-modem": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.7.tgz", + "integrity": "sha512-XJgGhoR/CLpqshm4d3L7rzH6t8NgDFUIIpztYlLHIApeJjMZKYJMz2zxPsYxnejq5h3ELYSw/RBsi3t5h7gNTA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.1", + "readable-stream": "^3.5.0", + "split-ca": "^1.0.1", + "ssh2": "^1.15.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/docker-modem/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/docker-modem/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dockerode": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-5.0.1.tgz", + "integrity": "sha512-avsq/xk4YPIrn0CgleX5bjT9Y8IT1p9PxrNQ++RBQ2WEyFfHCTDsT9kmyxz+H/axnjAwg8wJWEIuPGOUuNupiA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@balena/dockerignore": "^1.0.2", + "@grpc/grpc-js": "^1.11.1", + "@grpc/proto-loader": "^0.7.13", + "docker-modem": "^5.0.7", + "protobufjs": "^7.3.2", + "tar-fs": "^2.1.4" + }, + "engines": { + "node": ">= 14.17" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "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" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", + "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.0.tgz", + "integrity": "sha512-XKJXDsASUOo0LLtFwW5hCcQGH0N4WQc/Rn8/Pvoia+TJFOkkFPvrtW9lZOeeNcxQJspvOIERMwiRLsVFlhHEkA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/express-rate-limit/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/express-rate-limit/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", + "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/flatbuffers": { + "version": "25.9.23", + "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-25.9.23.tgz", + "integrity": "sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==", + "license": "Apache-2.0" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-port": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", + "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "license": "BSD-3-Clause", + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/guid-typescript": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz", + "integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==", + "license": "ISC" + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.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" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.31", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.31.tgz", + "integrity": "sha512-zJIHFrl6bq3RDd2YusFNCDlM8qUprxKswyi/OPzPyzKDdyBXDqWx8bZlZ7R+saTdSTatUmb3O7K4SspGPaEOQg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "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", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jose": { + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.4.tgz", + "integrity": "sha512-N8acGzVsQy6M/fjFcxtysNc4Q379TcM5dM/qKkNtsHFji88yANnXTr7BLeP75iPnFwBfQzM/jg2BZ9+HZrHCZA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "license": "ISC" + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/nan": { + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.28.0.tgz", + "integrity": "sha512-fTsDz99OTq2sVePhGdp4qQhggZFtKr64ZNVyVajRKtMOkJxYekplBh577PiJB12v/D3s2E5cGtOI45LWp6rnLQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.94.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.94.0.tgz", + "integrity": "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onnxruntime-common": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.21.0.tgz", + "integrity": "sha512-Q632iLLrtCAVOTO65dh2+mNbQir/QNTVBG3h/QdZBpns7mZ0RYbLRBgGABPbpU9351AgYy7SJf1WaeVwMrBFPQ==", + "license": "MIT" + }, + "node_modules/onnxruntime-node": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.21.0.tgz", + "integrity": "sha512-NeaCX6WW2L8cRCSqy3bInlo5ojjQqu2fD3D+9W5qb5irwxhEyWKXeH2vZ8W9r6VxaMPUan+4/7NDwZMtouZxEw==", + "hasInstallScript": true, + "license": "MIT", + "os": [ + "win32", + "darwin", + "linux" + ], + "dependencies": { + "global-agent": "^3.0.0", + "onnxruntime-common": "1.21.0", + "tar": "^7.0.1" + } + }, + "node_modules/onnxruntime-web": { + "version": "1.22.0-dev.20250409-89f8206ba4", + "resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.22.0-dev.20250409-89f8206ba4.tgz", + "integrity": "sha512-0uS76OPgH0hWCPrFKlL8kYVV7ckM7t/36HfbgoFw6Nd0CZVVbQC4PkrR8mBX8LtNUFZO25IQBqV2Hx2ho3FlbQ==", + "license": "MIT", + "dependencies": { + "flatbuffers": "^25.1.24", + "guid-typescript": "^1.0.9", + "long": "^5.2.3", + "onnxruntime-common": "1.22.0-dev.20250409-89f8206ba4", + "platform": "^1.3.6", + "protobufjs": "^7.2.4" + } + }, + "node_modules/onnxruntime-web/node_modules/onnxruntime-common": { + "version": "1.22.0-dev.20250409-89f8206ba4", + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.22.0-dev.20250409-89f8206ba4.tgz", + "integrity": "sha512-vDJMkfCfb0b1A836rgHj+ORuZf4B4+cc2bASQtpeoJLueuFc5DuYwjIZUBrSvx/fO5IrLjLz+oTrB3pcGlhovQ==", + "license": "MIT" + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/pg": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.22.0.tgz", + "integrity": "sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA==", + "license": "MIT", + "peer": true, + "dependencies": { + "pg-connection-string": "^2.14.0", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.15.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.4.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz", + "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.0.tgz", + "integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", + "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.15.0.tgz", + "integrity": "sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/platform": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", + "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==", + "license": "MIT" + }, + "node_modules/postcss": { + "version": "8.5.22", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.22.tgz", + "integrity": "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/proper-lockfile/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/properties-reader": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/properties-reader/-/properties-reader-3.0.1.tgz", + "integrity": "sha512-WPn+h9RGEExOKdu4bsF4HksG/uzd3cFq3MFtq8PsFeExPse5Ha/VOjQNyHhjboBFwGXGev6muJYTSPAOkROq2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@kwsites/file-exists": "^1.1.1", + "mkdirp": "^3.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/steveukx/properties?sponsor=1" + } + }, + "node_modules/protobufjs": { + "version": "7.6.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz", + "integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "license": "BSD-3-Clause", + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/router/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/router/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/router/node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.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.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.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.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split-ca": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz", + "integrity": "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause" + }, + "node_modules/sqlite-vec": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/sqlite-vec/-/sqlite-vec-0.1.9.tgz", + "integrity": "sha512-L7XJWRIBNvR9O5+vh1FQ+IGkh/3D2AzVksW5gdtk28m78Hy8skFD0pqReKH1Yp0/BUKRGcffgKvyO/EON5JXpA==", + "license": "MIT OR Apache", + "optionalDependencies": { + "sqlite-vec-darwin-arm64": "0.1.9", + "sqlite-vec-darwin-x64": "0.1.9", + "sqlite-vec-linux-arm64": "0.1.9", + "sqlite-vec-linux-x64": "0.1.9", + "sqlite-vec-windows-x64": "0.1.9" + } + }, + "node_modules/sqlite-vec-darwin-arm64": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/sqlite-vec-darwin-arm64/-/sqlite-vec-darwin-arm64-0.1.9.tgz", + "integrity": "sha512-jSsZpE42OfBkGL/ItyJTVCUwl6o6Ka3U5rc4j+UBDIQzC1ulSSKMEhQLthsOnF/MdAf1MuAkYhkdKmmcjaIZQg==", + "cpu": [ + "arm64" + ], + "license": "MIT OR Apache", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/sqlite-vec-darwin-x64": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/sqlite-vec-darwin-x64/-/sqlite-vec-darwin-x64-0.1.9.tgz", + "integrity": "sha512-KDlVyqQT7pnOhU1ymB9gs7dMbSoVmKHitT+k1/xkjarcX8bBqPxWrGlK/R+C5WmWkfvWwyq5FfXfiBYCBs6PlA==", + "cpu": [ + "x64" + ], + "license": "MIT OR Apache", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/sqlite-vec-linux-arm64": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/sqlite-vec-linux-arm64/-/sqlite-vec-linux-arm64-0.1.9.tgz", + "integrity": "sha512-5wXVJ9c9kR4CHm/wVqXb/R+XUHTdpZ4nWbPHlS+gc9qQFVHs92Km4bPnCKX4rtcPMzvNis+SIzMJR1SCEwpuUw==", + "cpu": [ + "arm64" + ], + "license": "MIT OR Apache", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/sqlite-vec-linux-x64": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/sqlite-vec-linux-x64/-/sqlite-vec-linux-x64-0.1.9.tgz", + "integrity": "sha512-w3tCH8xK2finW8fQJ/m8uqKodXUZ9KAuAar2UIhz4BHILfpE0WM/MTGCRfa7RjYbrYim5Luk3guvMOGI7T7JQA==", + "cpu": [ + "x64" + ], + "license": "MIT OR Apache", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/sqlite-vec-windows-x64": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/sqlite-vec-windows-x64/-/sqlite-vec-windows-x64-0.1.9.tgz", + "integrity": "sha512-y3gEIyy/17bq2QFPQOWLE68TYWcRZkBQVA2XLrTPHNTOp55xJi/BBBmOm40tVMDMjtP+Elpk6UBUXdaq+46b0Q==", + "cpu": [ + "x64" + ], + "license": "MIT OR Apache", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/ssh-remote-port-forward": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/ssh-remote-port-forward/-/ssh-remote-port-forward-1.0.4.tgz", + "integrity": "sha512-x0LV1eVDwjf1gmG7TTnfqIzf+3VPRz7vrNIjX6oYLbeCrf/PeVY6hkT68Mg+q02qXxQhrLjB0jfgvhevoCRmLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ssh2": "^0.5.48", + "ssh2": "^1.4.0" + } + }, + "node_modules/ssh-remote-port-forward/node_modules/@types/ssh2": { + "version": "0.5.52", + "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-0.5.52.tgz", + "integrity": "sha512-lbLLlXxdCZOSJMCInKH2+9V/77ET2J6NPQHpFI0kda61Dd1KglJs+fPQBchizmzYSOJBgdTajhPqBO1xxLywvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/ssh2-streams": "*" + } + }, + "node_modules/ssh2": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.17.0.tgz", + "integrity": "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "asn1": "^0.2.6", + "bcrypt-pbkdf": "^1.0.2" + }, + "engines": { + "node": ">=10.16.0" + }, + "optionalDependencies": { + "cpu-features": "~0.0.10", + "nan": "^2.23.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/streamx": { + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.0.tgz", + "integrity": "sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==", + "dev": true, + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "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.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar": { + "version": "7.5.22", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", + "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar-fs": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/testcontainers": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-12.1.0.tgz", + "integrity": "sha512-YjDLqIITuhGLMnM10yhg3oV6lIG5IMpz1R1DPBZoOOks83q7i7IVpeSWRTiyl7roozjiyLmwIoLK/KY8OnZmIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@balena/dockerignore": "^1.0.2", + "@types/dockerode": "^4.0.1", + "archiver": "^7.0.1", + "async-lock": "^1.4.1", + "byline": "^5.0.0", + "debug": "^4.4.3", + "docker-compose": "^1.4.2", + "dockerode": "^5.0.1", + "get-port": "^5.1.1", + "proper-lockfile": "^4.1.2", + "properties-reader": "^3.0.1", + "ssh-remote-port-forward": "^1.0.4", + "tar-fs": "^3.1.3", + "tmp": "^0.2.7", + "undici": "^8.9.0" + }, + "engines": { + "node": ">= 22.22" + } + }, + "node_modules/testcontainers/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/testcontainers/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/testcontainers/node_modules/tar-fs": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.3.tgz", + "integrity": "sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/testcontainers/node_modules/tar-stream": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz", + "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/tsx": { + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.1.tgz", + "integrity": "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.10.0.tgz", + "integrity": "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "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==", + "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" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-node/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/vite-node/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "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", + "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", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.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", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/zip-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/zip-stream/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/zip-stream/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + } +} diff --git a/mcp/package.json b/mcp/package.json new file mode 100644 index 0000000000..25475ca240 --- /dev/null +++ b/mcp/package.json @@ -0,0 +1,44 @@ +{ + "name": "ditto-mcp-server", + "version": "0.1.0", + "private": true, + "type": "module", + "engines": { + "node": ">=22" + }, + "bin": { + "ditto-mcp-stdio": "dist/bin/stdio.js", + "ditto-mcp-http": "dist/bin/http.js", + "ditto-mcp-ingest": "dist/bin/ingest.js" + }, + "scripts": { + "build": "tsc -p tsconfig.json", + "typecheck": "tsc -p tsconfig.json --noEmit", + "test": "vitest run", + "test:watch": "vitest", + "test:pg": "vitest run --config vitest.pg.config.ts", + "dev:stdio": "tsx src/bin/stdio.ts", + "dev:http": "tsx src/bin/http.ts", + "dev:ingest": "tsx src/bin/ingest.ts" + }, + "dependencies": { + "@huggingface/transformers": "^3.8.1", + "@modelcontextprotocol/sdk": "^1", + "better-sqlite3": "^11.10.0", + "express": "^4.21.2", + "pg": "^8.22.0", + "sqlite-vec": "^0.1.9", + "yaml": "^2.9.0", + "zod": "^3.23.8" + }, + "devDependencies": { + "@testcontainers/postgresql": "^12.1.0", + "@types/better-sqlite3": "^7.6.13", + "@types/express": "^4.17.21", + "@types/node": "^22.10.0", + "@types/pg": "^8.20.4", + "tsx": "^4.19.2", + "typescript": "^5.7.0", + "vitest": "^2.1.0" + } +} diff --git a/mcp/src/bin/http.ts b/mcp/src/bin/http.ts new file mode 100644 index 0000000000..1548ead437 --- /dev/null +++ b/mcp/src/bin/http.ts @@ -0,0 +1,26 @@ +#!/usr/bin/env node +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { loadConfig } from "../config/load.js"; +import { createHttpApp } from "../server/http-app.js"; +import { buildKnowledgeService } from "../knowledge/build.js"; + +const config = loadConfig(process.env.DITTO_MCP_CONFIG); +const knowledge = await buildKnowledgeService(config); +const app = createHttpApp(config, knowledge); +app.listen(config.server.http.port, config.server.http.host, () => { + process.stderr.write( + `[ditto-mcp] http server listening on ${config.server.http.host}:${config.server.http.port}/mcp\n`, + ); +}); diff --git a/mcp/src/bin/ingest.test.ts b/mcp/src/bin/ingest.test.ts new file mode 100644 index 0000000000..cba525a992 --- /dev/null +++ b/mcp/src/bin/ingest.test.ts @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { describe, it, expect } from "vitest"; +import { mkdtempSync, writeFileSync, existsSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { execFileSync } from "node:child_process"; +import { SqliteKnowledgeStore } from "../knowledge/sqlite-knowledge-store.js"; + +const here = dirname(fileURLToPath(import.meta.url)); +const entry = resolve(here, "ingest.ts"); + +describe("ingest CLI (spawn e2e)", () => { + it("builds a persisted fts index from a local dir corpus", async () => { + const corpus = mkdtempSync(join(tmpdir(), "ditto-corpus-")); + writeFileSync(join(corpus, "a.md"), "# Reconnect\n\nNetty leak out of memory crash."); + const idxDir = mkdtempSync(join(tmpdir(), "ditto-idx-")); + const path = join(idxDir, "index.db"); + const cfgPath = join(idxDir, "config.json"); + writeFileSync(cfgPath, JSON.stringify({ + knowledge: { + retriever: "fts", + publicSource: { enabled: false }, + localDir: { enabled: true, path: corpus }, + store: { kind: "sqlite", sqlite: { path } }, + }, + })); + + execFileSync(process.execPath, ["--import", "tsx", entry], { + env: { ...process.env, DITTO_MCP_CONFIG: cfgPath }, + stdio: "pipe", + }); + + expect(existsSync(path)).toBe(true); + const store = new SqliteKnowledgeStore(path); + expect(await store.isPopulated()).toBe(true); + expect((await store.ftsSearch("memory", 5)).length).toBeGreaterThan(0); + await store.close(); + }, 30000); + + it("re-ingest is a clean rebuild (no orphan chunks from a larger prior run)", async () => { + const corpus = mkdtempSync(join(tmpdir(), "ditto-corpus-")); + const idxDir = mkdtempSync(join(tmpdir(), "ditto-idx-")); + const path = join(idxDir, "index.db"); + const cfgPath = join(idxDir, "config.json"); + const cfg = (files: number) => { + // (re)write corpus with `files` docs + for (let i = 0; i < files; i++) writeFileSync(join(corpus, `d${i}.md`), `# D${i}\n\ndoc ${i} netty`); + writeFileSync(cfgPath, JSON.stringify({ + knowledge: { retriever: "fts", publicSource: { enabled: false }, + localDir: { enabled: true, path: corpus }, store: { kind: "sqlite", sqlite: { path } } }, + })); + }; + const run = () => execFileSync(process.execPath, ["--import", "tsx", entry], + { env: { ...process.env, DITTO_MCP_CONFIG: cfgPath }, stdio: "pipe" }); + + cfg(5); run(); + // shrink corpus: remove all, write 1 doc + for (let i = 0; i < 5; i++) rmSync(join(corpus, `d${i}.md`), { force: true }); + cfg(1); run(); + + const store = new SqliteKnowledgeStore(path); + // Only the single remaining doc's chunk id ("local#0") should exist. + expect(await store.getChunk("local#0")).toBeDefined(); + expect(await store.getChunk("local#4")).toBeUndefined(); // orphan from the 5-doc run is gone + await store.close(); + }, 30000); +}); diff --git a/mcp/src/bin/ingest.ts b/mcp/src/bin/ingest.ts new file mode 100644 index 0000000000..364314bf86 --- /dev/null +++ b/mcp/src/bin/ingest.ts @@ -0,0 +1,43 @@ +#!/usr/bin/env node +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { loadConfig } from "../config/load.js"; +import { buildIndex, metaFor } from "../knowledge/build-index.js"; +import { makeSources, makeEmbedder } from "../knowledge/factories.js"; +import { withIngestStore } from "../knowledge/ingest-store.js"; +import type { EmbeddingProvider } from "../knowledge/embedding.js"; + +async function main(): Promise { + const config = loadConfig(process.env.DITTO_MCP_CONFIG); + if (!config.knowledge.enabled) throw new Error("knowledge is disabled in config"); + + const sources = makeSources(config); + if (sources.length === 0) throw new Error("no knowledge sources enabled"); + + let embedder: EmbeddingProvider | undefined; + if (config.knowledge.retriever !== "fts") { + embedder = await makeEmbedder(config); + } + + await withIngestStore(config, (store) => + buildIndex(sources, store, embedder, metaFor(config, embedder)), + ); + const dest = config.knowledge.store.kind === "sqlite" ? config.knowledge.store.sqlite.path : config.knowledge.store.kind; + process.stderr.write(`[ditto-mcp ingest] done: ${dest}\n`); +} + +main().catch((err) => { + process.stderr.write(`[ditto-mcp ingest] fatal: ${err instanceof Error ? err.stack : String(err)}\n`); + process.exit(1); +}); diff --git a/mcp/src/bin/stdio.test.ts b/mcp/src/bin/stdio.test.ts new file mode 100644 index 0000000000..de149e7561 --- /dev/null +++ b/mcp/src/bin/stdio.test.ts @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { describe, it, expect } from "vitest"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; +import { fileURLToPath } from "node:url"; +import { dirname, resolve } from "node:path"; +import { writeFileSync, mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const here = dirname(fileURLToPath(import.meta.url)); +const entry = resolve(here, "stdio.ts"); + +describe("stdio entrypoint (spawn e2e)", () => { + it("serves ping over stdio", async () => { + // knowledge disabled to keep this transport test hermetic; knowledge covered in knowledge tests. + const dir = mkdtempSync(join(tmpdir(), "ditto-mcp-")); + const cfgPath = join(dir, "config.json"); + writeFileSync(cfgPath, JSON.stringify({ knowledge: { enabled: false } })); + + const transport = new StdioClientTransport({ + command: process.execPath, + args: ["--import", "tsx", entry], + env: { + ...process.env, + DITTO_MCP_CONFIG: cfgPath, + }, + }); + const client = new Client({ name: "stdio-test", version: "0.0.0" }); + await client.connect(transport); + const res = await client.callTool({ name: "ping", arguments: {} }); + const content = res.content as Array<{ type: string; text: string }>; + expect(content[0].text).toBe("pong"); + await client.close(); + }); +}); diff --git a/mcp/src/bin/stdio.ts b/mcp/src/bin/stdio.ts new file mode 100644 index 0000000000..5349c9608e --- /dev/null +++ b/mcp/src/bin/stdio.ts @@ -0,0 +1,35 @@ +#!/usr/bin/env node +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { loadConfig } from "../config/load.js"; +import { registerTools } from "../tools/index.js"; +import { buildServer } from "../server/build-server.js"; +import { buildKnowledgeService } from "../knowledge/build.js"; + +async function main(): Promise { + const config = loadConfig(process.env.DITTO_MCP_CONFIG); + const knowledge = await buildKnowledgeService(config); + const registry = await registerTools(config, knowledge); + const server = buildServer(registry, config); + const transport = new StdioServerTransport(); + await server.connect(transport); + // Never write to stdout except MCP protocol frames; logs go to stderr. + process.stderr.write(`[ditto-mcp] stdio server ready: ${config.server.name}\n`); +} + +main().catch((err) => { + process.stderr.write(`[ditto-mcp] fatal: ${String(err)}\n`); + process.exit(1); +}); diff --git a/mcp/src/config/examples.test.ts b/mcp/src/config/examples.test.ts new file mode 100644 index 0000000000..cdb8a178b9 --- /dev/null +++ b/mcp/src/config/examples.test.ts @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { describe, it, expect } from "vitest"; +import { readdirSync } from "node:fs"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { loadConfig } from "./load.js"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const examplesDir = join(__dirname, "../../examples"); + +describe("example configs", () => { + const exampleFiles = readdirSync(examplesDir).filter((f) => f.endsWith(".json")); + + it("should have at least one example", () => { + expect(exampleFiles.length).toBeGreaterThan(0); + }); + + exampleFiles.forEach((file) => { + it(`should parse ${file} without error`, () => { + const path = join(examplesDir, file); + expect(() => loadConfig(path)).not.toThrow(); + }); + }); +}); diff --git a/mcp/src/config/load.test.ts b/mcp/src/config/load.test.ts new file mode 100644 index 0000000000..ba120316ab --- /dev/null +++ b/mcp/src/config/load.test.ts @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { describe, it, expect } from "vitest"; +import { writeFileSync, mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { loadConfig } from "./load.js"; + +describe("loadConfig", () => { + it("returns defaults when no path is given", () => { + const cfg = loadConfig(); + expect(cfg.server.name).toBe("ditto-mcp"); + expect(cfg.server.http.port).toBe(3000); + expect(cfg.server.http.host).toBe("127.0.0.1"); + expect(cfg.server.http.enableDnsRebindingProtection).toBe(true); + expect(cfg.tools.ping).toBe(true); + expect(cfg.knowledge.enabled).toBe(true); + expect(cfg.knowledge.publicSource.url).toBe( + "https://eclipse.dev/ditto/llms.txt", + ); + expect(cfg.knowledge.retriever).toBe("fts"); + expect(cfg.knowledge.embedding.model).toBe("Xenova/bge-small-en-v1.5"); + expect(cfg.knowledge.embedding.dim).toBe(384); + expect(cfg.knowledge.embedding.batchSize).toBe(32); + expect(cfg.knowledge.localDir.enabled).toBe(false); + expect(cfg.knowledge.store.kind).toBe("sqlite"); + expect(cfg.ditto.enabled).toBe(false); + expect(cfg.ditto.credential.kind).toBe("basic"); + expect(cfg.ditto.policy.allowMethods).toEqual(["GET"]); + }); + + it("accepts a pgvector store config", () => { + const dir = mkdtempSync(join(tmpdir(), "ditto-mcp-")); + const file = join(dir, "c.json"); + writeFileSync(file, JSON.stringify({ + knowledge: { store: { kind: "pgvector", pgvector: { connectionString: "postgres://x" } } }, + })); + const cfg = loadConfig(file); + expect(cfg.knowledge.store.kind).toBe("pgvector"); + expect(cfg.knowledge.store.pgvector.connectionString).toBe("postgres://x"); + }); + + it("merges values from a JSON file over defaults", () => { + const dir = mkdtempSync(join(tmpdir(), "ditto-mcp-")); + const file = join(dir, "config.json"); + writeFileSync(file, JSON.stringify({ server: { http: { port: 8080 } } })); + const cfg = loadConfig(file); + expect(cfg.server.http.port).toBe(8080); + expect(cfg.server.http.host).toBe("127.0.0.1"); + expect(cfg.server.http.enableDnsRebindingProtection).toBe(true); + expect(cfg.tools.ping).toBe(true); + }); + + it("throws on an invalid value", () => { + const dir = mkdtempSync(join(tmpdir(), "ditto-mcp-")); + const file = join(dir, "config.json"); + writeFileSync(file, JSON.stringify({ server: { http: { port: "nope" } } })); + expect(() => loadConfig(file)).toThrow(); + }); + + it("accepts an oidc credential and an optional devopsCredential", () => { + const dir = mkdtempSync(join(tmpdir(), "ditto-mcp-")); + const file = join(dir, "c.json"); + writeFileSync(file, JSON.stringify({ + ditto: { + enabled: true, + credential: { kind: "oidc", tokenUrl: "https://idp/token", clientId: "c", clientSecret: "s", scope: "ditto" }, + devopsCredential: { kind: "oidc", tokenUrl: "https://idp/token", clientId: "dev", clientSecret: "s2" }, + }, + })); + const cfg = loadConfig(file); + expect(cfg.ditto.credential.kind).toBe("oidc"); + expect(cfg.ditto.credential.tokenUrl).toBe("https://idp/token"); + expect(cfg.ditto.devopsCredential?.clientId).toBe("dev"); + }); + + it("rejects the removed devops credential kind", () => { + const dir = mkdtempSync(join(tmpdir(), "ditto-mcp-")); + const file = join(dir, "c.json"); + writeFileSync(file, JSON.stringify({ ditto: { enabled: true, credential: { kind: "devops", username: "d", password: "s" } } })); + expect(() => loadConfig(file)).toThrow(); + }); +}); + +describe("openApi version config", () => { + it("defaults versionUrlTemplate to the eclipse-ditto raw URL", () => { + const cfg = loadConfig(); + expect(cfg.ditto.openApi.version).toBeUndefined(); + expect(cfg.ditto.openApi.versionUrlTemplate).toBe( + "https://raw.githubusercontent.com/eclipse-ditto/ditto/${version}/documentation/src/main/resources/openapi/ditto-api-2.yml", + ); + }); + + it("accepts an explicit version and custom template", () => { + const dir = mkdtempSync(join(tmpdir(), "ditto-mcp-")); + const file = join(dir, "c.json"); + writeFileSync(file, JSON.stringify({ + ditto: { openApi: { version: "3.6.0", versionUrlTemplate: "https://mirror.example/${version}/spec.yml" } }, + })); + const cfg = loadConfig(file); + expect(cfg.ditto.openApi.version).toBe("3.6.0"); + expect(cfg.ditto.openApi.versionUrlTemplate).toBe("https://mirror.example/${version}/spec.yml"); + }); +}); diff --git a/mcp/src/config/load.ts b/mcp/src/config/load.ts new file mode 100644 index 0000000000..9bbe8227ea --- /dev/null +++ b/mcp/src/config/load.ts @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { readFileSync } from "node:fs"; +import { AppConfigSchema, type AppConfig } from "./schema.js"; + +export function loadConfig(path?: string): AppConfig { + const raw: unknown = path ? JSON.parse(readFileSync(path, "utf8")) : {}; + return AppConfigSchema.parse(raw); +} diff --git a/mcp/src/config/schema.ts b/mcp/src/config/schema.ts new file mode 100644 index 0000000000..ca0559cbf3 --- /dev/null +++ b/mcp/src/config/schema.ts @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { z } from "zod"; + +const CredentialSchema = z.object({ + kind: z.enum(["basic", "oidc"]).default("basic"), + username: z.string().optional(), + password: z.string().optional(), + tokenUrl: z.string().optional(), + clientId: z.string().optional(), + clientSecret: z.string().optional(), + scope: z.string().optional(), +}); + +export type CredentialConfig = z.infer; + +export const AppConfigSchema = z + .object({ + server: z + .object({ + name: z.string().default("ditto-mcp"), + http: z + .object({ + port: z.number().int().positive().default(3000), + host: z.string().default("127.0.0.1"), + enableDnsRebindingProtection: z.boolean().default(true), + allowedHosts: z.array(z.string()).optional(), + allowedOrigins: z.array(z.string()).optional(), + }) + .default({ + port: 3000, + host: "127.0.0.1", + enableDnsRebindingProtection: true, + }), + }) + .default({ + name: "ditto-mcp", + http: { port: 3000, host: "127.0.0.1", enableDnsRebindingProtection: true }, + }), + tools: z + .object({ ping: z.boolean().default(true) }) + .default({ ping: true }), + knowledge: z + .object({ + enabled: z.boolean().default(true), + retriever: z.enum(["fts", "vector", "hybrid"]).default("fts"), + embedding: z + .object({ + model: z.string().default("Xenova/bge-small-en-v1.5"), + dim: z.number().int().positive().default(384), + modelPath: z.string().optional(), + allowRemoteModels: z.boolean().default(true), + cacheDir: z.string().optional(), + batchSize: z.number().int().positive().default(32), + }) + .default({ model: "Xenova/bge-small-en-v1.5", dim: 384, allowRemoteModels: true, batchSize: 32 }), + chunk: z + .object({ + maxChars: z.number().int().positive().default(1000), + overlap: z.number().int().min(0).default(150), + }) + .refine((c) => c.overlap < c.maxChars, { + message: "knowledge.chunk.overlap must be less than maxChars", + }) + .default({ maxChars: 1000, overlap: 150 }), + search: z + .object({ + limit: z.number().int().positive().max(20).default(5), + context: z.number().int().min(0).max(5).default(1), + }) + .default({ limit: 5, context: 1 }), + localDir: z + .object({ + enabled: z.boolean().default(false), + path: z.string().optional(), + id: z.string().default("local"), + }) + .default({ enabled: false, id: "local" }), + publicSource: z + .object({ + enabled: z.boolean().default(true), + url: z + .string() + .default("https://eclipse.dev/ditto/llms.txt"), + maxDocs: z.number().int().positive().optional(), + }) + .default({ enabled: true, url: "https://eclipse.dev/ditto/llms.txt" }), + store: z + .object({ + kind: z.enum(["sqlite", "pgvector"]).default("sqlite"), + sqlite: z + .object({ path: z.string().optional() }) + .default({}), + pgvector: z + .object({ + connectionString: z.string().optional(), + table: z.string().default("ditto_kn"), + }) + .default({ table: "ditto_kn" }), + }) + .default({ kind: "sqlite", sqlite: {}, pgvector: { table: "ditto_kn" } }), + }) + .default({ + enabled: true, + retriever: "fts", + embedding: { model: "Xenova/bge-small-en-v1.5", dim: 384, allowRemoteModels: true, batchSize: 32 }, + chunk: { maxChars: 1000, overlap: 150 }, + search: { limit: 5, context: 1 }, + localDir: { enabled: false, id: "local" }, + publicSource: { enabled: true, url: "https://eclipse.dev/ditto/llms.txt" }, + store: { kind: "sqlite", sqlite: {}, pgvector: { table: "ditto_kn" } }, + }), + ditto: z + .object({ + enabled: z.boolean().default(false), + baseUrl: z.string().optional(), + openApi: z + .object({ + path: z.string().optional(), + url: z.string().optional(), + version: z.string().optional(), + versionUrlTemplate: z + .string() + .default( + "https://raw.githubusercontent.com/eclipse-ditto/ditto/${version}/documentation/src/main/resources/openapi/ditto-api-2.yml", + ), + }) + .default({}), + credential: CredentialSchema.default({ kind: "basic" }), + devopsCredential: CredentialSchema.optional(), + policy: z + .object({ + allowMethods: z.array(z.string()).default(["GET"]), + writeAllowlist: z.array(z.string()).default([]), + sudoAllowlist: z.array(z.string()).default([]), + }) + .default({ allowMethods: ["GET"], writeAllowlist: [], sudoAllowlist: [] }), + }) + .default({ + enabled: false, + openApi: {}, + credential: { kind: "basic" }, + policy: { allowMethods: ["GET"], writeAllowlist: [], sudoAllowlist: [] }, + }), + }) + .default({}); + +export type AppConfig = z.infer; diff --git a/mcp/src/core/types.ts b/mcp/src/core/types.ts new file mode 100644 index 0000000000..42c0aac2ee --- /dev/null +++ b/mcp/src/core/types.ts @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import type { ZodRawShape } from "zod"; +import type { AppConfig } from "../config/schema.js"; + +export interface ToolResult { + content: Array<{ type: "text"; text: string }>; + [key: string]: unknown; +} + +export interface RequestCtx { + config: AppConfig; + /** Present for HTTP sessions; absent over stdio. */ + sessionId?: string; + /** HTTP request headers when available (used by later credential passthrough). */ + headers?: Record; + /** Abort signal for the in-flight request; cancels downstream work. */ + signal?: AbortSignal; +} + +export interface ToolDef { + name: string; + description: string; + /** A zod raw shape (object of zod validators); `{}` for no inputs. */ + inputSchema: ZodRawShape; + handler(args: unknown, ctx: RequestCtx): Promise; +} diff --git a/mcp/src/ditto/action-tool.test.ts b/mcp/src/ditto/action-tool.test.ts new file mode 100644 index 0000000000..1c2c19224c --- /dev/null +++ b/mcp/src/ditto/action-tool.test.ts @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { describe, it, expect, afterEach } from "vitest"; +import { operationToTool } from "./action-tool.js"; +import { HttpDittoClient } from "./client.js"; +import { startFakeDitto } from "./fake-ditto.js"; +import { AppConfigSchema } from "../config/schema.js"; +import { createConfigCredential } from "./credential.js"; +import type { DittoOperation } from "./openapi.js"; + +const cfg = (ditto: object) => AppConfigSchema.parse({ ditto: { enabled: true, ...ditto } }); +const op = (o: Partial): DittoOperation => ({ + operationId: "getThingById", method: "GET", path: "/things/{thingId}", summary: "Retrieve a thing", + description: "Retrieve a thing", params: [{ name: "thingId", in: "path", required: true, type: "string" }], + hasBody: false, securitySchemes: [], ...o, +}); + +let fake: Awaited>; +afterEach(async () => { await fake?.stop(); }); + +describe("operationToTool", () => { + it("builds a tool that calls Ditto with the config credential and returns the response", async () => { + fake = await startFakeDitto(() => ({ status: 200, body: JSON.stringify({ thingId: "ns:1" }) })); + const client = new HttpDittoClient(fake.baseUrl); + const config = cfg({ baseUrl: fake.baseUrl, credential: { kind: "basic", username: "u", password: "p" } }); + const configCredential = createConfigCredential(config.ditto.credential); + const tool = operationToTool(op({}), client, { standard: configCredential }); + const res = await tool.handler({ thingId: "ns:1" }, { config, headers: {} } as never); + expect(res.content[0].text).toContain("ns:1"); + expect(fake.requests[0].auth).toBe(`Basic ${Buffer.from("u:p").toString("base64")}`); + }); + + it("refuses a sudo op without a devops credential (does not call Ditto)", async () => { + fake = await startFakeDitto(() => ({ status: 200, body: "should not be called" })); + const client = new HttpDittoClient(fake.baseUrl); + const config = cfg({ baseUrl: fake.baseUrl, credential: { kind: "basic", username: "u", password: "p" } }); + const configCredential = createConfigCredential(config.ditto.credential); + const tool = operationToTool(op({ operationId: "sudoRetrieveThing", path: "/sudo/things/{thingId}" }), client, { standard: configCredential }); + const res = await tool.handler({ thingId: "ns:1" }, { config, headers: {} } as never); + expect(res.content[0].text.toLowerCase()).toContain("devops"); + expect(fake.requests).toHaveLength(0); + }); + + it("produces a typed body schema when bodySchema has props", () => { + const client = new HttpDittoClient("http://fake"); + const config = cfg({ baseUrl: "http://fake", credential: { kind: "basic", username: "u", password: "p" } }); + const configCredential = createConfigCredential(config.ditto.credential); + const withBodySchema = op({ + operationId: "postThing", method: "POST", hasBody: true, + bodySchema: { props: [ + { name: "thingId", type: "string", required: true }, + { name: "counter", type: "number", required: false }, + ]}, + }); + const tool = operationToTool(withBodySchema, client, { standard: configCredential }); + const schema = tool.inputSchema; + expect(Object.keys(schema)).toContain("body"); + expect(Object.keys(schema)).toContain("thingId"); + // zod shape should include typed body props + const bodyShape = (schema.body as any)?._def; + expect(bodyShape).toBeDefined(); + }); + + it("exposes a body arg for hasBody even without bodySchema", () => { + const client = new HttpDittoClient("http://fake"); + const config = cfg({ baseUrl: "http://fake", credential: { kind: "basic", username: "u", password: "p" } }); + const configCredential = createConfigCredential(config.ditto.credential); + const noBodySchema = op({ operationId: "postAnything", method: "POST", hasBody: true }); + const tool = operationToTool(noBodySchema, client, { standard: configCredential }); + const schema = tool.inputSchema; + expect(Object.keys(schema)).toContain("body"); + }); + + it("accepts an object value for a $ref/unknown body prop (z.any, not z.string)", () => { + const client = new HttpDittoClient("http://fake"); + const config = cfg({ baseUrl: "http://fake", credential: { kind: "basic", username: "u", password: "p" } }); + const configCredential = createConfigCredential(config.ditto.credential); + const withUnknownProp = op({ + operationId: "putThing", method: "PUT", hasBody: true, + bodySchema: { props: [ + { name: "policyId", type: "string", required: true }, + { name: "attributes", type: "unknown", required: false }, + ]}, + }); + const tool = operationToTool(withUnknownProp, client, { standard: configCredential }); + const schema = tool.inputSchema; + // Validate that an object value is accepted for 'attributes' (proves it's z.any, not z.string) + const bodySchema = (schema.body as any); + expect(() => bodySchema.parse({ body: { attributes: { color: "blue" } } })).not.toThrow(); + expect(() => bodySchema.parse({ body: { policyId: "ns:p", attributes: { nested: { deep: true } } } })).not.toThrow(); + }); + + it("makes all body props optional (even required props)", () => { + const client = new HttpDittoClient("http://fake"); + const config = cfg({ baseUrl: "http://fake", credential: { kind: "basic", username: "u", password: "p" } }); + const configCredential = createConfigCredential(config.ditto.credential); + const withRequiredProp = op({ + operationId: "postThing", method: "POST", hasBody: true, + bodySchema: { props: [ + { name: "thingId", type: "string", required: true }, + ]}, + }); + const tool = operationToTool(withRequiredProp, client, { standard: configCredential }); + const schema = tool.inputSchema; + const bodySchema = (schema.body as any); + // The tool accepts a body without the "required" prop + expect(() => bodySchema.parse({ body: {} })).not.toThrow(); + expect(() => bodySchema.parse({ body: { otherField: "x" } })).not.toThrow(); + }); + + it("routes a sudo op to the devops credential", async () => { + fake = await startFakeDitto(() => ({ status: 200, body: "{}" })); + const client = new HttpDittoClient(fake.baseUrl); + const config = cfg({ + baseUrl: fake.baseUrl, + credential: { kind: "basic", username: "app", password: "p" }, + devopsCredential: { kind: "basic", username: "dev", password: "s" }, + }); + const creds = { + standard: createConfigCredential(config.ditto.credential), + devops: createConfigCredential(config.ditto.devopsCredential!), + }; + const tool = operationToTool(op({ operationId: "sudoRetrieveThing", path: "/sudo/things/{thingId}" }), client, creds); + await tool.handler({ thingId: "ns:1" }, { config, headers: {} } as never); + expect(fake.requests[0].auth).toBe(`Basic ${Buffer.from("dev:s").toString("base64")}`); + }); + + it("routes a non-sudo op to the standard credential", async () => { + fake = await startFakeDitto(() => ({ status: 200, body: "{}" })); + const client = new HttpDittoClient(fake.baseUrl); + const config = cfg({ + baseUrl: fake.baseUrl, + credential: { kind: "basic", username: "app", password: "p" }, + devopsCredential: { kind: "basic", username: "dev", password: "s" }, + }); + const creds = { + standard: createConfigCredential(config.ditto.credential), + devops: createConfigCredential(config.ditto.devopsCredential!), + }; + const tool = operationToTool(op({}), client, creds); // getThingById, GET /things/{thingId} + await tool.handler({ thingId: "ns:1" }, { config, headers: {} } as never); + expect(fake.requests[0].auth).toBe(`Basic ${Buffer.from("app:p").toString("base64")}`); + }); +}); diff --git a/mcp/src/ditto/action-tool.ts b/mcp/src/ditto/action-tool.ts new file mode 100644 index 0000000000..6a34748d3d --- /dev/null +++ b/mcp/src/ditto/action-tool.ts @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { z, type ZodRawShape } from "zod"; +import type { ToolDef, ToolResult, RequestCtx } from "../core/types.js"; +import type { DittoOperation } from "./openapi.js"; +import type { DittoClient } from "./client.js"; +import type { DittoCredential } from "./credential.js"; +import { resolveCredential } from "./credential.js"; +import { isSudo } from "./tool-policy.js"; + +// Map an operationId to a legal MCP tool name (MCP names must match [A-Za-z0-9_-]). +// Replace illegal chars, collapse runs of "_", and trim the edges. For specs that +// declare no operationId, the id is a synthesized "_" fallback, so +// collapsing keeps names clean: the /api/2/things GET tool becomes "GET_api_2_things" +// rather than "GET__api_2_things". +function sanitizeName(id: string): string { + return id + .replace(/[^A-Za-z0-9_]/g, "_") + .replace(/_+/g, "_") + .replace(/^_|_$/g, "") + .slice(0, 64); +} + +function inputSchema(op: DittoOperation): ZodRawShape { + const shape: ZodRawShape = {}; + for (const p of op.params) { + const base = p.type === "number" ? z.number() : p.type === "boolean" ? z.boolean() : z.string(); + shape[p.name] = (p.required ? base : base.optional()).describe(`${p.in} parameter ${p.name}`); + } + if (op.bodySchema?.props.length) { + const bodyShape: ZodRawShape = {}; + for (const bp of op.bodySchema.props) { + const base = bp.type === "number" ? z.number() + : bp.type === "boolean" ? z.boolean() + : bp.type === "object" ? z.record(z.any()) + : bp.type === "array" ? z.array(z.any()) + : bp.type === "unknown" ? z.any() + : z.string(); + const desc = `body.${bp.name}` + (bp.required ? " (required)" : ""); + bodyShape[bp.name] = base.optional().describe(desc); + } + shape.body = z.object(bodyShape).passthrough().optional().describe("JSON request body"); + } else if (op.hasBody) { + shape.body = z.any().optional().describe("JSON request body"); + } + return shape; +} + +function text(t: string): ToolResult { + return { content: [{ type: "text", text: t }] }; +} + +export interface ToolCredentials { + standard: DittoCredential; + devops?: DittoCredential; +} + +export function operationToTool(op: DittoOperation, client: DittoClient, creds: ToolCredentials): ToolDef { + const sudo = isSudo(op); + return { + name: sanitizeName(op.operationId), + description: + `${op.method} ${op.path} — ${op.description || op.summary}` + + (sudo ? " [sudo — requires a devops credential]" : ""), + inputSchema: inputSchema(op), + handler: async (args: unknown, ctx: RequestCtx): Promise => { + // Non-sudo ops always have `standard`; sudo ops require the `devops` slot. + const base = sudo ? creds.devops : creds.standard; + if (!base) { + return text(`Refused: "${op.operationId}" is a sudo operation and requires ditto.devopsCredential.`); + } + const credential = resolveCredential(base, { headers: ctx.headers }); + const res = await client.execute(op, (args ?? {}) as Record, credential, ctx.signal); + return text(`HTTP ${res.status}\n${res.body}`); + }, + }; +} diff --git a/mcp/src/ditto/action-tools.test.ts b/mcp/src/ditto/action-tools.test.ts new file mode 100644 index 0000000000..eee6294024 --- /dev/null +++ b/mcp/src/ditto/action-tools.test.ts @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { describe, it, expect, afterEach } from "vitest"; +import { makeActionTools, buildVersionUrl } from "./action-tools.js"; +import { HttpDittoClient } from "./client.js"; +import { startFakeDitto } from "./fake-ditto.js"; +import { AppConfigSchema } from "../config/schema.js"; + +describe("buildVersionUrl", () => { + it("substitutes the ${version} placeholder", () => { + const template = + "https://raw.githubusercontent.com/eclipse-ditto/ditto/${version}/documentation/src/main/resources/openapi/ditto-api-2.yml"; + expect(buildVersionUrl(template, "3.6.0")).toBe( + "https://raw.githubusercontent.com/eclipse-ditto/ditto/3.6.0/documentation/src/main/resources/openapi/ditto-api-2.yml", + ); + }); + + it("substitutes every occurrence", () => { + expect(buildVersionUrl("a/${version}/b/${version}", "1.2.3")).toBe("a/1.2.3/b/1.2.3"); + }); +}); + +const SPEC = { + paths: { + "/things/{thingId}": { + get: { operationId: "getThingById", summary: "get", parameters: [{ name: "thingId", in: "path", required: true, schema: { type: "string" } }] }, + put: { operationId: "putThing", summary: "put", parameters: [{ name: "thingId", in: "path", required: true, schema: { type: "string" } }], requestBody: {} }, + }, + "/sudo/things/{thingId}": { + get: { operationId: "sudoRetrieveThing", summary: "sudo get", parameters: [{ name: "thingId", in: "path", required: true, schema: { type: "string" } }] }, + }, + }, +}; + +let fake: Awaited>; +afterEach(async () => { await fake?.stop(); }); + +async function tools(policy: object, credKind = "basic") { + fake = await startFakeDitto((req) => ({ status: 200, body: JSON.stringify({ url: req.url }) })); + const config = AppConfigSchema.parse({ + ditto: { enabled: true, baseUrl: fake.baseUrl, credential: { kind: credKind, username: "u", password: "p" }, policy }, + }); + const list = await makeActionTools(config, { loadSpec: async () => SPEC, client: new HttpDittoClient(fake.baseUrl) }); + return { config, byName: Object.fromEntries(list.map((t) => [t.name, t])) }; +} + +describe("makeActionTools", () => { + it("registers only GET (read-only) by default", async () => { + const { byName } = await tools({ allowMethods: ["GET"], writeAllowlist: [], sudoAllowlist: [] }); + expect(byName.getThingById).toBeDefined(); + expect(byName.putThing).toBeUndefined(); // write blocked + expect(byName.sudoRetrieveThing).toBeUndefined(); // sudo blocked + }); + + it("includes a write when allowlisted and a sudo when sudoAllowlisted", async () => { + const { byName } = await tools({ allowMethods: ["GET"], writeAllowlist: ["putThing"], sudoAllowlist: ["sudoRetrieveThing"] }); + expect(byName.putThing).toBeDefined(); + expect(byName.sudoRetrieveThing).toBeDefined(); + }); + + it("a registered GET tool calls the fake Ditto", async () => { + const { byName, config } = await tools({ allowMethods: ["GET"], writeAllowlist: [], sudoAllowlist: [] }); + const res = await byName.getThingById.handler({ thingId: "ns:1" }, { config, headers: {} } as never); + expect(res.content[0].text).toContain("/things/ns:1"); + }); + + it("de-duplicates colliding tool names by appending _2, _3, ...", async () => { + const specWithCollision = { + paths: { + "/a": { get: { operationId: "foo-bar", summary: "a" } }, + "/b": { get: { operationId: "foo/bar", summary: "b" } }, + "/c": { get: { operationId: "foo.bar", summary: "c" } }, + }, + }; + fake = await startFakeDitto(() => ({ status: 200, body: "{}" })); + const config = AppConfigSchema.parse({ + ditto: { enabled: true, baseUrl: fake.baseUrl, credential: { kind: "basic", username: "u", password: "p" }, policy: { allowMethods: ["GET"], writeAllowlist: [], sudoAllowlist: [] } }, + }); + const list = await makeActionTools(config, { loadSpec: async () => specWithCollision, client: new HttpDittoClient(fake.baseUrl) }); + const names = list.map((t) => t.name); + expect(names).toContain("foo_bar"); + expect(names).toContain("foo_bar_2"); + expect(names).toContain("foo_bar_3"); + expect(names.length).toBe(3); + }); + + it("builds a clean tool name from a synthesized operationId (no operationId in spec)", async () => { + const specNoOpId = { + paths: { + "/api/2/things/{thingId}": { + get: { summary: "get", parameters: [{ name: "thingId", in: "path", required: true, schema: { type: "string" } }] }, + }, + }, + }; + fake = await startFakeDitto(() => ({ status: 200, body: "{}" })); + const config = AppConfigSchema.parse({ + ditto: { enabled: true, baseUrl: fake.baseUrl, credential: { kind: "basic", username: "u", password: "p" }, policy: { allowMethods: ["GET"], writeAllowlist: [], sudoAllowlist: [] } }, + }); + const list = await makeActionTools(config, { loadSpec: async () => specNoOpId, client: new HttpDittoClient(fake.baseUrl) }); + expect(list.map((t) => t.name)).toEqual(["GET_api_2_things_thingId"]); + }); + + it("a sudo tool refuses at call time when no devopsCredential is configured", async () => { + const { byName, config } = await tools({ allowMethods: ["GET"], writeAllowlist: [], sudoAllowlist: ["sudoRetrieveThing"] }); + const res = await byName.sudoRetrieveThing.handler({ thingId: "ns:1" }, { config, headers: {} } as never); + expect(res.content[0].text.toLowerCase()).toContain("devopscredential"); + }); +}); diff --git a/mcp/src/ditto/action-tools.ts b/mcp/src/ditto/action-tools.ts new file mode 100644 index 0000000000..9158c23132 --- /dev/null +++ b/mcp/src/ditto/action-tools.ts @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { readFile } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; +import YAML from "yaml"; +import type { AppConfig } from "../config/schema.js"; +import type { ToolDef } from "../core/types.js"; +import type { DittoClient } from "./client.js"; +import { HttpDittoClient } from "./client.js"; +import { parseOperations } from "./openapi.js"; +import { isAllowed } from "./tool-policy.js"; +import { operationToTool, type ToolCredentials } from "./action-tool.js"; +import { createConfigCredential } from "./credential.js"; + +export interface ActionToolDeps { + loadSpec?: () => Promise; + client?: DittoClient; +} + +// Canonical Ditto OpenAPI committed in the monorepo. Resolves from both src (tsx) +// and dist (tsc): dirname is src/ditto or dist/ditto -> three levels up = repo root. +const CANONICAL_SPEC = join( + dirname(fileURLToPath(import.meta.url)), + "..", "..", "..", + "documentation", "src", "main", "resources", "openapi", "ditto-api-2.yml", +); + +// Substitute the literal ${version} placeholder in a version URL template. +export function buildVersionUrl(template: string, version: string): string { + return template.replaceAll("${version}", version); +} + +async function fetchSpec(url: string): Promise { + const res = await fetch(url, { signal: AbortSignal.timeout(15000) }); + if (!res.ok) throw new Error(`openapi fetch ${url} -> ${res.status}`); + return YAML.parse(await res.text()); +} + +// YAML.parse also parses JSON, so this handles .yml, .yaml, and .json specs. +async function defaultLoadSpec(config: AppConfig): Promise { + const { path, url, version, versionUrlTemplate } = config.ditto.openApi; + if (path) return YAML.parse(await readFile(path, "utf8")); + if (url) return fetchSpec(url); + if (version) return fetchSpec(buildVersionUrl(versionUrlTemplate, version)); + // Fallback: the canonical Ditto spec committed in this monorepo. + return YAML.parse(await readFile(CANONICAL_SPEC, "utf8")); +} + +export async function makeActionTools(config: AppConfig, deps: ActionToolDeps = {}): Promise { + const client = + deps.client ?? + (config.ditto.baseUrl + ? new HttpDittoClient(config.ditto.baseUrl) + : undefined); + if (!client) { + process.stderr.write("[ditto-mcp] action tools disabled: ditto.baseUrl is required\n"); + return []; + } + let spec: unknown; + try { + spec = deps.loadSpec ? await deps.loadSpec() : await defaultLoadSpec(config); + } catch (err) { + process.stderr.write(`[ditto-mcp] action tools disabled: ${String(err)}\n`); + return []; + } + const dc = config.ditto.devopsCredential; + if (dc?.kind === "basic" && dc.username === undefined) { + process.stderr.write( + "[ditto-mcp] devopsCredential is 'basic' but has no username; sudo requests will be sent unauthenticated\n", + ); + } + const creds: ToolCredentials = { + standard: createConfigCredential(config.ditto.credential), + devops: dc ? createConfigCredential(dc) : undefined, + }; + const tools = parseOperations(spec) + .filter((op) => isAllowed(op, config.ditto.policy)) + .map((op) => operationToTool(op, client, creds)); + // De-duplicate tool names: on collision, append _2, _3, ... + const seen = new Map(); + for (const tool of tools) { + const base = tool.name; + const count = seen.get(base) ?? 0; + seen.set(base, count + 1); + if (count > 0) tool.name = `${base}_${count + 1}`; + } + return tools; +} diff --git a/mcp/src/ditto/bundled-spec.test.ts b/mcp/src/ditto/bundled-spec.test.ts new file mode 100644 index 0000000000..9e38335e0c --- /dev/null +++ b/mcp/src/ditto/bundled-spec.test.ts @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { describe, it, expect } from "vitest"; +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; +import YAML from "yaml"; +import { parseOperations } from "./openapi.js"; +import { isSudo } from "./tool-policy.js"; + +const specPath = join( + dirname(fileURLToPath(import.meta.url)), + "..", "..", "..", + "documentation", "src", "main", "resources", "openapi", "ditto-api-2.yml", +); + +describe("bundled Ditto spec", () => { + it("parses and yields operations incl. a things GET with a resolved path param", () => { + const spec = YAML.parse(readFileSync(specPath, "utf8")); + const ops = parseOperations(spec); + expect(ops.length).toBeGreaterThan(20); + const getThing = ops.find((o) => o.method === "GET" && o.path.includes("/things/{thingId}")); + expect(getThing).toBeDefined(); + expect(getThing!.params.some((p) => p.name === "thingId" && p.in === "path")).toBe(true); + }); + + it("classifies /api/2/connections GET as isSudo (DevOpsBasic security)", () => { + const spec = YAML.parse(readFileSync(specPath, "utf8")); + const ops = parseOperations(spec); + const getConnections = ops.find((o) => o.method === "GET" && o.path.startsWith("/api/2/connections")); + expect(getConnections).toBeDefined(); + expect(isSudo(getConnections!)).toBe(true); + }); + + it("resolves bodySchema for PUT /api/2/things/{thingId}", () => { + const spec = YAML.parse(readFileSync(specPath, "utf8")); + const ops = parseOperations(spec); + const putThing = ops.find((o) => o.method === "PUT" && o.path === "/api/2/things/{thingId}"); + expect(putThing).toBeDefined(); + expect(putThing!.hasBody).toBe(true); + // The NewThing schema is an object with properties, so bodySchema should be defined + expect(putThing!.bodySchema).toBeDefined(); + expect(putThing!.bodySchema!.props.length).toBeGreaterThan(0); + // Verify some expected properties are present (policyId, definition, attributes, features, _policy, _copyPolicyFrom) + const propNames = putThing!.bodySchema!.props.map(p => p.name); + expect(propNames).toContain("policyId"); + }); +}); diff --git a/mcp/src/ditto/client.test.ts b/mcp/src/ditto/client.test.ts new file mode 100644 index 0000000000..7a7ad16ebd --- /dev/null +++ b/mcp/src/ditto/client.test.ts @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { describe, it, expect, afterEach } from "vitest"; +import { HttpDittoClient } from "./client.js"; +import { startFakeDitto } from "./fake-ditto.js"; +import type { DittoOperation } from "./openapi.js"; + +const op = (over: Partial): DittoOperation => ({ + operationId: "op", method: "GET", path: "/x", summary: "", description: "", params: [], hasBody: false, ...over, +}); +const cred = (h?: string) => ({ authHeader: async () => h }); + +let fake: Awaited>; +afterEach(async () => { await fake?.stop(); }); + +describe("HttpDittoClient", () => { + it("substitutes path params, sends query, forwards auth", async () => { + fake = await startFakeDitto((req) => ({ status: 200, body: JSON.stringify({ ok: req.url }) })); + const client = new HttpDittoClient(fake.baseUrl); + const res = await client.execute( + op({ path: "/things/{thingId}", params: [ + { name: "thingId", in: "path", required: true, type: "string" }, + { name: "fields", in: "query", required: false, type: "string" }] }), + { thingId: "ns:1", fields: "attributes" }, + cred("Basic abc"), + ); + expect(res.status).toBe(200); + expect(fake.requests[0].url).toBe("/things/ns:1?fields=attributes"); + expect(fake.requests[0].auth).toBe("Basic abc"); + }); + + it("sends a JSON body for write ops", async () => { + fake = await startFakeDitto(() => ({ status: 201, body: "" })); + const client = new HttpDittoClient(fake.baseUrl); + const res = await client.execute( + op({ method: "PUT", path: "/things/{id}", hasBody: true, + params: [{ name: "id", in: "path", required: true, type: "string" }] }), + { id: "ns:1", body: { attributes: { a: 1 } } }, + cred(), + ); + expect(res.status).toBe(201); + expect(JSON.parse(fake.requests[0].body!)).toEqual({ attributes: { a: 1 } }); + }); + + it("encodes unsafe path-param chars but keeps the Ditto namespace colon", async () => { + fake = await startFakeDitto(() => ({ status: 200, body: "" })); + const client = new HttpDittoClient(fake.baseUrl); + await client.execute( + op({ path: "/things/{thingId}", params: [{ name: "thingId", in: "path", required: true, type: "string" }] }), + { thingId: "ns:a b/c" }, + cred(), + ); + expect(fake.requests[0].url).toBe("/things/ns:a%20b%2Fc"); // colon kept; space+slash encoded + }); +}); diff --git a/mcp/src/ditto/client.ts b/mcp/src/ditto/client.ts new file mode 100644 index 0000000000..521946eb1a --- /dev/null +++ b/mcp/src/ditto/client.ts @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import type { DittoOperation } from "./openapi.js"; +import type { DittoCredential } from "./credential.js"; + +export interface DittoResponse { + status: number; + body: string; +} + +export interface DittoClient { + execute( + op: DittoOperation, + args: Record, + credential: DittoCredential, + signal?: AbortSignal, + ): Promise; +} + +export class HttpDittoClient implements DittoClient { + constructor( + private readonly baseUrl: string, + private readonly fetchFn: typeof fetch = fetch, + ) {} + + async execute( + op: DittoOperation, + args: Record, + credential: DittoCredential, + signal?: AbortSignal, + ): Promise { + let path = op.path; + const query = new URLSearchParams(); + for (const p of op.params) { + const v = args[p.name]; + if (p.in === "path") { + path = path.replace(`{${p.name}}`, encodeURIComponent(String(v ?? "")).replace(/%3A/g, ":")); + } else if (v !== undefined) { + query.set(p.name, String(v)); + } + } + const qs = query.toString(); + const url = `${this.baseUrl}${path}${qs ? `?${qs}` : ""}`; + + const headers: Record = {}; + const auth = await credential.authHeader(signal); + if (auth) headers["authorization"] = auth; + let body: string | undefined; + if (op.hasBody && args.body !== undefined) { + headers["content-type"] = "application/json"; + body = JSON.stringify(args.body); + } + + const res = await this.fetchFn(url, { method: op.method, headers, body, signal }); + return { status: res.status, body: await res.text() }; + } +} diff --git a/mcp/src/ditto/credential.test.ts b/mcp/src/ditto/credential.test.ts new file mode 100644 index 0000000000..5eea2fbac8 --- /dev/null +++ b/mcp/src/ditto/credential.test.ts @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { describe, it, expect } from "vitest"; +import { AppConfigSchema } from "../config/schema.js"; +import { createConfigCredential, resolveCredential } from "./credential.js"; +import { startFakeOidc } from "./fake-oidc.js"; + +const cred = (c: object) => + AppConfigSchema.parse({ ditto: { enabled: true, credential: c } }).ditto.credential; + +describe("credentials", () => { + it("basic → Basic header", async () => { + const c = createConfigCredential(cred({ kind: "basic", username: "u", password: "p" })); + expect(await c.authHeader()).toBe(`Basic ${Buffer.from("u:p").toString("base64")}`); + }); + + it("basic with no username → no header", async () => { + const c = createConfigCredential(cred({ kind: "basic" })); + expect(await c.authHeader()).toBeUndefined(); + }); + + it("session Authorization overrides the base credential", async () => { + const base = createConfigCredential(cred({ kind: "basic", username: "u", password: "p" })); + const c = resolveCredential(base, { headers: { Authorization: "Bearer sess" } }); + expect(await c.authHeader()).toBe("Bearer sess"); + }); + + it("resolveCredential returns the base when no session header", async () => { + const base = createConfigCredential(cred({ kind: "basic", username: "u", password: "p" })); + const c = resolveCredential(base, { headers: {} }); + expect(await c.authHeader()).toBe(`Basic ${Buffer.from("u:p").toString("base64")}`); + }); + + it("oidc fetches a bearer token and caches it (one token call for two uses)", async () => { + const oidc = await startFakeOidc({ access_token: "tok123", expires_in: 3600 }); + try { + const c = createConfigCredential(cred({ kind: "oidc", tokenUrl: oidc.tokenUrl, clientId: "c", clientSecret: "s" })); + expect(await c.authHeader()).toBe("Bearer tok123"); + expect(await c.authHeader()).toBe("Bearer tok123"); + expect(oidc.calls).toBe(1); + } finally { await oidc.stop(); } + }); + + it("oidc sends grant_type=client_credentials, Basic auth, and scope (when set)", async () => { + const oidc = await startFakeOidc({ access_token: "tok", expires_in: 3600 }); + try { + const c = createConfigCredential(cred({ kind: "oidc", tokenUrl: oidc.tokenUrl, clientId: "myClient", clientSecret: "mySecret", scope: "scope1 scope2" })); + await c.authHeader(); + const req = oidc.requests[0]; + expect(req.method).toBe("POST"); + expect(req.headers.authorization).toBe(`Basic ${Buffer.from("myClient:mySecret").toString("base64")}`); + expect(req.headers["content-type"]).toBe("application/x-www-form-urlencoded"); + expect(req.body).toContain("grant_type=client_credentials"); + expect(req.body).toContain("scope=scope1+scope2"); + } finally { await oidc.stop(); } + }); +}); diff --git a/mcp/src/ditto/credential.ts b/mcp/src/ditto/credential.ts new file mode 100644 index 0000000000..bb676f6949 --- /dev/null +++ b/mcp/src/ditto/credential.ts @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import type { CredentialConfig } from "../config/schema.js"; + +export interface DittoCredential { + authHeader(signal?: AbortSignal): Promise; +} + +function first(h: string | string[] | undefined): string | undefined { + return Array.isArray(h) ? h[0] : h; +} + +class StaticCredential implements DittoCredential { + constructor(private readonly header: string | undefined) {} + async authHeader(): Promise { + return this.header; + } +} + +interface OidcOptions { + tokenUrl: string; + clientId: string; + clientSecret: string; + scope?: string; +} + +export class OidcClientCredential implements DittoCredential { + private token?: string; + private expiresAt = 0; + private inflight?: Promise; + + constructor(private readonly opts: OidcOptions, private readonly fetchFn: typeof fetch = fetch) {} + + async authHeader(signal?: AbortSignal): Promise { + const now = Date.now(); + if (this.token && now < this.expiresAt) return `Bearer ${this.token}`; + if (!this.inflight) this.inflight = this.fetchToken(signal).finally(() => { this.inflight = undefined; }); + const token = await this.inflight; + return `Bearer ${token}`; + } + + private async fetchToken(signal?: AbortSignal): Promise { + const body = new URLSearchParams({ grant_type: "client_credentials" }); + if (this.opts.scope) body.set("scope", this.opts.scope); + const basic = Buffer.from(`${this.opts.clientId}:${this.opts.clientSecret}`).toString("base64"); + const timeoutSignal = AbortSignal.timeout(15000); + const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal; + const res = await this.fetchFn(this.opts.tokenUrl, { + method: "POST", + headers: { authorization: `Basic ${basic}`, "content-type": "application/x-www-form-urlencoded" }, + body: body.toString(), + signal: combinedSignal, + }); + if (!res.ok) throw new Error(`oidc token endpoint ${this.opts.tokenUrl} -> ${res.status}`); + const json = (await res.json()) as { access_token?: string; expires_in?: number }; + if (!json.access_token) throw new Error("oidc token response missing access_token"); + this.token = json.access_token; + this.expiresAt = Date.now() + Math.max(0, (json.expires_in ?? 300) - 30) * 1000; + return this.token; + } +} + +/** Build a credential from a single credential-config object (standard or devops slot). */ +export function createConfigCredential(c: CredentialConfig, fetchFn: typeof fetch = fetch): DittoCredential { + if (c.kind === "oidc") { + if (!c.tokenUrl || !c.clientId || !c.clientSecret) { + process.stderr.write("[ditto-mcp] oidc credential missing tokenUrl/clientId/clientSecret; using no credential\n"); + return new StaticCredential(undefined); + } + return new OidcClientCredential( + { tokenUrl: c.tokenUrl, clientId: c.clientId, clientSecret: c.clientSecret, scope: c.scope }, + fetchFn, + ); + } + if (c.username !== undefined) { + return new StaticCredential(`Basic ${Buffer.from(`${c.username}:${c.password ?? ""}`).toString("base64")}`); + } + return new StaticCredential(undefined); +} + +/** A per-session `Authorization` header, when present, replaces the base credential. */ +export function resolveCredential( + base: DittoCredential, + ctx: { headers?: Record }, +): DittoCredential { + const headers = ctx.headers ?? {}; + const key = Object.keys(headers).find((k) => k.toLowerCase() === "authorization"); + const sessionAuth = first(key ? headers[key] : undefined); + if (sessionAuth) return new StaticCredential(sessionAuth); + return base; +} diff --git a/mcp/src/ditto/fake-ditto.ts b/mcp/src/ditto/fake-ditto.ts new file mode 100644 index 0000000000..3ebac59dea --- /dev/null +++ b/mcp/src/ditto/fake-ditto.ts @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { createServer, type Server } from "node:http"; + +export interface RecordedRequest { method: string; url: string; auth?: string; body?: string } +export interface FakeReply { status: number; body: string } + +export async function startFakeDitto( + handler: (req: RecordedRequest) => FakeReply, +): Promise<{ baseUrl: string; stop: () => Promise; requests: RecordedRequest[] }> { + const requests: RecordedRequest[] = []; + const server: Server = createServer((req, res) => { + const chunks: Buffer[] = []; + req.on("data", (c) => chunks.push(c as Buffer)); + req.on("end", () => { + const rec: RecordedRequest = { + method: req.method ?? "GET", + url: req.url ?? "/", + auth: req.headers["authorization"] as string | undefined, + body: chunks.length ? Buffer.concat(chunks).toString("utf8") : undefined, + }; + requests.push(rec); + const reply = handler(rec); + res.statusCode = reply.status; + res.setHeader("content-type", "application/json"); + res.end(reply.body); + }); + }); + await new Promise((r) => server.listen(0, "127.0.0.1", r)); + const addr = server.address(); + const port = typeof addr === "object" && addr ? addr.port : 0; + return { + baseUrl: `http://127.0.0.1:${port}`, + stop: () => new Promise((r) => server.close(() => r())), + requests, + }; +} diff --git a/mcp/src/ditto/fake-oidc.ts b/mcp/src/ditto/fake-oidc.ts new file mode 100644 index 0000000000..4ccdbd15dd --- /dev/null +++ b/mcp/src/ditto/fake-oidc.ts @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { createServer, type Server } from "node:http"; + +export interface FakeOidcRequest { + method: string; + headers: Record; + body: string; +} + +export async function startFakeOidc( + token: { access_token: string; expires_in: number }, +): Promise<{ tokenUrl: string; stop: () => Promise; calls: number; requests: FakeOidcRequest[] }> { + let calls = 0; + const requests: FakeOidcRequest[] = []; + const server: Server = createServer((req, res) => { + calls++; + let body = ""; + req.on("data", (chunk) => { body += chunk; }); + req.on("end", () => { + requests.push({ method: req.method ?? "", headers: req.headers as Record, body }); + res.setHeader("content-type", "application/json"); + res.end(JSON.stringify(token)); + }); + }); + await new Promise((r) => server.listen(0, "127.0.0.1", r)); + const addr = server.address(); + const port = typeof addr === "object" && addr ? addr.port : 0; + return { + tokenUrl: `http://127.0.0.1:${port}/token`, + stop: () => new Promise((r) => server.close(() => r())), + get calls() { return calls; }, + requests, + }; +} diff --git a/mcp/src/ditto/openapi.test.ts b/mcp/src/ditto/openapi.test.ts new file mode 100644 index 0000000000..f03aa6fc74 --- /dev/null +++ b/mcp/src/ditto/openapi.test.ts @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { describe, it, expect } from "vitest"; +import { parseOperations } from "./openapi.js"; + +// Mirrors Ditto's real shape: a $ref'd component parameter + a path-item-level parameter. +const SPEC = { + components: { + parameters: { + ThingIdPathParam: { name: "thingId", in: "path", required: true, schema: { type: "string" } }, + }, + }, + paths: { + "/things/{thingId}": { + parameters: [{ $ref: "#/components/parameters/ThingIdPathParam" }], // path-item level, shared + get: { + operationId: "getThingById", + summary: "Retrieve a thing", + parameters: [{ name: "fields", in: "query", required: false, schema: { type: "string" } }], + }, + put: { + operationId: "putThing", + summary: "Create or update a thing", + requestBody: { content: { "application/json": { schema: { $ref: "#/components/schemas/NewThing" } } } }, + }, + }, + "/search/things": { + get: { + operationId: "searchThings", + summary: "Search things", + parameters: [{ name: "filter", in: "query", required: false, schema: { type: "string" } }], + }, + }, + }, +}; + +describe("parseOperations", () => { + it("merges path-item params, resolves $ref params, extracts body existence", () => { + const ops = parseOperations(SPEC); + const get = ops.find((o) => o.operationId === "getThingById")!; + expect(get.method).toBe("GET"); + expect(get.path).toBe("/things/{thingId}"); + // path-item $ref param (thingId) merged with op-level query param (fields): + expect(get.params).toContainEqual({ name: "thingId", in: "path", required: true, type: "string" }); + expect(get.params).toContainEqual({ name: "fields", in: "query", required: false, type: "string" }); + expect(get.hasBody).toBe(false); + + const put = ops.find((o) => o.operationId === "putThing")!; + expect(put.method).toBe("PUT"); + expect(put.params).toContainEqual({ name: "thingId", in: "path", required: true, type: "string" }); // inherited + expect(put.hasBody).toBe(true); // requestBody exists (schema $ref not resolved) + + const search = ops.find((o) => o.operationId === "searchThings")!; + expect(search.params[0]).toEqual({ name: "filter", in: "query", required: false, type: "string" }); + }); + + it("synthesizes an operationId when missing", () => { + const ops = parseOperations({ paths: { "/x": { get: {} } } }); + expect(ops[0].operationId).toBe("GET_/x"); + }); + + it("captures securitySchemes from op.security", () => { + const spec = { + paths: { + "/api/2/connections": { + get: { + operationId: "getConnections", + summary: "list connections", + security: [{ DevOpsBasic: [] }], + }, + }, + }, + }; + const ops = parseOperations(spec); + expect(ops[0].securitySchemes).toEqual(["DevOpsBasic"]); + }); + + it("captures securitySchemes from spec-level security when op.security is missing", () => { + const spec = { + security: [{ ApiKeyAuth: [] }], + paths: { + "/things": { + get: { operationId: "getThings", summary: "list things" }, + }, + }, + }; + const ops = parseOperations(spec); + expect(ops[0].securitySchemes).toEqual(["ApiKeyAuth"]); + }); + + it("returns empty securitySchemes when no security is defined", () => { + const spec = { + paths: { + "/public": { + get: { operationId: "getPublic", summary: "public endpoint" }, + }, + }, + }; + const ops = parseOperations(spec); + expect(ops[0].securitySchemes).toEqual([]); + }); + + it("resolves a requestBody object schema ($ref) into a shallow BodySchema", () => { + const spec = { + components: { schemas: { NewThing: { type: "object", required: ["thingId"], properties: { + thingId: { type: "string" }, attributes: { type: "object" }, counter: { type: "integer" } } } } }, + paths: { "/things": { post: { operationId: "postThing", + requestBody: { content: { "application/json": { schema: { $ref: "#/components/schemas/NewThing" } } } } } } }, + }; + const ops = parseOperations(spec); + const postOp = ops.find((o) => o.operationId === "postThing")!; + expect(postOp.hasBody).toBe(true); + expect(postOp.bodySchema?.props).toContainEqual({ name: "thingId", type: "string", required: true }); + expect(postOp.bodySchema?.props).toContainEqual({ name: "counter", type: "number", required: false }); + expect(postOp.bodySchema?.props).toContainEqual({ name: "attributes", type: "object", required: false }); + }); + + it("marks a $ref or allOf/oneOf/anyOf body property as type 'unknown' (not string)", () => { + const spec = { + components: { schemas: { + ComplexThing: { type: "object", required: ["policyId"], properties: { + policyId: { type: "string" }, + attributes: { $ref: "#/components/schemas/Attributes" }, + _policy: { allOf: [{ $ref: "#/components/schemas/Policy" }] }, + features: { oneOf: [{ type: "object" }, { type: "null" }] }, + }}, + }}, + paths: { "/things": { put: { operationId: "putComplexThing", + requestBody: { content: { "application/json": { schema: { $ref: "#/components/schemas/ComplexThing" } } } } } } }, + }; + const ops = parseOperations(spec); + const putOp = ops.find((o) => o.operationId === "putComplexThing")!; + expect(putOp.bodySchema?.props).toContainEqual({ name: "policyId", type: "string", required: true }); + expect(putOp.bodySchema?.props).toContainEqual({ name: "attributes", type: "unknown", required: false }); + expect(putOp.bodySchema?.props).toContainEqual({ name: "_policy", type: "unknown", required: false }); + expect(putOp.bodySchema?.props).toContainEqual({ name: "features", type: "unknown", required: false }); + }); +}); diff --git a/mcp/src/ditto/openapi.ts b/mcp/src/ditto/openapi.ts new file mode 100644 index 0000000000..7e2f4d8949 --- /dev/null +++ b/mcp/src/ditto/openapi.ts @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +export interface OpParam { + name: string; + in: "path" | "query"; + required: boolean; + type: "string" | "number" | "boolean"; +} + +export interface BodyProp { + name: string; + type: "string" | "number" | "boolean" | "object" | "array" | "unknown"; + required: boolean; +} + +export interface BodySchema { + props: BodyProp[]; +} + +export interface DittoOperation { + operationId: string; + method: string; + path: string; + summary: string; + description: string; + params: OpParam[]; + hasBody: boolean; + bodySchema?: BodySchema; + securitySchemes: string[]; +} + +const METHODS = ["get", "put", "post", "delete", "patch"]; + +interface RawParam { name?: string; in?: string; required?: boolean; schema?: unknown; $ref?: string } + +function paramType(schema: unknown): OpParam["type"] { + const t = (schema as { type?: string } | undefined)?.type; + return t === "number" || t === "integer" ? "number" : t === "boolean" ? "boolean" : "string"; +} + +function bodyPropType(schema: unknown): BodyProp["type"] { + const s = schema as { type?: string; $ref?: string; allOf?: unknown; oneOf?: unknown; anyOf?: unknown } | undefined; + // If the schema is a $ref or has composition keywords (allOf/oneOf/anyOf), or has no recognizable type, return "unknown" + if (s?.$ref || s?.allOf || s?.oneOf || s?.anyOf) return "unknown"; + const t = s?.type; + if (t === "integer" || t === "number") return "number"; + if (t === "boolean") return "boolean"; + if (t === "object") return "object"; + if (t === "array") return "array"; + if (t === "string") return "string"; + return "unknown"; // no recognizable primitive type +} + +function resolveSchemaRef(s: SpecShape, schema: unknown): unknown { + const ref = (schema as { $ref?: string } | undefined)?.$ref; + if (ref) return (s.components?.schemas as Record | undefined)?.[ref.split("/").pop() ?? ""]; + return schema; +} + +function bodySchemaOf(s: SpecShape, op: { requestBody?: unknown }): BodySchema | undefined { + const schemaRef = (op.requestBody as { content?: Record } | undefined) + ?.content?.["application/json"]?.schema; + const schema = resolveSchemaRef(s, schemaRef) as + | { type?: string; properties?: Record; required?: string[] } | undefined; + if (!schema || schema.type !== "object" || !schema.properties) return undefined; + const required = new Set(schema.required ?? []); + const props: BodyProp[] = Object.entries(schema.properties).map(([name, p]) => ({ + name, + type: bodyPropType(p), // pass the full property schema + required: required.has(name), + })); + return props.length ? { props } : undefined; +} + +/** Resolve a `{$ref: '#/components/parameters/Name'}` param against the spec; pass others through. */ +function resolveParam(spec: SpecShape, p: RawParam): RawParam { + if (p.$ref) { + const name = p.$ref.split("/").pop() ?? ""; + return (spec.components?.parameters?.[name] as RawParam | undefined) ?? {}; + } + return p; +} + +interface SpecShape { + paths?: Record>; + components?: { parameters?: Record; schemas?: Record }; + security?: Array>; +} + +export function parseOperations(spec: unknown): DittoOperation[] { + const s = (spec ?? {}) as SpecShape; + const paths = s.paths ?? {}; + const ops: DittoOperation[] = []; + for (const [path, item] of Object.entries(paths)) { + // Path-item-level parameters apply to every operation under this path. + const pathParams = ((item.parameters as RawParam[] | undefined) ?? []).map((p) => resolveParam(s, p)); + for (const method of METHODS) { + const op = item[method] as + | { operationId?: string; summary?: string; description?: string; parameters?: RawParam[]; requestBody?: unknown; security?: Array> } + | undefined; + if (!op) continue; + const opParams = (op.parameters ?? []).map((p) => resolveParam(s, p)); + const seen = new Set(); + const params: OpParam[] = [...opParams, ...pathParams] // op-level wins on name clash + .filter((p) => p.in === "path" || p.in === "query") + .filter((p) => (seen.has(`${p.in}:${p.name}`) ? false : seen.add(`${p.in}:${p.name}`))) + .map((p) => ({ + name: String(p.name), + in: p.in as "path" | "query", + required: p.required === true || p.in === "path", + type: paramType(p.schema), + })); + // Effective security = op.security ?? spec.security ?? []; flatten to scheme names + const effectiveSecurity = op.security ?? s.security ?? []; + const securitySchemes = Array.from( + new Set(effectiveSecurity.flatMap((req) => Object.keys(req))) + ); + const hasBody = op.requestBody !== undefined; + ops.push({ + operationId: op.operationId ?? `${method.toUpperCase()}_${path}`, + method: method.toUpperCase(), + path, + summary: op.summary ?? "", + description: op.description ?? op.summary ?? "", + params, + hasBody, + bodySchema: hasBody ? bodySchemaOf(s, op) : undefined, + securitySchemes, + }); + } + } + return ops; +} diff --git a/mcp/src/ditto/tool-policy.test.ts b/mcp/src/ditto/tool-policy.test.ts new file mode 100644 index 0000000000..171e4236cf --- /dev/null +++ b/mcp/src/ditto/tool-policy.test.ts @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { describe, it, expect } from "vitest"; +import { isSudo, isAllowed } from "./tool-policy.js"; +import type { DittoOperation } from "./openapi.js"; + +const op = (o: Partial): DittoOperation => ({ + operationId: "x", method: "GET", path: "/x", summary: "", description: "", params: [], hasBody: false, securitySchemes: [], ...o, +}); +const policy = (p: object) => ({ allowMethods: ["GET"], writeAllowlist: [], sudoAllowlist: [], ...p }); + +describe("tool-policy", () => { + it("allows GET by default, blocks writes", () => { + expect(isAllowed(op({ method: "GET" }), policy({}))).toBe(true); + expect(isAllowed(op({ operationId: "putThing", method: "PUT" }), policy({}))).toBe(false); + }); + it("allows a write only when allowlisted", () => { + expect(isAllowed(op({ operationId: "putThing", method: "PUT" }), policy({ writeAllowlist: ["putThing"] }))).toBe(true); + }); + it("allows a write via a 'METHOD path' allowlist entry (spec has no operationId)", () => { + const putThing = op({ + operationId: "PUT_/api/2/things/{thingId}", // synthesized fallback + method: "PUT", + path: "/api/2/things/{thingId}", + }); + expect(isAllowed(putThing, policy({ writeAllowlist: ["PUT /api/2/things/{thingId}"] }))).toBe(true); + expect(isAllowed(putThing, policy({ writeAllowlist: ["PUT /api/2/other"] }))).toBe(false); + }); + it("allows a sudo op via a 'METHOD path' allowlist entry", () => { + const conn = op({ + operationId: "PUT_/api/2/connections/{connectionId}", + method: "PUT", + path: "/api/2/connections/{connectionId}", + securitySchemes: ["DevOpsBasic"], + }); + expect(isAllowed(conn, policy({ sudoAllowlist: ["PUT /api/2/connections/{connectionId}"] }))).toBe(true); + }); + it("treats sudo ops specially: only via sudoAllowlist", () => { + const s = op({ operationId: "sudoRetrieveThing", method: "GET" }); + expect(isSudo(s)).toBe(true); + expect(isAllowed(s, policy({}))).toBe(false); // GET but sudo -> not auto-allowed + expect(isAllowed(s, policy({ sudoAllowlist: ["sudoRetrieveThing"] }))).toBe(true); + }); + + it("treats /devops paths as devops-privileged (sudo-gated)", () => { + const d = op({ operationId: "getLogging", method: "GET", path: "/devops/logging" }); + expect(isSudo(d)).toBe(true); + expect(isAllowed(d, policy({}))).toBe(false); // GET but /devops -> not auto-allowed + expect(isAllowed(d, policy({ sudoAllowlist: ["getLogging"] }))).toBe(true); + }); + + it("treats ops with DevOpsBasic/DevOpsBearer security as sudo", () => { + const conn = op({ + operationId: "getConnections", + method: "GET", + path: "/api/2/connections", + securitySchemes: ["DevOpsBasic"], + }); + expect(isSudo(conn)).toBe(true); + expect(isAllowed(conn, policy({}))).toBe(false); // GET but devops-secured -> not auto-allowed + expect(isAllowed(conn, policy({ sudoAllowlist: ["getConnections"] }))).toBe(true); + }); + + it("treats ops with DevOpsBearer (case-insensitive) as sudo", () => { + const op2 = op({ securitySchemes: ["DevOpsBearer"], path: "/api/2/connections/foo" }); + expect(isSudo(op2)).toBe(true); + }); + + it("treats /api/2/connections as sudo even without declared devops security (path rule)", () => { + const conn = op({ operationId: "getConnections", method: "GET", path: "/api/2/connections", securitySchemes: [] }); + expect(isSudo(conn)).toBe(true); + expect(isAllowed(conn, policy({}))).toBe(false); // GET but connections -> not auto-allowed + expect(isAllowed(conn, policy({ sudoAllowlist: ["getConnections"] }))).toBe(true); + }); +}); diff --git a/mcp/src/ditto/tool-policy.ts b/mcp/src/ditto/tool-policy.ts new file mode 100644 index 0000000000..61e1f4b908 --- /dev/null +++ b/mcp/src/ditto/tool-policy.ts @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import type { AppConfig } from "../config/schema.js"; +import type { DittoOperation } from "./openapi.js"; + +export function isSudo(op: DittoOperation): boolean { + const p = op.path.toLowerCase(); + const hasDevopsSecurity = op.securitySchemes.some((s) => s.toLowerCase().includes("devops")); + return ( + hasDevopsSecurity || + op.operationId.toLowerCase().startsWith("sudo") || + p.includes("/sudo") || + p.startsWith("/devops") || + p.includes("/connections") // connectivity is secret-bearing → always devops + ); +} + +// Allowlist entries may be either the OpenAPI operationId (e.g. "putThing") or a +// "METHOD path" key (e.g. "PUT /api/2/things/{thingId}"). The latter is the stable, +// user-friendly form when the spec omits operationIds (Ditto's does), in which case +// operationId is a synthesized "METHOD_path" fallback. +function matchesAllowlist(op: DittoOperation, list: string[]): boolean { + return list.includes(op.operationId) || list.includes(`${op.method} ${op.path}`); +} + +export function isAllowed(op: DittoOperation, policy: AppConfig["ditto"]["policy"]): boolean { + if (isSudo(op)) return matchesAllowlist(op, policy.sudoAllowlist); + return policy.allowMethods.includes(op.method) || matchesAllowlist(op, policy.writeAllowlist); +} diff --git a/mcp/src/knowledge/build-index.test.ts b/mcp/src/knowledge/build-index.test.ts new file mode 100644 index 0000000000..2cf6574d00 --- /dev/null +++ b/mcp/src/knowledge/build-index.test.ts @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { describe, it, expect, afterEach } from "vitest"; +import { buildIndex } from "./build-index.js"; +import { SqliteKnowledgeStore } from "./sqlite-knowledge-store.js"; +import type { KnowledgeSource, Chunk } from "./types.js"; +import type { EmbeddingProvider } from "./embedding.js"; + +const src = (id: string, chunks: Chunk[]): KnowledgeSource => ({ id, loadChunks: async () => chunks }); +const chunk = (id: string, text: string): Chunk => ({ id, source: "s", title: "T", text, cite: `https://x/${id}` }); +const fake: EmbeddingProvider = { dim: 3, embed: async (t) => t.map(() => [1, 0, 0]) }; + +let store: SqliteKnowledgeStore; +afterEach(async () => await store?.close()); + +describe("buildIndex", () => { + it("adds chunks (fts) with no embedder", async () => { + store = new SqliteKnowledgeStore(); + await buildIndex([src("s1", [chunk("a", "reconnect memory")])], store); + expect(await store.isPopulated()).toBe(true); + expect((await store.ftsSearch("memory", 5))[0]).toBe("a"); + expect(await store.vectorSearch([1, 0, 0], 5)).toEqual([]); // no vectors + }); + + it("adds chunks + vectors with an embedder", async () => { + store = new SqliteKnowledgeStore(); + await buildIndex([src("s1", [chunk("a", "x")])], store, fake); + expect((await store.vectorSearch([1, 0, 0], 1))[0].id).toBe("a"); + }); +}); diff --git a/mcp/src/knowledge/build-index.ts b/mcp/src/knowledge/build-index.ts new file mode 100644 index 0000000000..01b534ca08 --- /dev/null +++ b/mcp/src/knowledge/build-index.ts @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import type { KnowledgeSource } from "./types.js"; +import type { KnowledgeStore } from "./knowledge-store.js"; +import type { EmbeddingProvider } from "./embedding.js"; +import { SCHEMA_VERSION } from "./knowledge-store.js"; +import type { AppConfig } from "../config/schema.js"; + +export function metaFor( + config: AppConfig, + embedder?: EmbeddingProvider, +): { retriever: string; embeddingModel?: string; embeddingDim?: number } { + return { + retriever: config.knowledge.retriever, + embeddingModel: embedder ? config.knowledge.embedding.model : undefined, + embeddingDim: embedder ? config.knowledge.embedding.dim : undefined, + }; +} + +export async function buildIndex( + sources: KnowledgeSource[], + store: KnowledgeStore, + embedder?: EmbeddingProvider, + meta?: { retriever: string; embeddingModel?: string; embeddingDim?: number }, + signal?: AbortSignal, +): Promise { + if (embedder) await store.ensureVectorTable(embedder.dim); + for (const source of sources) { + const chunks = await source.loadChunks(signal); + await store.addChunks(chunks); + if (embedder && chunks.length > 0) { + const vectors = await embedder.embed(chunks.map((c) => c.text), signal); + await store.upsertVectors(chunks.map((c, i) => ({ id: c.id, vector: vectors[i] }))); + } + } + await store.setMeta({ + schemaVersion: SCHEMA_VERSION, + retriever: meta?.retriever ?? "fts", + embeddingModel: meta?.embeddingModel, + embeddingDim: meta?.embeddingDim, + complete: true, + }); +} diff --git a/mcp/src/knowledge/build.test.ts b/mcp/src/knowledge/build.test.ts new file mode 100644 index 0000000000..789c9a416e --- /dev/null +++ b/mcp/src/knowledge/build.test.ts @@ -0,0 +1,244 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { describe, it, expect, afterEach } from "vitest"; +import { mkdtempSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { buildKnowledgeService } from "./build.js"; +import { AppConfigSchema } from "../config/schema.js"; +import type { FetchFn } from "./public-source.js"; +import type { KnowledgeService } from "./knowledge-service.js"; +import type { EmbeddingProvider } from "./embedding.js"; + +let service: KnowledgeService | undefined; +afterEach(() => { + service = undefined; +}); + +const fakeIndex = ` +# Ditto Docs +- [Things](thing.md) +`; + +const fakeDoc = ` +# Things +A thing is a digital twin of a physical device. +`; + +const fakeFetch: FetchFn = async (url) => { + if (url.endsWith("index.md")) return fakeIndex; + if (url.endsWith("thing.md")) return fakeDoc; + throw new Error(`unexpected fetch: ${url}`); +}; + +const failFetch: FetchFn = async () => { + throw new Error("fetch failed"); +}; + +describe("buildKnowledgeService", () => { + it("returns undefined when knowledge disabled", async () => { + const config = AppConfigSchema.parse({ knowledge: { enabled: false } }); + const result = await buildKnowledgeService(config, { + fetchFn: fakeFetch, + }); + expect(result).toBeUndefined(); + }); + + it("returns undefined when no sources enabled", async () => { + const config = AppConfigSchema.parse({ + knowledge: { enabled: true, publicSource: { enabled: false } }, + }); + const result = await buildKnowledgeService(config, { + fetchFn: fakeFetch, + }); + expect(result).toBeUndefined(); + }); + + it("gracefully degrades on init failure (returns undefined, logs)", async () => { + const config = AppConfigSchema.parse({ + knowledge: { + enabled: true, + publicSource: { + enabled: true, + url: "http://example.com/index.md", + }, + }, + }); + const result = await buildKnowledgeService(config, { + fetchFn: failFetch, + }); + expect(result).toBeUndefined(); + }); + + it("builds and inits a working service with fake fetch", async () => { + const config = AppConfigSchema.parse({ + knowledge: { + enabled: true, + publicSource: { + enabled: true, + url: "http://example.com/index.md", + }, + }, + }); + service = await buildKnowledgeService(config, { fetchFn: fakeFetch }); + expect(service).toBeDefined(); + const hits = await service!.search("digital twin", 5); + expect(hits.length).toBeGreaterThan(0); + expect(hits[0].chunk.text).toContain("digital twin"); + }); +}); + +const fakeEmbedder: EmbeddingProvider = { + dim: 3, + embed: async (texts) => + texts.map((t) => { + const s = t.toLowerCase(); + if (s.includes("reconnect") || s.includes("oom") || s.includes("memory")) return [1, 0, 0]; + if (s.includes("policy") || s.includes("access")) return [0, 1, 0]; + return [0, 0, 1]; + }), +}; + +describe("buildKnowledgeService — vector retriever over a local dir", () => { + it("returns semantic hits using an injected embedder (no network/model)", async () => { + const dir = mkdtempSync(join(tmpdir(), "ditto-corpus-")); + writeFileSync(join(dir, "oom.md"), "# OOM\n\nNetty leak out of memory crash."); + writeFileSync(join(dir, "pol.md"), "# Policy\n\npolicy access control."); + + const config = AppConfigSchema.parse({ + knowledge: { + retriever: "vector", + embedding: { dim: 3 }, + publicSource: { enabled: false }, + localDir: { enabled: true, path: dir }, + }, + }); + const svc = await buildKnowledgeService(config, { embeddingProvider: fakeEmbedder }); + expect(svc).toBeDefined(); + const hits = await svc!.search("why does it die on reconnect", 1); + expect(hits[0].chunk.text.toLowerCase()).toContain("out of memory"); + }); + + it("hybrid retriever wiring end-to-end (injected embedder, no network/model)", async () => { + const dir = mkdtempSync(join(tmpdir(), "ditto-corpus-")); + writeFileSync(join(dir, "oom.md"), "# OOM\n\nNetty leak out of memory crash."); + writeFileSync(join(dir, "keyword.md"), "# Keyword\n\nsomething with unique-keyword-token."); + + const config = AppConfigSchema.parse({ + knowledge: { + retriever: "hybrid", + embedding: { dim: 3 }, + publicSource: { enabled: false }, + localDir: { enabled: true, path: dir }, + }, + }); + const svc = await buildKnowledgeService(config, { embeddingProvider: fakeEmbedder }); + expect(svc).toBeDefined(); + const hits = await svc!.search("reconnect memory issues", 2); + expect(hits.some((rc) => rc.chunk.text.toLowerCase().includes("out of memory"))).toBe(true); + }); +}); + +describe("buildKnowledgeService — prebuilt sqlite file", () => { + it("opens a populated index file without rebuilding (fts)", async () => { + const dir = mkdtempSync(join(tmpdir(), "ditto-idx-")); + const corpus = mkdtempSync(join(tmpdir(), "ditto-corpus-")); + writeFileSync(join(corpus, "a.md"), "# Reconnect\n\nNetty leak out of memory crash."); + const path = join(dir, "index.db"); + + // Pre-populate the file via the same buildIndex the CLI uses. + const { SqliteKnowledgeStore } = await import("./sqlite-knowledge-store.js"); + const { buildIndex } = await import("./build-index.js"); + const { LocalDirSource } = await import("./local-dir-source.js"); + const w = new SqliteKnowledgeStore(path); + await buildIndex([new LocalDirSource({ dir: corpus })], w); + await w.close(); + + const config = AppConfigSchema.parse({ + knowledge: { + retriever: "fts", + publicSource: { enabled: false }, + localDir: { enabled: true, path: "/nonexistent-should-not-be-read" }, + store: { kind: "sqlite", sqlite: { path } }, + }, + }); + // localDir points at a missing dir on purpose: if the server rebuilt, it would + // find nothing; since it must LOAD the prebuilt file, search still works. + const svc = await buildKnowledgeService(config); + expect(svc).toBeDefined(); + const hits = await svc!.search("out of memory", 5); + expect(hits[0].chunk.text.toLowerCase()).toContain("out of memory"); + }); + + it("rebuilds (fallback) when a prebuilt file's retriever metadata mismatches config", async () => { + const dir = mkdtempSync(join(tmpdir(), "ditto-idx-")); + const path = join(dir, "index.db"); + const { SqliteKnowledgeStore } = await import("./sqlite-knowledge-store.js"); + const { buildIndex } = await import("./build-index.js"); + const w = new SqliteKnowledgeStore(path); + await buildIndex([{ id: "s", loadChunks: async () => [ + { id: "s#0", source: "s", title: "T", text: "netty oom", cite: "x" }] }], w, undefined, + { retriever: "fts" }); + await w.close(); + // Server configured for "vector" but the file was built for "fts" -> must not serve it. + const config = AppConfigSchema.parse({ + knowledge: { retriever: "vector", embedding: { dim: 3 }, publicSource: { enabled: false }, + localDir: { enabled: false }, store: { kind: "sqlite", sqlite: { path } } }, + }); + const fake = { dim: 3, embed: async (t: string[]) => t.map(() => [1, 0, 0]) }; + const svc = await buildKnowledgeService(config, { embeddingProvider: fake }); + // localDir disabled + publicSource disabled -> fallback build has no sources -> undefined. + // The point: it did NOT serve the mismatched fts file as a vector index. + expect(svc).toBeUndefined(); + }); + + it("falls back to in-memory build when the store path is a corrupt file", async () => { + const dir = mkdtempSync(join(tmpdir(), "ditto-idx-")); + const path = join(dir, "index.db"); + writeFileSync(path, "this is not a sqlite database"); + const corpus = mkdtempSync(join(tmpdir(), "ditto-corpus-")); + writeFileSync(join(corpus, "a.md"), "# A\n\nnetty out of memory"); + const config = AppConfigSchema.parse({ + knowledge: { retriever: "fts", publicSource: { enabled: false }, + localDir: { enabled: true, path: corpus }, store: { kind: "sqlite", sqlite: { path } } }, + }); + const svc = await buildKnowledgeService(config); + expect(svc).toBeDefined(); + expect((await svc!.search("memory", 5))[0].chunk.text.toLowerCase()).toContain("out of memory"); + }); + + it("refuses to serve a prebuilt vector index with mismatched embedding dim", async () => { + const dir = mkdtempSync(join(tmpdir(), "ditto-idx-")); + const path = join(dir, "index.db"); + // Build a vector index with dim=3 + const { SqliteKnowledgeStore } = await import("./sqlite-knowledge-store.js"); + const { buildIndex } = await import("./build-index.js"); + const fake3 = { dim: 3, embed: async (t: string[]) => t.map(() => [1, 0, 0]) }; + const w = new SqliteKnowledgeStore(path); + await buildIndex([{ id: "s", loadChunks: async () => [ + { id: "s#0", source: "s", title: "T", text: "netty oom", cite: "x" }] }], w, fake3, + { retriever: "vector", embeddingModel: "fake/model", embeddingDim: 3 }); + await w.close(); + // Now try to serve it with config expecting dim=4 + const config = AppConfigSchema.parse({ + knowledge: { retriever: "vector", embedding: { model: "fake/model", dim: 4 }, + publicSource: { enabled: false }, localDir: { enabled: false }, + store: { kind: "sqlite", sqlite: { path } } }, + }); + const fake4 = { dim: 4, embed: async (t: string[]) => t.map(() => [1, 0, 0, 0]) }; + const svc = await buildKnowledgeService(config, { embeddingProvider: fake4 }); + // The mismatched file must NOT be served; fallback has no sources -> undefined. + expect(svc).toBeUndefined(); + }); +}); diff --git a/mcp/src/knowledge/build.ts b/mcp/src/knowledge/build.ts new file mode 100644 index 0000000000..96fbcdaa45 --- /dev/null +++ b/mcp/src/knowledge/build.ts @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { existsSync } from "node:fs"; +import type { AppConfig } from "../config/schema.js"; +import type { Retriever } from "./types.js"; +import type { EmbeddingProvider } from "./embedding.js"; +import type { KnowledgeStore, IndexMeta } from "./knowledge-store.js"; +import { SCHEMA_VERSION } from "./knowledge-store.js"; +import { openStore } from "./store-factory.js"; +import { KnowledgeService } from "./knowledge-service.js"; +import { FtsRetriever } from "./fts-retriever.js"; +import { VectorRetriever } from "./vector-retriever.js"; +import { HybridRetriever } from "./hybrid-retriever.js"; +import { buildIndex, metaFor } from "./build-index.js"; +import { makeSources, makeEmbedder, type FactoryDeps } from "./factories.js"; + +export type KnowledgeDeps = FactoryDeps; + +function metaMatches(meta: IndexMeta | null | undefined, config: AppConfig): boolean { + if (!meta || meta.complete !== true || meta.schemaVersion !== SCHEMA_VERSION) return false; + if (meta.retriever !== config.knowledge.retriever) return false; + if (config.knowledge.retriever === "fts") return true; + const e = config.knowledge.embedding; + return meta.embeddingModel === e.model && meta.embeddingDim === e.dim; +} + +export async function buildKnowledgeService( + config: AppConfig, + deps: KnowledgeDeps = {}, +): Promise { + if (!config.knowledge.enabled) return undefined; + + const needsVectors = config.knowledge.retriever !== "fts"; + const embedder: EmbeddingProvider | undefined = needsVectors + ? await makeEmbedder(config, deps) + : undefined; + + try { + // Non-sqlite stores (e.g. pgvector): open, check if populated + meta matches, serve; ELSE warn (no auto-build). + if (config.knowledge.store.kind !== "sqlite") { + const store = await openStore(config); + try { + if (await store.isPopulated()) { + const meta = await store.getMeta(); + if (metaMatches(meta, config)) return new KnowledgeService(store, makeRetriever(config, store, embedder)); + } + // No matching prebuilt index → warn and disable knowledge (do NOT auto-build for pgvector). + process.stderr.write( + `[ditto-mcp] no matching prebuilt index in the configured Postgres store; ` + + `run \`ingest\` to build it — the server does not build pgvector indexes\n`, + ); + await store.close(); + return undefined; + } catch (err) { + await store.close(); + process.stderr.write(`[ditto-mcp] knowledge init failed: ${String(err)}\n`); + return undefined; + } + } + + // Sqlite: prebuilt file path. + const path = config.knowledge.store.sqlite.path; + // Prebuilt file: open and serve without rebuilding. + if (path && existsSync(path)) { + try { + const store = await openStore(config, { path }); + if (await store.isPopulated()) { + const meta = await store.getMeta(); + if (metaMatches(meta, config)) return new KnowledgeService(store, makeRetriever(config, store, embedder)); + process.stderr.write( + `[ditto-mcp] prebuilt index at ${path} is incomplete or does not match config ` + + `(retriever/model/dim/version); rebuilding in memory\n`, + ); + await store.close(); + } else { + await store.close(); + } + } catch (err) { + process.stderr.write(`[ditto-mcp] cannot open prebuilt index at ${path}: ${String(err)}; rebuilding in memory\n`); + } + } + + // Fallback: build in-memory (never writes the configured file). + if (path) { + process.stderr.write( + `[ditto-mcp] no prebuilt index at ${path}; building in memory (run \`ingest\` to persist)\n`, + ); + } + const sources = makeSources(config, deps); + if (sources.length === 0) return undefined; + const store: KnowledgeStore = await openStore(config, { path: ":memory:" }); + try { + await buildIndex(sources, store, embedder, metaFor(config, embedder)); + } catch (err) { + await store.close(); + throw err; + } + return new KnowledgeService(store, makeRetriever(config, store, embedder)); + } catch (err) { + process.stderr.write( + `[ditto-mcp] knowledge init failed, disabling knowledge tools: ${String(err)}\n`, + ); + return undefined; + } +} + +function makeRetriever(config: AppConfig, store: KnowledgeStore, embedder?: EmbeddingProvider): Retriever { + const kind = config.knowledge.retriever; + if (kind === "fts") return new FtsRetriever(store); + if (!embedder) throw new Error("vector/hybrid retriever requires an embedder"); + const vector = new VectorRetriever(store, embedder); + if (kind === "vector") return vector; + return new HybridRetriever([new FtsRetriever(store), vector]); +} diff --git a/mcp/src/knowledge/chunker.test.ts b/mcp/src/knowledge/chunker.test.ts new file mode 100644 index 0000000000..c82170590a --- /dev/null +++ b/mcp/src/knowledge/chunker.test.ts @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { describe, it, expect } from "vitest"; +import { chunkMarkdown } from "./chunker.js"; + +const meta = { source: "doc", title: "Doc", cite: "https://x/doc" }; + +describe("chunkMarkdown", () => { + it("returns one chunk for short input, with stable id and metadata", () => { + const chunks = chunkMarkdown("# Title\n\nShort body.", meta); + expect(chunks).toHaveLength(1); + expect(chunks[0].id).toBe("doc#0"); + expect(chunks[0].source).toBe("doc"); + expect(chunks[0].title).toBe("Doc"); + expect(chunks[0].cite).toBe("https://x/doc"); + expect(chunks[0].text).toContain("Short body."); + }); + + it("splits long input into multiple size-bounded chunks with sequential ids", () => { + const long = "para. ".repeat(600); // ~3600 chars + const chunks = chunkMarkdown(long, { ...meta, maxChars: 1000, overlap: 100 }); + expect(chunks.length).toBeGreaterThan(1); + chunks.forEach((c, i) => expect(c.id).toBe(`doc#${i}`)); + chunks.forEach((c) => expect(c.text.length).toBeLessThanOrEqual(1000)); + }); + + it("is deterministic (same input -> identical chunks)", () => { + const a = chunkMarkdown("# H\n\n" + "word ".repeat(500), meta); + const b = chunkMarkdown("# H\n\n" + "word ".repeat(500), meta); + expect(a.length).toBeGreaterThan(1); + expect(a).toEqual(b); + }); + + it("returns no chunks for empty/whitespace input", () => { + expect(chunkMarkdown(" \n\n ", meta)).toEqual([]); + }); + + it("throws when overlap >= maxChars", () => { + expect(() => chunkMarkdown("x".repeat(50), { ...meta, maxChars: 100, overlap: 100 })).toThrow(/overlap/); + }); +}); diff --git a/mcp/src/knowledge/chunker.ts b/mcp/src/knowledge/chunker.ts new file mode 100644 index 0000000000..039c4889ae --- /dev/null +++ b/mcp/src/knowledge/chunker.ts @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import type { Chunk } from "./types.js"; + +export interface ChunkOptions { + source: string; + title: string; + cite: string; + maxChars?: number; + overlap?: number; +} + +/** + * Split markdown into size-bounded chunks. Paragraphs (blank-line separated) + * are packed into windows of at most `maxChars`; a paragraph longer than + * `maxChars` is hard-split with `overlap` characters carried between pieces. + * Deterministic: same input yields identical chunks with ids `${source}#${n}`. + */ +export function chunkMarkdown(md: string, opts: ChunkOptions): Chunk[] { + const maxChars = opts.maxChars ?? 1000; + const overlap = opts.overlap ?? 150; + if (overlap < 0) throw new Error(`overlap must be >= 0 (got ${overlap})`); + if (overlap >= maxChars) { + throw new Error(`overlap (${overlap}) must be less than maxChars (${maxChars})`); + } + const paragraphs = md + .split(/\n\s*\n/) + .map((p) => p.trim()) + .filter((p) => p.length > 0); + + const pieces: string[] = []; + let buf = ""; + const flush = () => { + if (buf.trim().length > 0) pieces.push(buf.trim()); + buf = ""; + }; + + for (const para of paragraphs) { + if (para.length > maxChars) { + flush(); + let start = 0; + while (start < para.length) { + const end = Math.min(start + maxChars, para.length); + pieces.push(para.slice(start, end).trim()); + if (end >= para.length) break; + start = end - overlap; + if (start < 0) start = 0; + } + continue; + } + if (buf.length + para.length + 2 > maxChars) flush(); + buf = buf.length === 0 ? para : `${buf}\n\n${para}`; + } + flush(); + + return pieces.map((text, n) => ({ + id: `${opts.source}#${n}`, + source: opts.source, + title: opts.title, + text, + cite: opts.cite, + })); +} diff --git a/mcp/src/knowledge/embedding.itest.ts b/mcp/src/knowledge/embedding.itest.ts new file mode 100644 index 0000000000..1cb08905d9 --- /dev/null +++ b/mcp/src/knowledge/embedding.itest.ts @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { describe, it, expect } from "vitest"; +import { LocalEmbeddings } from "./embedding.js"; + +// Loads the real bge model (network on first run). Skipped unless RUN_EMBED_ITEST is set. +const run = process.env.RUN_EMBED_ITEST ? describe : describe.skip; + +run("LocalEmbeddings (real model)", () => { + it("produces 384-dim vectors and ranks paraphrase above unrelated", async () => { + const emb = new LocalEmbeddings(); + const [a, b, c] = await emb.embed([ + "why does it die on reconnect", + "the Netty leak causes an out of memory crash on reconnect", + "policies define access control for things", + ]); + expect(a).toHaveLength(384); + const cos = (x: number[], y: number[]) => x.reduce((s, xi, i) => s + xi * y[i], 0); + expect(cos(a, b)).toBeGreaterThan(cos(a, c)); + }, 120000); +}); diff --git a/mcp/src/knowledge/embedding.test.ts b/mcp/src/knowledge/embedding.test.ts new file mode 100644 index 0000000000..bcd5d0da70 --- /dev/null +++ b/mcp/src/knowledge/embedding.test.ts @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { describe, it, expect } from "vitest"; +import { toBatches } from "./embedding.js"; + +describe("toBatches", () => { + it("splits into fixed-size batches with a remainder", () => { + expect(toBatches([1, 2, 3, 4, 5], 2)).toEqual([[1, 2], [3, 4], [5]]); + }); + + it("returns a single batch when size >= length", () => { + expect(toBatches([1, 2], 5)).toEqual([[1, 2]]); + }); + + it("returns [] for empty input", () => { + expect(toBatches([], 3)).toEqual([]); + }); + + it("throws on a non-positive batch size", () => { + expect(() => toBatches([1], 0)).toThrow(/batch size/i); + }); +}); diff --git a/mcp/src/knowledge/embedding.ts b/mcp/src/knowledge/embedding.ts new file mode 100644 index 0000000000..f103c23101 --- /dev/null +++ b/mcp/src/knowledge/embedding.ts @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { pipeline, env, type FeatureExtractionPipeline } from "@huggingface/transformers"; + +export interface EmbeddingProvider { + readonly dim: number; + embed(texts: string[], signal?: AbortSignal): Promise; +} + +export interface LocalEmbeddingsOptions { + model?: string; + dim?: number; + modelPath?: string; + allowRemoteModels?: boolean; + cacheDir?: string; + /** Texts embedded per forward pass. Bounds peak memory; default 32. */ + batchSize?: number; +} + +const DEFAULT_MODEL = "Xenova/bge-small-en-v1.5"; +const DEFAULT_DIM = 384; +const DEFAULT_BATCH_SIZE = 32; + +/** Split `items` into consecutive batches of at most `size`. */ +export function toBatches(items: T[], size: number): T[][] { + if (size <= 0) throw new Error(`batch size must be positive (got ${size})`); + const out: T[][] = []; + for (let i = 0; i < items.length; i += size) { + out.push(items.slice(i, i + size)); + } + return out; +} + +/** + * Local ONNX embeddings via Transformers.js. The model weights are downloaded + * from the HuggingFace hub on first use and cached to disk; set + * `allowRemoteModels: false` + `modelPath` for offline/gated deployments. + */ +export class LocalEmbeddings implements EmbeddingProvider { + readonly dim: number; + private readonly model: string; + private readonly batchSize: number; + private extractorPromise?: Promise; + + constructor(opts: LocalEmbeddingsOptions = {}) { + this.model = opts.model ?? DEFAULT_MODEL; + this.dim = opts.dim ?? DEFAULT_DIM; + this.batchSize = opts.batchSize ?? DEFAULT_BATCH_SIZE; + if (opts.allowRemoteModels !== undefined) env.allowRemoteModels = opts.allowRemoteModels; + if (opts.modelPath !== undefined) env.localModelPath = opts.modelPath; + if (opts.cacheDir !== undefined) env.cacheDir = opts.cacheDir; + } + + private extractor(): Promise { + if (!this.extractorPromise) { + // Type assertion needed due to complex union type from pipeline generic + this.extractorPromise = pipeline("feature-extraction", this.model) as unknown as Promise; + } + return this.extractorPromise; + } + + async embed(texts: string[]): Promise { + if (texts.length === 0) return []; + const extractor = await this.extractor(); + // Embed in bounded batches: a single forward pass over the whole corpus + // allocates a tensor for every input at once and can exhaust memory. + const out: number[][] = []; + for (const batch of toBatches(texts, this.batchSize)) { + const output = await extractor(batch, { pooling: "mean", normalize: true }); + out.push(...(output.tolist() as number[][])); + } + return out; + } +} diff --git a/mcp/src/knowledge/factories.test.ts b/mcp/src/knowledge/factories.test.ts new file mode 100644 index 0000000000..e9487a9426 --- /dev/null +++ b/mcp/src/knowledge/factories.test.ts @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { describe, it, expect } from "vitest"; +import { makeSources } from "./factories.js"; +import { AppConfigSchema } from "../config/schema.js"; + +const LONG_DOC = [ + "Alpha paragraph about token integration here.", + "Beta paragraph about policy subjects here now.", + "Gamma paragraph about activation actions here.", + "Delta paragraph about JWT permission grants.", +].join("\n\n"); + +const INDEX = `# Ditto docs\n- [Doc](https://eclipse.dev/ditto/doc.md): a doc\n`; + +const DOCS: Record = { + "https://eclipse.dev/ditto/llms.txt": INDEX, + "https://eclipse.dev/ditto/doc.md": LONG_DOC, +}; + +const fakeFetch = async (url: string): Promise => { + if (!(url in DOCS)) throw new Error(`404 ${url}`); + return DOCS[url]; +}; + +describe("knowledge.chunk config", () => { + it("defaults to maxChars 1000 / overlap 150", () => { + const cfg = AppConfigSchema.parse({}); + expect(cfg.knowledge.chunk).toEqual({ maxChars: 1000, overlap: 150 }); + }); + + it("rejects overlap >= maxChars", () => { + expect(() => + AppConfigSchema.parse({ knowledge: { chunk: { maxChars: 100, overlap: 100 } } }), + ).toThrow(); + }); + + it("flows configured chunk size through makeSources to PublicSource", async () => { + const cfg = AppConfigSchema.parse({ + knowledge: { chunk: { maxChars: 60, overlap: 10 } }, + }); + const sources = makeSources(cfg, { fetchFn: fakeFetch }); + const pub = sources.find((s) => s.id === "public"); + expect(pub).toBeDefined(); + const chunks = await pub!.loadChunks(); + // With maxChars=60 the 4 paragraphs cannot pack into one chunk. + expect(chunks.length).toBeGreaterThan(1); + expect(Math.max(...chunks.map((c) => c.text.length))).toBeLessThanOrEqual(60); + }); +}); diff --git a/mcp/src/knowledge/factories.ts b/mcp/src/knowledge/factories.ts new file mode 100644 index 0000000000..65574669de --- /dev/null +++ b/mcp/src/knowledge/factories.ts @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import type { AppConfig } from "../config/schema.js"; +import type { KnowledgeSource } from "./types.js"; +import type { EmbeddingProvider } from "./embedding.js"; +import { PublicSource, type FetchFn } from "./public-source.js"; +import { LocalDirSource } from "./local-dir-source.js"; + +export interface FactoryDeps { + fetchFn?: FetchFn; + embeddingProvider?: EmbeddingProvider; +} + +export function makeSources(config: AppConfig, deps?: FactoryDeps): KnowledgeSource[] { + const sources: KnowledgeSource[] = []; + const chunkOptions = config.knowledge.chunk; + if (config.knowledge.publicSource.enabled) { + sources.push( + new PublicSource({ + url: config.knowledge.publicSource.url, + maxDocs: config.knowledge.publicSource.maxDocs, + fetchFn: deps?.fetchFn, + chunkOptions, + }), + ); + } + if (config.knowledge.localDir.enabled && config.knowledge.localDir.path) { + sources.push( + new LocalDirSource({ + dir: config.knowledge.localDir.path, + id: config.knowledge.localDir.id, + chunkOptions, + }), + ); + } + return sources; +} + +export async function makeEmbedder(config: AppConfig, deps?: FactoryDeps): Promise { + if (deps?.embeddingProvider) return deps.embeddingProvider; + // Lazy: local embeddings pull @huggingface/transformers (onnxruntime-node). + // Only needed for retriever=vector|hybrid without an injected provider. + let LocalEmbeddings: typeof import("./embedding.js").LocalEmbeddings; + try { + ({ LocalEmbeddings } = await import("./embedding.js")); + } catch (err) { + const reason = err instanceof Error ? err.message : String(err); + throw new Error( + `local embeddings require @huggingface/transformers, which failed to load: ${reason}. ` + + `Install it, inject deps.embeddingProvider, or use retriever=fts (no embeddings).`, + ); + } + const e = config.knowledge.embedding; + return new LocalEmbeddings({ + model: e.model, dim: e.dim, modelPath: e.modelPath, + allowRemoteModels: e.allowRemoteModels, cacheDir: e.cacheDir, batchSize: e.batchSize, + }); +} diff --git a/mcp/src/knowledge/fts-retriever.test.ts b/mcp/src/knowledge/fts-retriever.test.ts new file mode 100644 index 0000000000..6d11664f8a --- /dev/null +++ b/mcp/src/knowledge/fts-retriever.test.ts @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { describe, it, expect, afterEach } from "vitest"; +import { FtsRetriever } from "./fts-retriever.js"; +import { SqliteKnowledgeStore } from "./sqlite-knowledge-store.js"; +import type { Chunk } from "./types.js"; + +const chunk = (id: string, text: string): Chunk => ({ + id, source: "s", title: "T", text, cite: `https://x/${id}`, +}); + +let store: SqliteKnowledgeStore; +afterEach(async () => await store?.close()); + +describe("FtsRetriever", () => { + async function retriever(chunks: Chunk[]) { + store = new SqliteKnowledgeStore(); + await store.addChunks(chunks); + return new FtsRetriever(store); + } + + it("returns keyword hits tagged matchedBy=['fts'], best first", async () => { + const r = await retriever([ + chunk("a", "Netty leak out of memory crash"), + chunk("b", "policies access control"), + ]); + const hits = await r.search("memory crash", 5); + expect(hits[0].chunk.id).toBe("a"); + expect(hits[0].matchedBy).toEqual(["fts"]); + }); + + it("honors k and returns [] on no match", async () => { + const r = await retriever([chunk("a", "alpha"), chunk("b", "alpha")]); + expect(await r.search("alpha", 1)).toHaveLength(1); + expect(await r.search("zzznope", 5)).toEqual([]); + }); +}); diff --git a/mcp/src/knowledge/fts-retriever.ts b/mcp/src/knowledge/fts-retriever.ts new file mode 100644 index 0000000000..72fc030e27 --- /dev/null +++ b/mcp/src/knowledge/fts-retriever.ts @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import type { RetrievedChunk, Retriever } from "./types.js"; +import type { KnowledgeStore } from "./knowledge-store.js"; + +export class FtsRetriever implements Retriever { + readonly kind = "fts"; + constructor(private readonly store: KnowledgeStore) {} + + async search(query: string, k: number): Promise { + const ids = await this.store.ftsSearch(query, k); + const out: RetrievedChunk[] = []; + for (const id of ids) { + const chunk = await this.store.getChunk(id); + if (chunk) out.push({ chunk, matchedBy: ["fts"] }); + } + return out; + } +} diff --git a/mcp/src/knowledge/hybrid-retriever.test.ts b/mcp/src/knowledge/hybrid-retriever.test.ts new file mode 100644 index 0000000000..10a5c1f02b --- /dev/null +++ b/mcp/src/knowledge/hybrid-retriever.test.ts @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { describe, it, expect } from "vitest"; +import { HybridRetriever } from "./hybrid-retriever.js"; +import type { Retriever, Chunk, RetrievedChunk } from "./types.js"; + +const chunk = (id: string): Chunk => ({ + id, source: "s", title: id, text: id, cite: `https://x/${id}`, +}); + +// Fake retrievers returning fixed ranked lists. +function fixed(kind: string, ids: string[]): Retriever { + return { + kind, + search: async (_q, k): Promise => ids.slice(0, k).map((id) => ({ + chunk: chunk(id), + matchedBy: [kind], + })), + }; +} + +describe("HybridRetriever", () => { + it("has kind 'hybrid'", () => { + const h = new HybridRetriever([fixed("fts", ["a"]), fixed("vector", ["b"])]); + expect(h.kind).toBe("hybrid"); + }); + + it("fuses two ranked lists via RRF and dedupes", async () => { + // 'b' appears in both lists -> should rank at/near the top after fusion. + const a = fixed("fts", ["a", "b", "c"]); + const d = fixed("vector", ["b", "d", "e"]); + const h = new HybridRetriever([a, d]); + const hits = await h.search("q", 3); + const ids = hits.map((rc) => rc.chunk.id); + expect(ids[0]).toBe("b"); + expect(new Set(ids).size).toBe(ids.length); // no duplicates + expect(hits).toHaveLength(3); + }); + + it("merges matchedBy from both retrievers for duplicates", async () => { + // 'b' appears in both lists -> matchedBy should be ["fts","vector"] + const a = fixed("fts", ["a", "b", "c"]); + const d = fixed("vector", ["b", "d", "e"]); + const h = new HybridRetriever([a, d]); + const hits = await h.search("q", 5); + const b = hits.find((rc) => rc.chunk.id === "b"); + expect(b).toBeDefined(); + expect(b!.matchedBy).toEqual(["fts", "vector"]); + // Single-source chunks should have single matchedBy + const a_hit = hits.find((rc) => rc.chunk.id === "a"); + expect(a_hit!.matchedBy).toEqual(["fts"]); + const d_hit = hits.find((rc) => rc.chunk.id === "d"); + expect(d_hit!.matchedBy).toEqual(["vector"]); + }); + +}); diff --git a/mcp/src/knowledge/hybrid-retriever.ts b/mcp/src/knowledge/hybrid-retriever.ts new file mode 100644 index 0000000000..ecdad1b9bf --- /dev/null +++ b/mcp/src/knowledge/hybrid-retriever.ts @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import type { Chunk, Retriever, RetrievedChunk } from "./types.js"; + +const RRF_K = 60; + +export class HybridRetriever implements Retriever { + readonly kind = "hybrid"; + + constructor(private readonly retrievers: Retriever[]) {} + + async search(query: string, k: number): Promise { + const lists = await Promise.all( + this.retrievers.map((r) => r.search(query, k)), + ); + const scores = new Map(); + const byId = new Map(); + const matchedBy = new Map>(); + for (const list of lists) { + list.forEach((rc, rank) => { + byId.set(rc.chunk.id, rc.chunk); + scores.set(rc.chunk.id, (scores.get(rc.chunk.id) ?? 0) + 1 / (RRF_K + rank + 1)); + if (!matchedBy.has(rc.chunk.id)) { + matchedBy.set(rc.chunk.id, new Set()); + } + for (const kind of rc.matchedBy) { + matchedBy.get(rc.chunk.id)!.add(kind); + } + }); + } + return [...scores.entries()] + .sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1)) + .slice(0, k) + .map(([id]) => ({ + chunk: byId.get(id)!, + matchedBy: Array.from(matchedBy.get(id)!).sort(), + })); + } +} diff --git a/mcp/src/knowledge/ingest-store.test.ts b/mcp/src/knowledge/ingest-store.test.ts new file mode 100644 index 0000000000..22a2af307b --- /dev/null +++ b/mcp/src/knowledge/ingest-store.test.ts @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { describe, it, expect } from "vitest"; +import { mkdtempSync, existsSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { AppConfigSchema } from "../config/schema.js"; +import { withIngestStore } from "./ingest-store.js"; +import { SqliteKnowledgeStore } from "./sqlite-knowledge-store.js"; + +describe("withIngestStore (sqlite)", () => { + it("builds into a temp file then renames atomically", async () => { + const dir = mkdtempSync(join(tmpdir(), "ditto-idx-")); + const path = join(dir, "index.db"); + const config = AppConfigSchema.parse({ knowledge: { store: { kind: "sqlite", sqlite: { path } } } }); + await withIngestStore(config, async (store) => { + await store.addChunks([{ id: "a", source: "s", title: "T", text: "netty", cite: "x" }]); + await store.setMeta({ schemaVersion: 1, retriever: "fts", complete: true }); + }); + expect(existsSync(path)).toBe(true); + expect(existsSync(`${path}.tmp`)).toBe(false); + const s = new SqliteKnowledgeStore(path); + expect(await s.isPopulated()).toBe(true); + await s.close(); + }); + + it("removes tmp file on error, does not rename, and propagates error", async () => { + const dir = mkdtempSync(join(tmpdir(), "ditto-idx-")); + const path = join(dir, "index.db"); + const config = AppConfigSchema.parse({ knowledge: { store: { kind: "sqlite", sqlite: { path } } } }); + await expect( + withIngestStore(config, async () => { + throw new Error("boom"); + }), + ).rejects.toThrow("boom"); + expect(existsSync(path)).toBe(false); + expect(existsSync(`${path}.tmp`)).toBe(false); + }); +}); diff --git a/mcp/src/knowledge/ingest-store.ts b/mcp/src/knowledge/ingest-store.ts new file mode 100644 index 0000000000..a096797060 --- /dev/null +++ b/mcp/src/knowledge/ingest-store.ts @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { existsSync, renameSync, rmSync } from "node:fs"; +import type { AppConfig } from "../config/schema.js"; +import type { KnowledgeStore } from "./knowledge-store.js"; +import { openStore } from "./store-factory.js"; + +export async function withIngestStore( + config: AppConfig, + fn: (store: KnowledgeStore) => Promise, +): Promise { + const kind = config.knowledge.store.kind; + if (kind === "sqlite") { + const path = config.knowledge.store.sqlite.path; + if (!path) throw new Error("knowledge.store.sqlite.path is required for ingest"); + const tmp = `${path}.tmp`; + if (existsSync(tmp)) rmSync(tmp, { force: true }); + const store = await openStore(config, { path: tmp }); + try { + await fn(store); + await store.close(); + } catch (err) { + await store.close(); + rmSync(tmp, { force: true }); + throw err; + } + renameSync(tmp, path); + return; + } + if (kind === "pgvector") { + const store = await openStore(config); + try { + await store.reset(); + await fn(store); + } finally { + await store.close(); + } + return; + } + throw new Error(`unsupported store kind for ingest: ${kind}`); +} diff --git a/mcp/src/knowledge/knowledge-service.test.ts b/mcp/src/knowledge/knowledge-service.test.ts new file mode 100644 index 0000000000..403ab820cf --- /dev/null +++ b/mcp/src/knowledge/knowledge-service.test.ts @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { describe, it, expect, afterEach } from "vitest"; +import { KnowledgeService } from "./knowledge-service.js"; +import { SqliteKnowledgeStore } from "./sqlite-knowledge-store.js"; +import { FtsRetriever } from "./fts-retriever.js"; +import { buildIndex } from "./build-index.js"; +import type { KnowledgeSource, Chunk, RetrievedChunk, Retriever } from "./types.js"; + +const src = (id: string, chunks: Chunk[]): KnowledgeSource => ({ id, loadChunks: async () => chunks }); +const chunk = (id: string, text: string): Chunk => ({ id, source: "s", title: "T", text, cite: `https://x/${id}` }); + +// A chunk positioned in a document: id `pub#`, grouped by `cite`. +const c = (n: number, cite: string, text = `text ${n}`): Chunk => ({ + id: `pub#${n}`, + source: "pub", + title: `T${n}`, + text, + cite, +}); + +const stubRetriever = (anchors: RetrievedChunk[]): Retriever => ({ + kind: "stub", + search: async () => anchors, +}); + +let store: SqliteKnowledgeStore; +afterEach(async () => await store?.close()); + +describe("KnowledgeService", () => { + it("searches via the retriever and gets chunks via the store", async () => { + store = new SqliteKnowledgeStore(); + await buildIndex([src("s1", [chunk("a", "reconnect memory crash")])], store); + const svc = new KnowledgeService(store, new FtsRetriever(store)); + const hits = await svc.search("memory", 5); + expect(hits[0].chunk.id).toBe("a"); + expect((await svc.getChunk("a"))?.text).toBe("reconnect memory crash"); + }); +}); + +describe("KnowledgeService neighbor expansion", () => { + it("context=0 returns only anchors", async () => { + store = new SqliteKnowledgeStore(); + await store.addChunks([c(0, "A"), c(1, "A"), c(2, "A")]); + const svc = new KnowledgeService(store, stubRetriever([{ chunk: c(1, "A"), matchedBy: ["fts"] }])); + const hits = await svc.search("q", 5, { context: 0 }); + expect(hits.map((h) => h.chunk.id)).toEqual(["pub#1"]); + }); + + it("expands to same-cite neighbors in ordinal order, tagged by role", async () => { + store = new SqliteKnowledgeStore(); + await store.addChunks([c(0, "A"), c(1, "A"), c(2, "A")]); + const svc = new KnowledgeService(store, stubRetriever([{ chunk: c(1, "A"), matchedBy: ["vector"] }])); + const hits = await svc.search("q", 5, { context: 1 }); + expect(hits.map((h) => h.chunk.id)).toEqual(["pub#0", "pub#1", "pub#2"]); + expect(hits.map((h) => h.role)).toEqual(["context", "anchor", "context"]); + expect(hits.find((h) => h.chunk.id === "pub#1")!.matchedBy).toEqual(["vector"]); + }); + + it("does not cross document boundaries (different cite)", async () => { + store = new SqliteKnowledgeStore(); + await store.addChunks([c(0, "A"), c(1, "A"), c(2, "A"), c(3, "B"), c(4, "B")]); + // anchor pub#3 = first chunk of doc B; pub#2 is numerically adjacent but belongs to doc A. + const svc = new KnowledgeService(store, stubRetriever([{ chunk: c(3, "B"), matchedBy: ["fts"] }])); + const hits = await svc.search("q", 5, { context: 1 }); + expect(hits.map((h) => h.chunk.id)).toEqual(["pub#3", "pub#4"]); + }); + + it("dedups and merges overlapping anchor windows into one contiguous span", async () => { + store = new SqliteKnowledgeStore(); + await store.addChunks([c(0, "A"), c(1, "A"), c(2, "A"), c(3, "A")]); + const svc = new KnowledgeService( + store, + stubRetriever([ + { chunk: c(1, "A"), matchedBy: ["fts"] }, + { chunk: c(2, "A"), matchedBy: ["vector"] }, + ]), + ); + const hits = await svc.search("q", 5, { context: 1 }); + expect(hits.map((h) => h.chunk.id)).toEqual(["pub#0", "pub#1", "pub#2", "pub#3"]); + expect(hits.map((h) => h.role)).toEqual(["context", "anchor", "anchor", "context"]); + expect(new Set(hits.map((h) => h.chunk.id)).size).toBe(hits.length); // no dupes + }); +}); diff --git a/mcp/src/knowledge/knowledge-service.ts b/mcp/src/knowledge/knowledge-service.ts new file mode 100644 index 0000000000..b2f085aa13 --- /dev/null +++ b/mcp/src/knowledge/knowledge-service.ts @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import type { Chunk, RetrievedChunk, Retriever } from "./types.js"; +import type { KnowledgeStore } from "./knowledge-store.js"; + +export interface SearchOptions { + /** Positional neighbors to pull in around each anchor (same document). 0 = anchors only. */ + context?: number; +} + +/** Parse a chunk id of the form `${source}#${ordinal}`. Returns null if the id + * has no numeric ordinal suffix (such chunks can't be expanded positionally). */ +function parseId(id: string): { source: string; ord: number } | null { + const h = id.lastIndexOf("#"); + if (h < 0) return null; + const ord = Number(id.slice(h + 1)); + if (!Number.isInteger(ord)) return null; + return { source: id.slice(0, h), ord }; +} + +interface Rec { + chunk: Chunk; + matchedBy: string[]; + isAnchor: boolean; + rank: number; // best (lowest) anchor rank this chunk belongs to; drives ordering + ord: number | null; +} + +export class KnowledgeService { + constructor( + private readonly store: KnowledgeStore, + private readonly retriever: Retriever, + ) {} + + async search(query: string, k: number, opts: SearchOptions = {}): Promise { + const anchors = await this.retriever.search(query, k); + const context = opts.context ?? 0; + if (context <= 0 || anchors.length === 0) { + return anchors.map((a) => ({ ...a, role: "anchor" as const })); + } + return this.expand(anchors, context); + } + + getChunk(id: string): Promise { + return this.store.getChunk(id); + } + + /** Retriever-agnostic neighbor expansion: fetch ±context same-document chunks + * around each anchor, dedup, and emit contiguous spans ordered by best anchor + * rank (ordinal order within a span). Neighbors are fetched by id, never + * re-scored; "same document" = identical `cite`, which also stops expansion at + * document boundaries even when ordinals are globally contiguous. */ + private async expand(anchors: RetrievedChunk[], context: number): Promise { + const recs = new Map(); + + anchors.forEach((a, rank) => { + recs.set(a.chunk.id, { + chunk: a.chunk, + matchedBy: a.matchedBy, + isAnchor: true, + rank, + ord: parseId(a.chunk.id)?.ord ?? null, + }); + }); + + for (let rank = 0; rank < anchors.length; rank++) { + const anchor = anchors[rank]; + const p = parseId(anchor.chunk.id); + if (!p) continue; + for (const dir of [-1, 1]) { + for (let j = 1; j <= context; j++) { + const nid = `${p.source}#${p.ord + dir * j}`; + const existing = recs.get(nid); + if (existing) { + if (existing.chunk.cite !== anchor.chunk.cite) break; // boundary + if (rank < existing.rank) existing.rank = rank; + continue; + } + const c = await this.store.getChunk(nid); + if (!c || c.cite !== anchor.chunk.cite) break; // boundary or missing + recs.set(nid, { chunk: c, matchedBy: [], isAnchor: false, rank, ord: p.ord + dir * j }); + } + } + } + + return this.orderSegments([...recs.values()]); + } + + /** Group recs by document (cite), split into contiguous ordinal runs, order + * runs by their best anchor rank, and flatten (ordinal order within a run). */ + private orderSegments(recs: Rec[]): RetrievedChunk[] { + const byCite = new Map(); + const loose: Rec[] = []; // chunks without an ordinal — emitted as singletons + for (const r of recs) { + if (r.ord === null) { + loose.push(r); + continue; + } + const arr = byCite.get(r.chunk.cite) ?? []; + arr.push(r); + byCite.set(r.chunk.cite, arr); + } + + const segments: { rank: number; recs: Rec[] }[] = []; + for (const arr of byCite.values()) { + arr.sort((x, y) => (x.ord as number) - (y.ord as number)); + let seg: Rec[] = []; + let prev: number | undefined; + for (const r of arr) { + if (prev !== undefined && (r.ord as number) !== prev + 1) { + segments.push({ rank: Math.min(...seg.map((s) => s.rank)), recs: seg }); + seg = []; + } + seg.push(r); + prev = r.ord as number; + } + if (seg.length) segments.push({ rank: Math.min(...seg.map((s) => s.rank)), recs: seg }); + } + for (const r of loose) segments.push({ rank: r.rank, recs: [r] }); + + segments.sort((a, b) => a.rank - b.rank); + + const out: RetrievedChunk[] = []; + for (const s of segments) { + for (const r of s.recs) { + out.push({ + chunk: r.chunk, + matchedBy: r.matchedBy, + role: r.isAnchor ? "anchor" : "context", + }); + } + } + return out; + } +} diff --git a/mcp/src/knowledge/knowledge-store.ts b/mcp/src/knowledge/knowledge-store.ts new file mode 100644 index 0000000000..48cfa9dcce --- /dev/null +++ b/mcp/src/knowledge/knowledge-store.ts @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import type { Chunk } from "./types.js"; + +export const SCHEMA_VERSION = 1; + +export interface IndexMeta { + schemaVersion: number; + retriever: string; + embeddingModel?: string; + embeddingDim?: number; + complete: boolean; +} + +/** Holds chunks, a keyword (FTS) index, and vectors. Backend-agnostic + * (SqliteKnowledgeStore now; PgKnowledgeStore in P2c). */ +export interface KnowledgeStore { + addChunks(chunks: Chunk[]): Promise; + getChunk(id: string): Promise; + ftsSearch(query: string, k: number): Promise; + ensureVectorTable(dim: number): Promise; + upsertVectors(items: { id: string; vector: number[] }[]): Promise; + vectorSearch(vector: number[], k: number): Promise<{ id: string; distance: number }[]>; + isPopulated(): Promise; + hasVectors(): Promise; + setMeta(meta: IndexMeta): Promise; + getMeta(): Promise; + reset(): Promise; + close(): Promise; +} diff --git a/mcp/src/knowledge/local-dir-source.test.ts b/mcp/src/knowledge/local-dir-source.test.ts new file mode 100644 index 0000000000..ec00f1fb95 --- /dev/null +++ b/mcp/src/knowledge/local-dir-source.test.ts @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { describe, it, expect, beforeEach } from "vitest"; +import { mkdtempSync, writeFileSync, mkdirSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { LocalDirSource } from "./local-dir-source.js"; + +let dir: string; +beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), "ditto-corpus-")); + writeFileSync(join(dir, "runbook.md"), "# Runbook\n\nRestart connectivity to clear the reconnect storm."); + mkdirSync(join(dir, "sub")); + writeFileSync(join(dir, "sub", "tuning.markdown"), "# Tuning\n\nDisable Netty leak detection."); + writeFileSync(join(dir, "ignore.txt"), "not markdown"); +}); + +describe("LocalDirSource", () => { + it("loads and chunks markdown files recursively, ignoring non-markdown", async () => { + const src = new LocalDirSource({ dir }); + const chunks = await src.loadChunks(); + expect(src.id).toBe("local"); + const texts = chunks.map((c) => c.text).join("\n"); + expect(texts).toContain("reconnect storm"); + expect(texts).toContain("Netty leak detection"); + expect(texts).not.toContain("not markdown"); + expect(chunks.every((c) => c.source === "local")).toBe(true); + chunks.forEach((c, i) => expect(c.id).toBe(`local#${i}`)); + }); + + it("returns [] for a missing directory (non-fatal)", async () => { + const src = new LocalDirSource({ dir: join(dir, "does-not-exist") }); + expect(await src.loadChunks()).toEqual([]); + }); +}); diff --git a/mcp/src/knowledge/local-dir-source.ts b/mcp/src/knowledge/local-dir-source.ts new file mode 100644 index 0000000000..af6b727685 --- /dev/null +++ b/mcp/src/knowledge/local-dir-source.ts @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { readdirSync, readFileSync } from "node:fs"; +import { join, basename, extname } from "node:path"; +import type { Chunk, KnowledgeSource } from "./types.js"; +import { chunkMarkdown } from "./chunker.js"; + +export interface LocalDirSourceOptions { + dir: string; + id?: string; + chunkOptions?: { maxChars?: number; overlap?: number }; +} + +const MD_EXT = new Set([".md", ".markdown"]); + +export class LocalDirSource implements KnowledgeSource { + readonly id: string; + private readonly opts: LocalDirSourceOptions; + + constructor(opts: LocalDirSourceOptions) { + this.opts = opts; + this.id = opts.id ?? "local"; + } + + async loadChunks(): Promise { + let files: string[]; + try { + files = walk(this.opts.dir); + } catch (err) { + process.stderr.write( + `[ditto-mcp] local-dir-source: cannot read ${this.opts.dir}: ${String(err)}\n`, + ); + return []; + } + const chunks: Chunk[] = []; + for (const file of files) { + const md = readFileSync(file, "utf8"); + chunks.push( + ...chunkMarkdown(md, { + source: this.id, + title: basename(file), + cite: file, + maxChars: this.opts.chunkOptions?.maxChars, + overlap: this.opts.chunkOptions?.overlap, + }), + ); + } + return chunks.map((c, i) => ({ ...c, id: `${this.id}#${i}` })); + } +} + +function walk(dir: string): string[] { + const out: string[] = []; + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const full = join(dir, entry.name); + if (entry.isDirectory()) out.push(...walk(full)); + else if (MD_EXT.has(extname(entry.name).toLowerCase())) out.push(full); + } + return out.sort(); // deterministic order +} diff --git a/mcp/src/knowledge/pg-e2e.pgtest.ts b/mcp/src/knowledge/pg-e2e.pgtest.ts new file mode 100644 index 0000000000..d7868640e4 --- /dev/null +++ b/mcp/src/knowledge/pg-e2e.pgtest.ts @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import { startPgVector } from "./pg-testcontainer.js"; +import { AppConfigSchema } from "../config/schema.js"; +import { withIngestStore } from "./ingest-store.js"; +import { buildIndex, metaFor } from "./build-index.js"; +import { buildKnowledgeService } from "./build.js"; +import type { KnowledgeSource, Chunk } from "./types.js"; +import type { EmbeddingProvider } from "./embedding.js"; + +const fake: EmbeddingProvider = { + dim: 3, + embed: async (texts) => texts.map((t) => + /reconnect|oom|memory/i.test(t) ? [1, 0, 0] : /policy|access/i.test(t) ? [0, 1, 0] : [0, 0, 1]), +}; +const chunk = (id: string, text: string): Chunk => ({ id, source: "local", title: "T", text, cite: id }); +const src: KnowledgeSource = { id: "local", loadChunks: async () => [ + chunk("local#0", "Netty leak out of memory crash"), chunk("local#1", "policy access control")] }; + +let pg: Awaited>; +beforeAll(async () => { pg = await startPgVector(); }, 120000); +afterAll(async () => { await pg?.stop(); }); + +function cfg() { + return AppConfigSchema.parse({ + knowledge: { + retriever: "hybrid", embedding: { dim: 3 }, + publicSource: { enabled: false }, localDir: { enabled: false }, + store: { kind: "pgvector", pgvector: { connectionString: pg.connectionString, table: "e2e" } }, + }, + }); +} + +describe("pgvector end-to-end", () => { + it("ingest populates pg; server serves hybrid search from it", async () => { + await withIngestStore(cfg(), (store) => buildIndex([src], store, fake, metaFor(cfg(), fake))); + const svc = await buildKnowledgeService(cfg(), { embeddingProvider: fake }); + expect(svc).toBeDefined(); + const hits = await svc!.search("why does it die on reconnect", 2); + expect(hits[0].chunk.text.toLowerCase()).toContain("out of memory"); + expect(hits[0].matchedBy.length).toBeGreaterThan(0); + }); + + it("empty pg store → buildKnowledgeService returns undefined (no auto-write)", async () => { + const c = cfg(); + c.knowledge.store.pgvector!.table = `empty_${Math.floor(performance.now())}`; + const { openStore } = await import("./store-factory.js"); + const store = await openStore(c); + expect(await store.isPopulated()).toBe(false); // Confirm empty before building service. + await store.close(); + const svc = await buildKnowledgeService(c, { embeddingProvider: fake }); + expect(svc).toBeUndefined(); // Server does NOT auto-build for pgvector. + // Confirm the store is STILL empty (proves no auto-write). + const check = await openStore(c); + expect(await check.isPopulated()).toBe(false); + await check.close(); + }); +}); diff --git a/mcp/src/knowledge/pg-knowledge-store.pgtest.ts b/mcp/src/knowledge/pg-knowledge-store.pgtest.ts new file mode 100644 index 0000000000..15f2324df0 --- /dev/null +++ b/mcp/src/knowledge/pg-knowledge-store.pgtest.ts @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import { startPgVector } from "./pg-testcontainer.js"; +import { PgKnowledgeStore } from "./pg-knowledge-store.js"; +import type { Chunk } from "./types.js"; + +const chunk = (id: string, text: string): Chunk => ({ id, source: "s", title: "T", text, cite: `https://x/${id}` }); + +let pg: Awaited>; +beforeAll(async () => { pg = await startPgVector(); }, 120000); +afterAll(async () => { await pg?.stop(); }); + +async function fresh() { + const store = await PgKnowledgeStore.connect(pg.connectionString, `t_${Math.floor(performance.now())}`); + await store.reset(); + return store; +} + +describe("PgKnowledgeStore", () => { + it("addChunks + getChunk + isPopulated", async () => { + const s = await fresh(); + expect(await s.isPopulated()).toBe(false); + await s.addChunks([chunk("a", "netty out of memory")]); + expect(await s.isPopulated()).toBe(true); + expect((await s.getChunk("a"))?.text).toBe("netty out of memory"); + await s.close(); + }); + + it("ftsSearch ranks keyword matches", async () => { + const s = await fresh(); + await s.addChunks([chunk("a", "netty leak out of memory crash"), chunk("b", "policy access control")]); + expect((await s.ftsSearch("memory crash", 5))[0]).toBe("a"); + expect(await s.ftsSearch("zzznope", 5)).toEqual([]); + await s.close(); + }); + + it("vectorSearch returns nearest; hasVectors reflects state", async () => { + const s = await fresh(); + await s.ensureVectorTable(3); + expect(await s.hasVectors()).toBe(false); + await s.addChunks([chunk("x", "a"), chunk("y", "b")]); + await s.upsertVectors([{ id: "x", vector: [1, 0, 0] }, { id: "y", vector: [0, 1, 0] }]); + expect(await s.hasVectors()).toBe(true); + expect((await s.vectorSearch([1, 0, 0], 1))[0].id).toBe("x"); + await s.close(); + }); + + it("meta round-trip + reset clears everything", async () => { + const s = await fresh(); + await s.setMeta({ schemaVersion: 1, retriever: "fts", complete: true }); + expect((await s.getMeta())?.complete).toBe(true); + await s.addChunks([chunk("a", "x")]); + await s.reset(); + expect(await s.isPopulated()).toBe(false); + expect(await s.getMeta()).toBeUndefined(); + await s.close(); + }); + + it("reset() drops vec table → re-ingest can change dim", async () => { + const s = await fresh(); + await s.ensureVectorTable(3); + await s.addChunks([chunk("x", "a")]); + await s.upsertVectors([{ id: "x", vector: [1, 0, 0] }]); + expect(await s.hasVectors()).toBe(true); + await s.reset(); + expect(await s.isPopulated()).toBe(false); + // Re-ingest at a different dim succeeds (proves vec table was dropped + recreated). + await s.ensureVectorTable(4); + await s.addChunks([chunk("y", "b")]); + await s.upsertVectors([{ id: "y", vector: [0, 1, 0, 1] }]); + expect(await s.hasVectors()).toBe(true); + expect((await s.vectorSearch([0, 1, 0, 1], 1))[0].id).toBe("y"); + await s.close(); + }); + + it("ftsSearch uses OR semantics (chunk matches ANY token)", async () => { + const s = await fresh(); + await s.addChunks([chunk("a", "netty out of memory crash"), chunk("b", "policy access control")]); + // 2-term query where chunk b matches only "policy" (not "memory") → still returned (OR recall). + const hits = await s.ftsSearch("memory policy", 5); + expect(hits).toContain("a"); // matches "memory" + expect(hits).toContain("b"); // matches "policy" + await s.close(); + }); +}); diff --git a/mcp/src/knowledge/pg-knowledge-store.ts b/mcp/src/knowledge/pg-knowledge-store.ts new file mode 100644 index 0000000000..7cfe8b8d4e --- /dev/null +++ b/mcp/src/knowledge/pg-knowledge-store.ts @@ -0,0 +1,187 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import pg from "pg"; +import type { Chunk } from "./types.js"; +import type { IndexMeta, KnowledgeStore } from "./knowledge-store.js"; + +export class PgKnowledgeStore implements KnowledgeStore { + private constructor( + private readonly pool: pg.Pool, + private readonly t: string, // sanitized table prefix + private vecReady: boolean, + ) {} + + static async connect(connectionString: string, table: string): Promise { + if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(table)) { + throw new Error(`invalid pgvector table prefix: ${table}`); + } + if (table.length > 50) { + throw new Error(`table prefix too long (max 50 chars): ${table}`); + } + const pool = new pg.Pool({ connectionString }); + try { + await pool.query("CREATE EXTENSION IF NOT EXISTS vector"); + await pool.query( + `CREATE TABLE IF NOT EXISTS ${table}_chunks ( + id TEXT PRIMARY KEY, source TEXT, title TEXT, text TEXT, cite TEXT, + tsv tsvector GENERATED ALWAYS AS (to_tsvector('english', coalesce(title,'') || ' ' || coalesce(text,''))) STORED + )`, + ); + await pool.query(`CREATE INDEX IF NOT EXISTS ${table}_chunks_tsv ON ${table}_chunks USING GIN (tsv)`); + await pool.query(`CREATE TABLE IF NOT EXISTS ${table}_meta (id INT PRIMARY KEY CHECK (id = 1), json JSONB NOT NULL)`); + const vec = await pool.query( + "SELECT to_regclass($1) AS reg", + [`${table}_vec`], + ); + return new PgKnowledgeStore(pool, table, vec.rows[0].reg !== null); + } catch (e) { + await pool.end(); + throw e; + } + } + + async addChunks(chunks: Chunk[]): Promise { + const client = await this.pool.connect(); + try { + await client.query("BEGIN"); + for (const c of chunks) { + await client.query( + `INSERT INTO ${this.t}_chunks (id, source, title, text, cite) VALUES ($1,$2,$3,$4,$5) + ON CONFLICT (id) DO UPDATE SET source=$2, title=$3, text=$4, cite=$5`, + [c.id, c.source, c.title, c.text, c.cite], + ); + } + await client.query("COMMIT"); + } catch (e) { + await client.query("ROLLBACK").catch(() => {}); + throw e; + } finally { + client.release(); + } + } + + async getChunk(id: string): Promise { + const r = await this.pool.query( + `SELECT id, source, title, text, cite FROM ${this.t}_chunks WHERE id = $1`, + [id], + ); + return r.rows[0] as Chunk | undefined; + } + + async ftsSearch(query: string, k: number): Promise { + const tokens = [...query.matchAll(/[\p{L}\p{N}]+/gu)].map((m) => m[0]); + if (tokens.length === 0) return []; + const tsquery = tokens.join(" | "); + const r = await this.pool.query( + `SELECT id FROM ${this.t}_chunks + WHERE tsv @@ to_tsquery('english', $1) + ORDER BY ts_rank(tsv, to_tsquery('english', $1)) DESC, id + LIMIT $2`, + [tsquery, k], + ); + return r.rows.map((row) => row.id as string); + } + + async ensureVectorTable(dim: number): Promise { + if (this.vecReady) return; + if (!Number.isInteger(dim) || dim <= 0) throw new Error(`vector dim must be positive (got ${dim})`); + await this.pool.query( + `CREATE TABLE IF NOT EXISTS ${this.t}_vec (id TEXT PRIMARY KEY, embedding vector(${dim}))`, + ); + await this.pool.query( + `CREATE INDEX IF NOT EXISTS ${this.t}_vec_idx ON ${this.t}_vec USING hnsw (embedding vector_cosine_ops)`, + ); + this.vecReady = true; + } + + async upsertVectors(items: { id: string; vector: number[] }[]): Promise { + if (!this.vecReady) throw new Error("call ensureVectorTable(dim) before upsertVectors"); + const client = await this.pool.connect(); + try { + await client.query("BEGIN"); + for (const it of items) { + await client.query( + `INSERT INTO ${this.t}_vec (id, embedding) VALUES ($1, $2) + ON CONFLICT (id) DO UPDATE SET embedding = $2`, + [it.id, JSON.stringify(it.vector)], + ); + } + await client.query("COMMIT"); + } catch (e) { + await client.query("ROLLBACK").catch(() => {}); + throw e; + } finally { + client.release(); + } + } + + async vectorSearch(vector: number[], k: number): Promise<{ id: string; distance: number }[]> { + if (!this.vecReady) return []; + const r = await this.pool.query( + `SELECT id, embedding <=> $1 AS distance FROM ${this.t}_vec ORDER BY embedding <=> $1 LIMIT $2`, + [JSON.stringify(vector), k], + ); + return r.rows.map((row) => ({ id: row.id as string, distance: Number(row.distance) })); + } + + async isPopulated(): Promise { + const r = await this.pool.query(`SELECT EXISTS (SELECT 1 FROM ${this.t}_chunks) AS e`); + return r.rows[0].e === true; + } + + async hasVectors(): Promise { + if (!this.vecReady) return false; + const r = await this.pool.query(`SELECT EXISTS (SELECT 1 FROM ${this.t}_vec) AS e`); + return r.rows[0].e === true; + } + + async setMeta(meta: IndexMeta): Promise { + await this.pool.query( + `INSERT INTO ${this.t}_meta (id, json) VALUES (1, $1) ON CONFLICT (id) DO UPDATE SET json = $1`, + [JSON.stringify(meta)], + ); + } + + async getMeta(): Promise { + const r = await this.pool.query(`SELECT json FROM ${this.t}_meta WHERE id = 1`); + if (r.rows.length === 0) return undefined; + try { + const v = r.rows[0].json; + return (typeof v === "string" ? JSON.parse(v) : v) as IndexMeta; + } catch { + return undefined; + } + } + + async reset(): Promise { + const client = await this.pool.connect(); + try { + await client.query("BEGIN"); + await client.query(`TRUNCATE ${this.t}_chunks`); + await client.query(`DELETE FROM ${this.t}_meta`); + await client.query(`DROP TABLE IF EXISTS ${this.t}_vec`); + this.vecReady = false; + await client.query("COMMIT"); + } catch (e) { + await client.query("ROLLBACK").catch(() => {}); + throw e; + } finally { + client.release(); + } + } + + async close(): Promise { + await this.pool.end(); + } +} diff --git a/mcp/src/knowledge/pg-smoke.pgtest.ts b/mcp/src/knowledge/pg-smoke.pgtest.ts new file mode 100644 index 0000000000..72ff233687 --- /dev/null +++ b/mcp/src/knowledge/pg-smoke.pgtest.ts @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import { Client } from "pg"; +import { startPgVector } from "./pg-testcontainer.js"; + +let pg: Awaited>; +beforeAll(async () => { pg = await startPgVector(); }, 120000); +afterAll(async () => { await pg?.stop(); }); + +describe("pgvector container", () => { + it("has the vector extension available", async () => { + const client = new Client({ connectionString: pg.connectionString }); + await client.connect(); + await client.query("CREATE EXTENSION IF NOT EXISTS vector"); + const r = await client.query("SELECT '[1,2,3]'::vector AS v"); + expect(r.rows[0].v).toBeDefined(); + await client.end(); + }); +}); diff --git a/mcp/src/knowledge/pg-testcontainer.ts b/mcp/src/knowledge/pg-testcontainer.ts new file mode 100644 index 0000000000..5373f6403c --- /dev/null +++ b/mcp/src/knowledge/pg-testcontainer.ts @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { PostgreSqlContainer } from "@testcontainers/postgresql"; + +export async function startPgVector(): Promise<{ connectionString: string; stop: () => Promise }> { + const container = await new PostgreSqlContainer("pgvector/pgvector:pg16").start(); + return { + connectionString: container.getConnectionUri(), + stop: () => container.stop().then(() => undefined), + }; +} diff --git a/mcp/src/knowledge/public-source.test.ts b/mcp/src/knowledge/public-source.test.ts new file mode 100644 index 0000000000..5889f1dcdb --- /dev/null +++ b/mcp/src/knowledge/public-source.test.ts @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { describe, it, expect } from "vitest"; +import { PublicSource } from "./public-source.js"; + +const INDEX = `# Ditto docs +## Core +- [Things](https://eclipse.dev/ditto/things.md): about things +- [Policies](https://eclipse.dev/ditto/policies.md): about policies +Some prose that is not a link. +`; + +const DOCS: Record = { + "https://eclipse.dev/ditto/llms.txt": INDEX, + "https://eclipse.dev/ditto/things.md": "# Things\n\nA thing is a digital twin.", + "https://eclipse.dev/ditto/policies.md": "# Policies\n\nPolicies control access.", +}; + +const fakeFetch = async (url: string): Promise => { + if (!(url in DOCS)) throw new Error(`404 ${url}`); + return DOCS[url]; +}; + +describe("PublicSource", () => { + it("parses the index and chunks each linked document", async () => { + const src = new PublicSource({ + url: "https://eclipse.dev/ditto/llms.txt", + fetchFn: fakeFetch, + }); + const chunks = await src.loadChunks(); + expect(src.id).toBe("public"); + const cites = new Set(chunks.map((c) => c.cite)); + expect(cites.has("https://eclipse.dev/ditto/things.md")).toBe(true); + expect(cites.has("https://eclipse.dev/ditto/policies.md")).toBe(true); + expect(chunks.some((c) => c.text.includes("digital twin"))).toBe(true); + expect(chunks.every((c) => c.source === "public")).toBe(true); + }); + + it("honors maxDocs", async () => { + const src = new PublicSource({ + url: "https://eclipse.dev/ditto/llms.txt", + fetchFn: fakeFetch, + maxDocs: 1, + }); + const chunks = await src.loadChunks(); + expect(new Set(chunks.map((c) => c.cite)).size).toBe(1); + }); + + it("ingests only markdown links, skipping HTML/non-.md entries", async () => { + const index = `# Ditto docs +- [Repo](https://github.com/eclipse-ditto/ditto): source +- [OpenAPI](https://eclipse.dev/ditto/openapi/): api ui +- [Things](https://eclipse.dev/ditto/things.md): about things`; + const src = new PublicSource({ + url: "https://eclipse.dev/ditto/llms.txt", + fetchFn: async (u) => { + if (u === "https://eclipse.dev/ditto/llms.txt") return index; + if (u === "https://eclipse.dev/ditto/things.md") + return "# Things\n\nA thing is a digital twin."; + throw new Error(`should not fetch non-markdown url: ${u}`); + }, + }); + const chunks = await src.loadChunks(); + const cites = new Set(chunks.map((c) => c.cite)); + expect(cites).toEqual(new Set(["https://eclipse.dev/ditto/things.md"])); + }); + + it("skips documents that fail to fetch instead of throwing", async () => { + const src = new PublicSource({ + url: "https://eclipse.dev/ditto/llms.txt", + fetchFn: async (u) => { + if (u.endsWith("policies.md")) throw new Error("boom"); + return fakeFetch(u); + }, + }); + const chunks = await src.loadChunks(); + expect(chunks.some((c) => c.cite.endsWith("things.md"))).toBe(true); + expect(chunks.some((c) => c.cite.endsWith("policies.md"))).toBe(false); + }); +}); diff --git a/mcp/src/knowledge/public-source.ts b/mcp/src/knowledge/public-source.ts new file mode 100644 index 0000000000..280faaf7b5 --- /dev/null +++ b/mcp/src/knowledge/public-source.ts @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import type { Chunk, KnowledgeSource } from "./types.js"; +import { chunkMarkdown } from "./chunker.js"; + +export type FetchFn = (url: string, signal?: AbortSignal) => Promise; + +export interface PublicSourceOptions { + url: string; + fetchFn?: FetchFn; + maxDocs?: number; + chunkOptions?: { maxChars?: number; overlap?: number }; +} + +interface Entry { + title: string; + url: string; +} + +const defaultFetch: FetchFn = async (url, signal) => { + const timeout = AbortSignal.timeout(15000); + const sig = signal ? AbortSignal.any([signal, timeout]) : timeout; + const res = await fetch(url, { signal: sig }); + if (!res.ok) throw new Error(`fetch ${url} -> ${res.status}`); + const text = await res.text(); + if (text.length > 5_000_000) { + throw new Error(`fetch ${url} -> response too large (${text.length} bytes)`); + } + return text; +}; + +export class PublicSource implements KnowledgeSource { + readonly id = "public"; + private readonly opts: PublicSourceOptions; + private readonly fetchFn: FetchFn; + + constructor(opts: PublicSourceOptions) { + this.opts = opts; + this.fetchFn = opts.fetchFn ?? defaultFetch; + } + + async loadChunks(signal?: AbortSignal): Promise { + const index = await this.fetchFn(this.opts.url, signal); + let entries = parseEntries(index, this.opts.url); + if (this.opts.maxDocs !== undefined) { + entries = entries.slice(0, this.opts.maxDocs); + } + // Fetch documents with bounded concurrency (network-bound; sequential + // fetches make server startup block for a long time on large corpora). + // Order is preserved so chunk ids are deterministic across runs. + const CONCURRENCY = 8; + const perEntry: Chunk[][] = new Array(entries.length); + for (let i = 0; i < entries.length; i += CONCURRENCY) { + const batch = entries.slice(i, i + CONCURRENCY); + const results = await Promise.all( + batch.map(async (entry) => { + let md: string; + try { + md = await this.fetchFn(entry.url, signal); + } catch (err) { + process.stderr.write( + `[ditto-mcp] public-source: skipping ${entry.url}: ${String(err)}\n`, + ); + return []; + } + return chunkMarkdown(md, { + source: this.id, + title: entry.title, + cite: entry.url, + maxChars: this.opts.chunkOptions?.maxChars, + overlap: this.opts.chunkOptions?.overlap, + }); + }), + ); + results.forEach((r, j) => (perEntry[i + j] = r)); + } + // Re-key ids to be unique across documents (chunker numbers per-doc). + return perEntry.flat().map((c, i) => ({ ...c, id: `${this.id}#${i}` })); + } +} + +function parseEntries(index: string, baseUrl: string): Entry[] { + const re = /- \[([^\]]+)\]\(([^)]+)\)/g; + const entries: Entry[] = []; + let m: RegExpExecArray | null; + while ((m = re.exec(index)) !== null) { + const title = m[1].trim(); + const resolved = new URL(m[2].trim(), baseUrl); + // Guard against SSRF: only allow http/https schemes + if (resolved.protocol !== "http:" && resolved.protocol !== "https:") { + continue; + } + // Only ingest markdown docs; llms.txt indexes also link to HTML pages + // (repo, openapi/jsonschema UIs) that would pollute the corpus with markup. + const path = resolved.pathname.toLowerCase(); + if (!path.endsWith(".md") && !path.endsWith(".markdown")) { + continue; + } + entries.push({ title, url: resolved.toString() }); + } + return entries; +} diff --git a/mcp/src/knowledge/sqlite-knowledge-store.test.ts b/mcp/src/knowledge/sqlite-knowledge-store.test.ts new file mode 100644 index 0000000000..8d8184a481 --- /dev/null +++ b/mcp/src/knowledge/sqlite-knowledge-store.test.ts @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { describe, it, expect, afterEach } from "vitest"; +import { mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { SqliteKnowledgeStore } from "./sqlite-knowledge-store.js"; +import type { Chunk } from "./types.js"; + +const chunk = (id: string, text: string): Chunk => ({ + id, source: "s", title: "T", text, cite: `https://x/${id}`, +}); + +let store: SqliteKnowledgeStore; +afterEach(async () => await store?.close()); + +describe("SqliteKnowledgeStore", () => { + it("stores chunks and returns them by id", async () => { + store = new SqliteKnowledgeStore(); + await store.addChunks([chunk("a", "hello world")]); + expect((await store.getChunk("a"))?.text).toBe("hello world"); + expect(await store.getChunk("missing")).toBeUndefined(); + }); + + it("keyword-searches via FTS, best match first, honoring k, injection-safe", async () => { + store = new SqliteKnowledgeStore(); + await store.addChunks([ + chunk("a", "Netty leak out of memory crash on reconnect"), + chunk("b", "policies define access control"), + ]); + expect((await store.ftsSearch("memory crash", 5))[0]).toBe("a"); + expect(await store.ftsSearch("alpha", 1)).toEqual([]); + expect(await store.ftsSearch('"(bad) AND *', 5)).toEqual([]); // no throw + }); + + it("vector-searches nearest first after ensureVectorTable", async () => { + store = new SqliteKnowledgeStore(); + await store.ensureVectorTable(3); + await store.upsertVectors([ + { id: "x", vector: [1, 0, 0] }, + { id: "y", vector: [0, 1, 0] }, + ]); + const hits = await store.vectorSearch([1, 0, 0], 1); + expect(hits[0].id).toBe("x"); + }); + + it("vectorSearch returns [] when no vector table exists", async () => { + store = new SqliteKnowledgeStore(); + expect(await store.vectorSearch([1, 0, 0], 5)).toEqual([]); + }); + + it("isPopulated reflects whether chunks exist", async () => { + store = new SqliteKnowledgeStore(); + expect(await store.isPopulated()).toBe(false); + await store.addChunks([chunk("a", "x")]); + expect(await store.isPopulated()).toBe(true); + }); + + it("hasVectors is false until ensureVectorTable and upsertVectors; true after", async () => { + store = new SqliteKnowledgeStore(); + expect(await store.hasVectors()).toBe(false); + await store.ensureVectorTable(3); + expect(await store.hasVectors()).toBe(false); + await store.upsertVectors([{ id: "a", vector: [1, 0, 0] }]); + expect(await store.hasVectors()).toBe(true); + }); + + it("addChunks is idempotent (re-adding an id does not duplicate FTS results)", async () => { + store = new SqliteKnowledgeStore(); + await store.addChunks([chunk("a", "reconnect memory crash")]); + await store.addChunks([chunk("a", "reconnect memory crash")]); + expect(await store.ftsSearch("memory", 10)).toEqual(["a"]); + }); + + it("persists to a file: data survives close + reopen", async () => { + const dir = mkdtempSync(join(tmpdir(), "ditto-idx-")); + const path = join(dir, "index.db"); + const w = new SqliteKnowledgeStore(path); + await w.ensureVectorTable(3); + await w.addChunks([chunk("a", "reconnect memory")]); + await w.upsertVectors([{ id: "a", vector: [1, 0, 0] }]); + await w.close(); + + store = new SqliteKnowledgeStore(path); + expect(await store.isPopulated()).toBe(true); + expect((await store.getChunk("a"))?.text).toBe("reconnect memory"); + expect((await store.ftsSearch("memory", 5))[0]).toBe("a"); + expect((await store.vectorSearch([1, 0, 0], 1))[0].id).toBe("a"); + }); + + it("isPopulated/hasVectors are async and correct", async () => { + store = new SqliteKnowledgeStore(); + expect(await store.isPopulated()).toBe(false); + expect(await store.hasVectors()).toBe(false); + }); + + it("stores and returns index metadata", async () => { + store = new SqliteKnowledgeStore(); + expect(await store.getMeta()).toBeUndefined(); + await store.setMeta({ schemaVersion: 1, retriever: "fts", complete: true }); + expect(await store.getMeta()).toEqual({ schemaVersion: 1, retriever: "fts", complete: true }); + }); + + it("closes the DB handle on failed init (corrupt/non-sqlite file)", () => { + const dir = mkdtempSync(join(tmpdir(), "ditto-idx-")); + const path = join(dir, "corrupt.db"); + const { writeFileSync } = require("node:fs"); + writeFileSync(path, "not a db"); + expect(() => new SqliteKnowledgeStore(path)).toThrow(); + }); + + it("reset() clears chunks, fts, vectors, and meta", async () => { + store = new SqliteKnowledgeStore(); + store.ensureVectorTable ? await store.ensureVectorTable(3) : null; + await store.addChunks([{ id: "a", source: "s", title: "T", text: "netty", cite: "x" }]); + await store.upsertVectors([{ id: "a", vector: [1, 0, 0] }]); + await store.setMeta({ schemaVersion: 1, retriever: "fts", complete: true }); + await store.reset(); + expect(await store.isPopulated()).toBe(false); + expect(await store.getChunk("a")).toBeUndefined(); + expect(await store.getMeta()).toBeUndefined(); + }); +}); diff --git a/mcp/src/knowledge/sqlite-knowledge-store.ts b/mcp/src/knowledge/sqlite-knowledge-store.ts new file mode 100644 index 0000000000..69e5a1a59c --- /dev/null +++ b/mcp/src/knowledge/sqlite-knowledge-store.ts @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import Database from "better-sqlite3"; +import * as sqliteVec from "sqlite-vec"; +import type { Chunk } from "./types.js"; +import type { KnowledgeStore, IndexMeta } from "./knowledge-store.js"; + +export class SqliteKnowledgeStore implements KnowledgeStore { + private readonly db: Database.Database; + private vecReady = false; + + constructor(path = ":memory:") { + this.db = new Database(path); + try { + sqliteVec.load(this.db); + this.db.exec(` + CREATE TABLE IF NOT EXISTS chunks ( + id TEXT PRIMARY KEY, source TEXT, title TEXT, text TEXT, cite TEXT + ); + CREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts USING fts5(id UNINDEXED, title, text); + CREATE TABLE IF NOT EXISTS index_meta (id INTEGER PRIMARY KEY CHECK (id = 1), json TEXT NOT NULL); + `); + // Detect a pre-existing vec table (reopened prebuilt file). + const row = this.db + .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='vec_items'") + .get(); + this.vecReady = row !== undefined; + } catch (e) { + this.db.close(); + throw e; + } + } + + async addChunks(chunks: Chunk[]): Promise { + const insC = this.db.prepare( + "INSERT OR REPLACE INTO chunks (id, source, title, text, cite) VALUES (?, ?, ?, ?, ?)", + ); + const delF = this.db.prepare("DELETE FROM chunks_fts WHERE id = ?"); + const insF = this.db.prepare( + "INSERT INTO chunks_fts (id, title, text) VALUES (?, ?, ?)", + ); + const tx = this.db.transaction((rows: Chunk[]) => { + for (const c of rows) { + insC.run(c.id, c.source, c.title, c.text, c.cite); + delF.run(c.id); + insF.run(c.id, c.title, c.text); + } + }); + tx(chunks); + } + + async getChunk(id: string): Promise { + const r = this.db + .prepare("SELECT id, source, title, text, cite FROM chunks WHERE id = ?") + .get(id) as Chunk | undefined; + return r; + } + + async ftsSearch(query: string, k: number): Promise { + const match = toMatchQuery(query); + if (match === "") return []; + const rows = this.db + .prepare("SELECT id FROM chunks_fts WHERE chunks_fts MATCH ? ORDER BY rank, id LIMIT ?") + .all(match, k) as Array<{ id: string }>; + return rows.map((r) => r.id); + } + + async ensureVectorTable(dim: number): Promise { + if (this.vecReady) return; + if (!Number.isInteger(dim) || dim <= 0) { + throw new Error(`vector dim must be a positive integer (got ${dim})`); + } + this.db.exec( + `CREATE VIRTUAL TABLE IF NOT EXISTS vec_items USING vec0(id TEXT PRIMARY KEY, embedding float[${dim}]);`, + ); + this.vecReady = true; + } + + async upsertVectors(items: { id: string; vector: number[] }[]): Promise { + if (!this.vecReady) throw new Error("call ensureVectorTable(dim) before upsertVectors"); + const del = this.db.prepare("DELETE FROM vec_items WHERE id = ?"); + const ins = this.db.prepare("INSERT INTO vec_items (id, embedding) VALUES (?, ?)"); + const tx = this.db.transaction((rows: { id: string; vector: number[] }[]) => { + for (const r of rows) { + del.run(r.id); + ins.run(r.id, JSON.stringify(r.vector)); + } + }); + tx(items); + } + + async vectorSearch(vector: number[], k: number): Promise<{ id: string; distance: number }[]> { + if (!this.vecReady) return []; + const rows = this.db + .prepare("SELECT id, distance FROM vec_items WHERE embedding MATCH ? AND k = ? ORDER BY distance") + .all(JSON.stringify(vector), k) as Array<{ id: string; distance: number }>; + return rows.map((r) => ({ id: r.id, distance: r.distance })); + } + + async isPopulated(): Promise { + const row = this.db.prepare("SELECT COUNT(*) AS n FROM chunks").get() as { n: number }; + return row.n > 0; + } + + async hasVectors(): Promise { + if (!this.vecReady) return false; + const row = this.db.prepare("SELECT COUNT(*) AS n FROM vec_items").get() as { n: number }; + return row.n > 0; + } + + async setMeta(meta: IndexMeta): Promise { + this.db.prepare("INSERT OR REPLACE INTO index_meta (id, json) VALUES (1, ?)") + .run(JSON.stringify(meta)); + } + + async getMeta(): Promise { + const row = this.db.prepare("SELECT json FROM index_meta WHERE id = 1").get() as { json: string } | undefined; + if (!row) return undefined; + try { + return JSON.parse(row.json) as IndexMeta; + } catch { + return undefined; + } + } + + async reset(): Promise { + this.db.exec("DELETE FROM chunks; DELETE FROM chunks_fts; DELETE FROM index_meta;"); + if (this.vecReady) this.db.exec("DELETE FROM vec_items;"); + } + + async close(): Promise { + this.db.close(); + } +} + +/** Arbitrary user text -> safe FTS5 MATCH (word tokens, quoted, OR-joined). */ +function toMatchQuery(query: string): string { + const tokens = query.match(/[\p{L}\p{N}]+/gu); + if (!tokens || tokens.length === 0) return ""; + return tokens.map((t) => `"${t}"`).join(" OR "); +} diff --git a/mcp/src/knowledge/store-factory.test.ts b/mcp/src/knowledge/store-factory.test.ts new file mode 100644 index 0000000000..8488bf6361 --- /dev/null +++ b/mcp/src/knowledge/store-factory.test.ts @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { describe, it, expect, afterEach } from "vitest"; +import { AppConfigSchema } from "../config/schema.js"; +import { openStore } from "./store-factory.js"; +import type { KnowledgeStore } from "./knowledge-store.js"; + +let store: KnowledgeStore; +afterEach(async () => { await store?.close(); }); + +describe("openStore", () => { + it("opens an in-memory sqlite store by default", async () => { + store = await openStore(AppConfigSchema.parse({}), { path: ":memory:" }); + expect(await store.isPopulated()).toBe(false); + }); +}); diff --git a/mcp/src/knowledge/store-factory.ts b/mcp/src/knowledge/store-factory.ts new file mode 100644 index 0000000000..04513097c9 --- /dev/null +++ b/mcp/src/knowledge/store-factory.ts @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import type { AppConfig } from "../config/schema.js"; +import type { KnowledgeStore } from "./knowledge-store.js"; + +const reason = (e: unknown) => (e instanceof Error ? e.message : String(e)); + +/** Open a KnowledgeStore for the configured backend. `opts.path` overrides the + * sqlite path (e.g. ":memory:" for the in-memory fallback). + * + * Backends are imported lazily so their native deps load only when selected: + * sqlite pulls better-sqlite3 + sqlite-vec, pgvector pulls pg. A missing dep + * surfaces as a clear error naming the fix, not a raw MODULE_NOT_FOUND. */ +export async function openStore( + config: AppConfig, + opts: { path?: string } = {}, +): Promise { + const kind = config.knowledge.store.kind; + if (kind === "sqlite") { + const path = opts.path ?? config.knowledge.store.sqlite.path ?? ":memory:"; + let SqliteKnowledgeStore: typeof import("./sqlite-knowledge-store.js").SqliteKnowledgeStore; + try { + ({ SqliteKnowledgeStore } = await import("./sqlite-knowledge-store.js")); + } catch (e) { + throw new Error( + `sqlite store requires better-sqlite3 + sqlite-vec, which failed to load: ${reason(e)}. ` + + `Install them, or set knowledge.store.kind=pgvector.`, + ); + } + return new SqliteKnowledgeStore(path); + } + if (kind === "pgvector") { + const { connectionString, table } = config.knowledge.store.pgvector; + if (!connectionString) throw new Error("knowledge.store.pgvector.connectionString is required"); + let PgKnowledgeStore: typeof import("./pg-knowledge-store.js").PgKnowledgeStore; + try { + ({ PgKnowledgeStore } = await import("./pg-knowledge-store.js")); + } catch (e) { + throw new Error(`pgvector store requires the 'pg' package, which failed to load: ${reason(e)}.`); + } + return PgKnowledgeStore.connect(connectionString, table); + } + throw new Error(`unsupported knowledge.store.kind: ${kind}`); +} diff --git a/mcp/src/knowledge/types.ts b/mcp/src/knowledge/types.ts new file mode 100644 index 0000000000..88894588b9 --- /dev/null +++ b/mcp/src/knowledge/types.ts @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +export interface Chunk { + id: string; + source: string; + title: string; + text: string; + cite: string; +} + +export interface RetrievedChunk { + chunk: Chunk; + matchedBy: string[]; // leaf retriever kinds, e.g. ["fts"], ["vector"], ["fts","vector"] + // "anchor" = matched by the retriever; "context" = a positional neighbor pulled + // in by expansion. Absent means anchor (pre-expansion results). + role?: "anchor" | "context"; +} + +/** A corpus provider: yields chunks. Does not search. */ +export interface KnowledgeSource { + id: string; + loadChunks(signal?: AbortSignal): Promise; +} + +/** Indexes chunks and searches them. */ +export interface Retriever { + readonly kind: string; + search(query: string, k: number): Promise; +} diff --git a/mcp/src/knowledge/vector-retriever.test.ts b/mcp/src/knowledge/vector-retriever.test.ts new file mode 100644 index 0000000000..fabda53c7d --- /dev/null +++ b/mcp/src/knowledge/vector-retriever.test.ts @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { describe, it, expect, afterEach } from "vitest"; +import { VectorRetriever } from "./vector-retriever.js"; +import { SqliteKnowledgeStore } from "./sqlite-knowledge-store.js"; +import type { EmbeddingProvider } from "./embedding.js"; +import type { Chunk } from "./types.js"; + +const fake: EmbeddingProvider = { + dim: 3, + embed: async (texts) => + texts.map((t) => { + const s = t.toLowerCase(); + if (s.includes("reconnect") || s.includes("oom") || s.includes("memory")) return [1, 0, 0]; + if (s.includes("policy") || s.includes("access")) return [0, 1, 0]; + return [0, 0, 1]; + }), +}; +const chunk = (id: string, text: string): Chunk => ({ + id, source: "s", title: "T", text, cite: `https://x/${id}`, +}); + +let store: SqliteKnowledgeStore; +afterEach(async () => await store?.close()); + +describe("VectorRetriever", () => { + async function retriever(chunks: Chunk[]) { + store = new SqliteKnowledgeStore(); + await store.ensureVectorTable(fake.dim); + await store.addChunks(chunks); + const vectors = await fake.embed(chunks.map((c) => c.text)); + await store.upsertVectors(chunks.map((c, i) => ({ id: c.id, vector: vectors[i] }))); + return new VectorRetriever(store, fake); + } + + it("returns semantic hits tagged matchedBy=['vector']", async () => { + const r = await retriever([ + chunk("oom", "Netty leak out of memory crash"), + chunk("pol", "policy access control"), + ]); + const hits = await r.search("why does it die on reconnect", 1); + expect(hits[0].chunk.id).toBe("oom"); + expect(hits[0].matchedBy).toEqual(["vector"]); + }); +}); diff --git a/mcp/src/knowledge/vector-retriever.ts b/mcp/src/knowledge/vector-retriever.ts new file mode 100644 index 0000000000..4a1164832d --- /dev/null +++ b/mcp/src/knowledge/vector-retriever.ts @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import type { RetrievedChunk, Retriever } from "./types.js"; +import type { EmbeddingProvider } from "./embedding.js"; +import type { KnowledgeStore } from "./knowledge-store.js"; + +export class VectorRetriever implements Retriever { + readonly kind = "vector"; + constructor( + private readonly store: KnowledgeStore, + private readonly embedder: EmbeddingProvider, + ) {} + + async search(query: string, k: number): Promise { + const [vector] = await this.embedder.embed([query]); + const hits = await this.store.vectorSearch(vector, k); + const out: RetrievedChunk[] = []; + for (const hit of hits) { + const chunk = await this.store.getChunk(hit.id); + if (chunk) out.push({ chunk, matchedBy: ["vector"] }); + } + return out; + } +} diff --git a/mcp/src/registry/tool-registry.test.ts b/mcp/src/registry/tool-registry.test.ts new file mode 100644 index 0000000000..12d510efa8 --- /dev/null +++ b/mcp/src/registry/tool-registry.test.ts @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { describe, it, expect } from "vitest"; +import { ToolRegistry } from "./tool-registry.js"; +import type { ToolDef } from "../core/types.js"; + +const makeTool = (name: string): ToolDef => ({ + name, + description: `tool ${name}`, + inputSchema: {}, + handler: async () => ({ content: [{ type: "text", text: name }] }), +}); + +describe("ToolRegistry", () => { + it("registers and retrieves a tool", () => { + const r = new ToolRegistry(); + r.register(makeTool("a")); + expect(r.get("a")?.name).toBe("a"); + expect(r.list().map((t) => t.name)).toEqual(["a"]); + }); + + it("throws on duplicate tool names", () => { + const r = new ToolRegistry(); + r.register(makeTool("a")); + expect(() => r.register(makeTool("a"))).toThrow(/duplicate/i); + }); + + it("returns undefined for unknown tools", () => { + const r = new ToolRegistry(); + expect(r.get("missing")).toBeUndefined(); + }); +}); diff --git a/mcp/src/registry/tool-registry.ts b/mcp/src/registry/tool-registry.ts new file mode 100644 index 0000000000..9e491a5131 --- /dev/null +++ b/mcp/src/registry/tool-registry.ts @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import type { ToolDef } from "../core/types.js"; + +export class ToolRegistry { + private readonly tools = new Map(); + + register(def: ToolDef): void { + if (this.tools.has(def.name)) { + throw new Error(`duplicate tool: ${def.name}`); + } + this.tools.set(def.name, def); + } + + list(): ToolDef[] { + return [...this.tools.values()]; + } + + get(name: string): ToolDef | undefined { + return this.tools.get(name); + } +} diff --git a/mcp/src/sanity.test.ts b/mcp/src/sanity.test.ts new file mode 100644 index 0000000000..4b46ba0ae3 --- /dev/null +++ b/mcp/src/sanity.test.ts @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { describe, it, expect } from "vitest"; + +describe("toolchain sanity", () => { + it("runs vitest with ESM", () => { + expect(1 + 1).toBe(2); + }); +}); diff --git a/mcp/src/server/build-server.test.ts b/mcp/src/server/build-server.test.ts new file mode 100644 index 0000000000..ae2d74b8ee --- /dev/null +++ b/mcp/src/server/build-server.test.ts @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { describe, it, expect } from "vitest"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; +import { AppConfigSchema } from "../config/schema.js"; +import { registerTools } from "../tools/index.js"; +import { buildServer } from "./build-server.js"; + +async function connectedClient() { + // knowledge disabled to keep this transport test hermetic; knowledge covered in knowledge tests. + const config = AppConfigSchema.parse({ knowledge: { enabled: false } }); + const registry = await registerTools(config); + const server = buildServer(registry, config); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + await server.connect(serverTransport); + const client = new Client({ name: "test-client", version: "0.0.0" }); + await client.connect(clientTransport); + return client; +} + +describe("buildServer + ping (in-memory e2e)", () => { + it("lists the ping tool", async () => { + const client = await connectedClient(); + const { tools } = await client.listTools(); + expect(tools.map((t) => t.name)).toContain("ping"); + await client.close(); + }); + + it("calls ping and gets pong", async () => { + const client = await connectedClient(); + const res = await client.callTool({ name: "ping", arguments: {} }); + const content = res.content as Array<{ type: string; text: string }>; + expect(content[0]).toEqual({ type: "text", text: "pong" }); + await client.close(); + }); + + it("omits ping when disabled in config", async () => { + // knowledge disabled to keep this transport test hermetic; knowledge covered in knowledge tests. + const config = AppConfigSchema.parse({ + tools: { ping: false }, + knowledge: { enabled: false }, + }); + const registry = await registerTools(config); + const server = buildServer(registry, config); + const [ct, st] = InMemoryTransport.createLinkedPair(); + await server.connect(st); + const client = new Client({ name: "t", version: "0" }); + await client.connect(ct); + const { tools } = await client.listTools(); + expect(tools.map((t) => t.name)).not.toContain("ping"); + await client.close(); + }); +}); diff --git a/mcp/src/server/build-server.ts b/mcp/src/server/build-server.ts new file mode 100644 index 0000000000..44d5ef5c2c --- /dev/null +++ b/mcp/src/server/build-server.ts @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolRegistry } from "../registry/tool-registry.js"; +import type { AppConfig } from "../config/schema.js"; +import { buildCtx } from "./request-ctx.js"; + +export function buildServer( + registry: ToolRegistry, + config: AppConfig, +): McpServer { + const server = new McpServer({ name: config.server.name, version: "0.1.0" }); + + // Initialize tool handlers even when registry is empty + // by registering and immediately disabling a placeholder. + // This ensures tools/list is always available (SDK 1.29.0 lazy-initializes handlers). + if (registry.list().length === 0) { + const placeholder = server.registerTool( + "_init", + { description: "Placeholder to initialize tool handlers" }, + async () => ({ content: [] }), + ); + placeholder.disable(); + } + + for (const def of registry.list()) { + server.registerTool( + def.name, + { description: def.description, inputSchema: def.inputSchema }, + async (args: unknown, extra: unknown) => + def.handler(args, buildCtx(config, extra as Parameters[1])), + ); + } + return server; +} diff --git a/mcp/src/server/http-app.test.ts b/mcp/src/server/http-app.test.ts new file mode 100644 index 0000000000..598a1d0254 --- /dev/null +++ b/mcp/src/server/http-app.test.ts @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { describe, it, expect } from "vitest"; +import { request } from "node:http"; +import type { Server } from "node:http"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; +import { AppConfigSchema } from "../config/schema.js"; +import { createHttpApp } from "./http-app.js"; + +async function listen(): Promise<{ server: Server; url: string }> { + // protocol e2e; DNS-rebinding protection exercised separately below. + // knowledge disabled to keep this transport test hermetic; knowledge covered in knowledge tests. + const config = AppConfigSchema.parse({ + server: { http: { enableDnsRebindingProtection: false } }, + knowledge: { enabled: false }, + }); + const app = createHttpApp(config); + return await new Promise((res) => { + const server = app.listen(0, () => { + const addr = server.address(); + const port = typeof addr === "object" && addr ? addr.port : 0; + res({ server, url: `http://127.0.0.1:${port}/mcp` }); + }); + }); +} + +describe("streamable HTTP app (e2e)", () => { + it("serves ping over streamable HTTP with a session", async () => { + const { server, url } = await listen(); + const transport = new StreamableHTTPClientTransport(new URL(url)); + const client = new Client({ name: "http-test", version: "0.0.0" }); + await client.connect(transport); + const res = await client.callTool({ name: "ping", arguments: {} }); + const content = res.content as Array<{ type: string; text: string }>; + expect(content[0].text).toBe("pong"); + await client.close(); + await new Promise((r) => server.close(() => r())); + }); + + it("rejects prototype pollution attempts without crashing", async () => { + const { server, url } = await listen(); + const response = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + "mcp-session-id": "__proto__", + }, + body: JSON.stringify({ + jsonrpc: "2.0", + id: 1, + method: "tools/call", + params: { name: "ping", arguments: {} }, + }), + }); + expect(response.status).toBe(400); + const body = await response.json(); + expect(body.error.message).toBe("Bad Request: no valid session"); + await new Promise((r) => server.close(() => r())); + }); + + it("rejects DNS rebinding attacks by default", async () => { + // Default config has DNS-rebinding protection enabled + // knowledge disabled to keep this transport test hermetic; knowledge covered in knowledge tests. + const config = AppConfigSchema.parse({ knowledge: { enabled: false } }); + const app = createHttpApp(config); + const server = await new Promise((res) => { + const srv = app.listen(0, () => res(srv)); + }); + const addr = server.address(); + const port = typeof addr === "object" && addr ? addr.port : 0; + + const body = JSON.stringify({ + jsonrpc: "2.0", + id: 1, + method: "initialize", + params: { + protocolVersion: "2024-11-05", + capabilities: {}, + clientInfo: { name: "attacker", version: "1.0" }, + }, + }); + + const statusCode = await new Promise((resolve) => { + const req = request( + { + hostname: "127.0.0.1", + port, + path: "/mcp", + method: "POST", + headers: { + "Content-Type": "application/json", + "Content-Length": Buffer.byteLength(body), + host: "attacker.example", + }, + }, + (res) => { + resolve(res.statusCode ?? 0); + }, + ); + req.write(body); + req.end(); + }); + + expect(statusCode).toBe(403); + await new Promise((r) => server.close(() => r())); + }); +}); diff --git a/mcp/src/server/http-app.ts b/mcp/src/server/http-app.ts new file mode 100644 index 0000000000..b5e30eb879 --- /dev/null +++ b/mcp/src/server/http-app.ts @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import express, { type Express, type Request, type Response } from "express"; +import { randomUUID } from "node:crypto"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"; +import type { AppConfig } from "../config/schema.js"; +import { registerTools } from "../tools/index.js"; +import { buildServer } from "./build-server.js"; +import type { KnowledgeService } from "../knowledge/knowledge-service.js"; + +export function createHttpApp( + config: AppConfig, + knowledgeService?: KnowledgeService, +): Express { + const app = express(); + app.use(express.json()); + + const transports = new Map(); + + app.post("/mcp", async (req: Request, res: Response) => { + try { + const sessionId = req.headers["mcp-session-id"] as string | undefined; + let transport: StreamableHTTPServerTransport | undefined = + sessionId ? transports.get(sessionId) : undefined; + + if (!transport) { + if (sessionId || !isInitializeRequest(req.body)) { + res.status(400).json({ + jsonrpc: "2.0", + error: { code: -32000, message: "Bad Request: no valid session" }, + id: null, + }); + return; + } + const http = config.server.http; + const allowedHosts = http.allowedHosts ?? [ + `${http.host}:${http.port}`, + `127.0.0.1:${http.port}`, + `localhost:${http.port}`, + `[::1]:${http.port}`, + ]; + transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => randomUUID(), + onsessioninitialized: (sid) => { + transports.set(sid, transport as StreamableHTTPServerTransport); + }, + enableDnsRebindingProtection: http.enableDnsRebindingProtection, + allowedHosts: http.enableDnsRebindingProtection + ? allowedHosts + : undefined, + allowedOrigins: http.allowedOrigins, + }); + transport.onclose = () => { + if (transport?.sessionId) transports.delete(transport.sessionId); + }; + const registry = await registerTools(config, knowledgeService); + const server = buildServer(registry, config); + await server.connect(transport); + } + + await transport.handleRequest(req, res, req.body); + } catch (error) { + process.stderr.write( + `[ditto-mcp] POST /mcp error: ${error instanceof Error ? error.message : String(error)}\n`, + ); + if (!res.headersSent) { + res.status(500).json({ + jsonrpc: "2.0", + error: { code: -32603, message: "Internal error" }, + id: null, + }); + } + } + }); + + const handleSession = async (req: Request, res: Response) => { + try { + const sessionId = req.headers["mcp-session-id"] as string | undefined; + const transport = sessionId ? transports.get(sessionId) : undefined; + if (!transport) { + res.status(400).send("Invalid or missing session ID"); + return; + } + await transport.handleRequest(req, res); + } catch (error) { + process.stderr.write( + `[ditto-mcp] ${req.method} /mcp error: ${error instanceof Error ? error.message : String(error)}\n`, + ); + if (!res.headersSent) { + res.status(500).json({ + jsonrpc: "2.0", + error: { code: -32603, message: "Internal error" }, + id: null, + }); + } + } + }; + + app.get("/mcp", handleSession); + app.delete("/mcp", handleSession); + + const http = config.server.http; + const LOOPBACK = new Set(["127.0.0.1", "localhost", "::1", "[::1]"]); + if ( + http.enableDnsRebindingProtection && + !http.allowedHosts && + !LOOPBACK.has(http.host) + ) { + process.stderr.write( + `[ditto-mcp] WARNING: host '${http.host}' is non-loopback but server.http.allowedHosts is not set; ` + + `DNS-rebinding protection will 403 remote requests whose Host header is not in the derived loopback allowlist. ` + + `Set server.http.allowedHosts explicitly for remote deployments.\n`, + ); + } + + return app; +} diff --git a/mcp/src/server/request-ctx.test.ts b/mcp/src/server/request-ctx.test.ts new file mode 100644 index 0000000000..d42f53db5a --- /dev/null +++ b/mcp/src/server/request-ctx.test.ts @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { describe, it, expect } from "vitest"; +import { AppConfigSchema } from "../config/schema.js"; +import { buildCtx } from "./request-ctx.js"; + +describe("buildCtx", () => { + const config = AppConfigSchema.parse({}); + + it("maps sessionId, headers, and signal from extra", () => { + const controller = new AbortController(); + const ctx = buildCtx(config, { + sessionId: "sess-1", + requestInfo: { headers: { "x-test": "yes" } }, + signal: controller.signal, + }); + expect(ctx.config).toBe(config); + expect(ctx.sessionId).toBe("sess-1"); + expect(ctx.headers).toEqual({ "x-test": "yes" }); + expect(ctx.signal).toBe(controller.signal); + }); + + it("tolerates a minimal extra (stdio has no headers/session)", () => { + const ctx = buildCtx(config, {}); + expect(ctx.config).toBe(config); + expect(ctx.sessionId).toBeUndefined(); + expect(ctx.headers).toBeUndefined(); + expect(ctx.signal).toBeUndefined(); + }); +}); diff --git a/mcp/src/server/request-ctx.ts b/mcp/src/server/request-ctx.ts new file mode 100644 index 0000000000..643820a64d --- /dev/null +++ b/mcp/src/server/request-ctx.ts @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import type { AppConfig } from "../config/schema.js"; +import type { RequestCtx } from "../core/types.js"; + +/** Minimal structural view of the SDK's tool-handler `extra` argument. */ +export interface McpExtra { + sessionId?: string; + requestInfo?: { headers?: Record }; + signal?: AbortSignal; +} + +export function buildCtx(config: AppConfig, extra: McpExtra): RequestCtx { + return { + config, + sessionId: extra.sessionId, + headers: extra.requestInfo?.headers, + signal: extra.signal, + }; +} diff --git a/mcp/src/tools/index.test.ts b/mcp/src/tools/index.test.ts new file mode 100644 index 0000000000..9e25a9b322 --- /dev/null +++ b/mcp/src/tools/index.test.ts @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { describe, it, expect, afterEach } from "vitest"; +import { registerTools } from "./index.js"; +import { AppConfigSchema } from "../config/schema.js"; +import { KnowledgeService } from "../knowledge/knowledge-service.js"; +import { SqliteKnowledgeStore } from "../knowledge/sqlite-knowledge-store.js"; +import { FtsRetriever } from "../knowledge/fts-retriever.js"; +import type { Chunk } from "../knowledge/types.js"; + +const chunk = (id: string, text: string): Chunk => ({ + id, + source: "test", + title: "T", + text, + cite: `https://x/${id}`, +}); + +let store: SqliteKnowledgeStore | undefined; +afterEach(() => store?.close()); + +describe("registerTools wiring", () => { + it("registers ping by default", async () => { + const reg = await registerTools( + AppConfigSchema.parse({ knowledge: { enabled: false } }), + ); + expect(reg.get("ping")).toBeDefined(); + }); + + it("omits action tools when ditto disabled (default)", async () => { + const reg = await registerTools(AppConfigSchema.parse({})); + expect(reg.list().some((t) => t.name === "getThingById")).toBe(false); + }); + + it("omits knowledge tools when knowledge disabled", async () => { + const reg = await registerTools( + AppConfigSchema.parse({ knowledge: { enabled: false } }), + ); + expect(reg.get("search")).toBeUndefined(); + expect(reg.get("get_chunk")).toBeUndefined(); + }); + + it("omits knowledge tools when service not provided", async () => { + const reg = await registerTools( + AppConfigSchema.parse({ knowledge: { enabled: true } }), + ); + expect(reg.get("search")).toBeUndefined(); + expect(reg.get("get_chunk")).toBeUndefined(); + }); + + it("registers knowledge tools when enabled and service provided", async () => { + store = new SqliteKnowledgeStore(":memory:"); + await store.addChunks([chunk("a", "test content")]); + const retriever = new FtsRetriever(store); + const service = new KnowledgeService(store, retriever); + const reg = await registerTools( + AppConfigSchema.parse({ knowledge: { enabled: true } }), + service, + ); + expect(reg.get("ping")).toBeDefined(); + expect(reg.get("search")).toBeDefined(); + expect(reg.get("get_chunk")).toBeDefined(); + }); +}); diff --git a/mcp/src/tools/index.ts b/mcp/src/tools/index.ts new file mode 100644 index 0000000000..9b3aea59a9 --- /dev/null +++ b/mcp/src/tools/index.ts @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { ToolRegistry } from "../registry/tool-registry.js"; +import type { AppConfig } from "../config/schema.js"; +import { pingTool } from "./ping.js"; +import { makeKnowledgeTools } from "./knowledge.js"; +import type { KnowledgeService } from "../knowledge/knowledge-service.js"; + +export async function registerTools( + config: AppConfig, + knowledgeService?: KnowledgeService, +): Promise { + const registry = new ToolRegistry(); + if (config.tools.ping) registry.register(pingTool); + if (config.knowledge.enabled && knowledgeService) { + for (const tool of makeKnowledgeTools(knowledgeService, config.knowledge.search)) + registry.register(tool); + } + if (config.ditto.enabled) { + const { makeActionTools } = await import("../ditto/action-tools.js"); + for (const tool of await makeActionTools(config)) registry.register(tool); + } + return registry; +} diff --git a/mcp/src/tools/knowledge.test.ts b/mcp/src/tools/knowledge.test.ts new file mode 100644 index 0000000000..469e87badb --- /dev/null +++ b/mcp/src/tools/knowledge.test.ts @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { describe, it, expect, afterEach } from "vitest"; +import { KnowledgeService } from "../knowledge/knowledge-service.js"; +import { SqliteKnowledgeStore } from "../knowledge/sqlite-knowledge-store.js"; +import { FtsRetriever } from "../knowledge/fts-retriever.js"; +import { makeKnowledgeTools } from "./knowledge.js"; +import { AppConfigSchema } from "../config/schema.js"; + +const ctx = { config: AppConfigSchema.parse({}) }; +let store: SqliteKnowledgeStore | undefined; +afterEach(() => store?.close()); + +async function tools() { + store = new SqliteKnowledgeStore(":memory:"); + await store.addChunks([ + { id: "a", source: "s", title: "Things", text: "a thing is a digital twin", cite: "https://x/a" }, + ]); + const retriever = new FtsRetriever(store); + const svc = new KnowledgeService(store, retriever); + return Object.fromEntries(makeKnowledgeTools(svc).map((t) => [t.name, t])); +} + +describe("knowledge tools", () => { + it("search returns matching chunk text with citation and matched provenance", async () => { + const t = await tools(); + const res = await t.search.handler({ query: "digital twin" }, ctx); + const text = res.content.map((p) => p.text).join("\n"); + expect(text).toContain("digital twin"); + expect(text).toContain("https://x/a"); + expect(text).toContain("matched: fts"); + }); + + it("search reports no results cleanly", async () => { + const t = await tools(); + const res = await t.search.handler({ query: "zzzznotfound" }, ctx); + expect(res.content[0].text.toLowerCase()).toContain("no results"); + }); + + it("get_chunk returns the chunk by id, or a not-found message", async () => { + const t = await tools(); + const ok = await t.get_chunk.handler({ id: "a" }, ctx); + expect(ok.content[0].text).toContain("digital twin"); + const miss = await t.get_chunk.handler({ id: "nope" }, ctx); + expect(miss.content[0].text.toLowerCase()).toContain("not found"); + }); + + it("search pulls in same-document neighbors as labeled context", async () => { + store = new SqliteKnowledgeStore(":memory:"); + await store.addChunks([ + { id: "pub#0", source: "pub", title: "T", text: "alpha prelude", cite: "https://x/doc" }, + { id: "pub#1", source: "pub", title: "T", text: "bravo uniquematchword baz", cite: "https://x/doc" }, + { id: "pub#2", source: "pub", title: "T", text: "charlie epilogue", cite: "https://x/doc" }, + ]); + const svc = new KnowledgeService(store, new FtsRetriever(store)); + const t = Object.fromEntries( + makeKnowledgeTools(svc, { limit: 5, context: 1 }).map((tool) => [tool.name, tool]), + ); + const res = await t.search.handler({ query: "uniquematchword" }, ctx); + const text = res.content.map((p) => p.text).join("\n"); + // anchor labeled matched, neighbors labeled context + expect(text).toContain("matched: fts"); + expect(text).toContain("context"); + expect(text).toContain("alpha prelude"); // neighbor pub#0 + expect(text).toContain("charlie epilogue"); // neighbor pub#2 + // context=0 override suppresses neighbors + const only = await t.search.handler({ query: "uniquematchword", context: 0 }, ctx); + const onlyText = only.content.map((p) => p.text).join("\n"); + expect(onlyText).not.toContain("alpha prelude"); + }); +}); diff --git a/mcp/src/tools/knowledge.ts b/mcp/src/tools/knowledge.ts new file mode 100644 index 0000000000..6a81520b16 --- /dev/null +++ b/mcp/src/tools/knowledge.ts @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { z } from "zod"; +import type { ToolDef, ToolResult } from "../core/types.js"; +import type { Chunk, RetrievedChunk } from "../knowledge/types.js"; +import type { KnowledgeService } from "../knowledge/knowledge-service.js"; + +function formatChunk(c: Chunk, extraFooter = ""): string { + const footer = `source: ${c.cite} · id: ${c.id}${extraFooter}`; + return `## ${c.title}\n${c.text}\n\n[${footer}]`; +} + +function formatRetrievedChunk(rc: RetrievedChunk): string { + // Neighbors pulled in by context expansion aren't retriever matches — label + // them so their relevance isn't over-weighted vs. the actual anchors. + const tag = rc.role === "context" ? "context" : `matched: ${rc.matchedBy.join("+")}`; + return formatChunk(rc.chunk, ` · ${tag}`); +} + +function textResult(text: string): ToolResult { + return { content: [{ type: "text", text }] }; +} + +export interface SearchDefaults { + limit: number; + context: number; +} + +export function makeKnowledgeTools( + service: KnowledgeService, + defaults: SearchDefaults = { limit: 5, context: 0 }, +): ToolDef[] { + const search: ToolDef = { + name: "search", + description: + "Search the Ditto knowledge base (official docs plus any configured corpora) and " + + "return the most relevant documentation excerpts, each with a source URL and a chunk id. " + + "Use natural-language questions about Ditto concepts, configuration, HTTP/Ditto protocol, " + + "connectivity, policies, or operations. Each match ('matched: ...') may be followed by " + + "adjacent 'context' excerpts from the same document to preserve surrounding meaning.", + inputSchema: { + query: z + .string() + .describe( + "Natural-language search query about Ditto (e.g. 'how do policies grant access' " + + "or 'why does connectivity crash on reconnect').", + ), + limit: z + .number() + .int() + .positive() + .max(20) + .optional() + .describe( + `Maximum number of matching excerpts (anchors) to return. Default ${defaults.limit}, maximum 20. ` + + "Neighbors added by 'context' do not count against this.", + ), + context: z + .number() + .int() + .min(0) + .max(5) + .optional() + .describe( + `Adjacent same-document excerpts to include on each side of every match, for ` + + `surrounding context. Default ${defaults.context}, maximum 5. Set 0 for matches only.`, + ), + }, + handler: async (args: unknown): Promise => { + const { query, limit, context } = args as { query: string; limit?: number; context?: number }; + const hits = await service.search(query, limit ?? defaults.limit, { + context: context ?? defaults.context, + }); + if (hits.length === 0) return textResult(`No results for "${query}".`); + return textResult(hits.map(formatRetrievedChunk).join("\n\n---\n\n")); + }, + }; + + const getChunk: ToolDef = { + name: "get_chunk", + description: + "Fetch the full text of a single knowledge chunk by its id. Use an id returned by " + + "the `search` tool (shown as 'id: ', e.g. 'public#12').", + inputSchema: { + id: z + .string() + .describe("A chunk id returned by the `search` tool, e.g. 'public#12'."), + }, + handler: async (args: unknown): Promise => { + const { id } = args as { id: string }; + const c = await service.getChunk(id); + return textResult(c ? formatChunk(c) : `Chunk "${id}" not found.`); + }, + }; + + return [search, getChunk]; +} diff --git a/mcp/src/tools/ping.ts b/mcp/src/tools/ping.ts new file mode 100644 index 0000000000..181adfb272 --- /dev/null +++ b/mcp/src/tools/ping.ts @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import type { ToolDef } from "../core/types.js"; + +export const pingTool: ToolDef = { + name: "ping", + description: "Health check; returns pong", + inputSchema: {}, + handler: async () => ({ content: [{ type: "text", text: "pong" }] }), +}; diff --git a/mcp/tsconfig.json b/mcp/tsconfig.json new file mode 100644 index 0000000000..622598a3eb --- /dev/null +++ b/mcp/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "sourceMap": true + }, + "include": ["src"], + "exclude": ["dist", "node_modules", "**/*.test.ts"] +} diff --git a/mcp/vitest.config.ts b/mcp/vitest.config.ts new file mode 100644 index 0000000000..2f3dc01dd8 --- /dev/null +++ b/mcp/vitest.config.ts @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: false, + environment: "node", + include: ["src/**/*.test.ts", "src/**/*.itest.ts"], + exclude: ["**/node_modules/**", "**/*.pgtest.ts"], + testTimeout: 20000, + }, +}); diff --git a/mcp/vitest.pg.config.ts b/mcp/vitest.pg.config.ts new file mode 100644 index 0000000000..a82c52d88d --- /dev/null +++ b/mcp/vitest.pg.config.ts @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { defineConfig } from "vitest/config"; +export default defineConfig({ + test: { globals: false, environment: "node", include: ["src/**/*.pgtest.ts"], testTimeout: 120000, hookTimeout: 120000 }, +});