diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4a5bcf40..61d7de5d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -92,12 +92,23 @@ jobs: npm version "$version" --no-git-tag-version npm publish --access public + - name: Publish companion package (manual) + if: steps.release-mode.outputs.manual_release == 'true' + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + version='${{ steps.release-mode.outputs.release_version }}' + # Keep the optional Transformers.js companion in lockstep with the main package so a + # compatible version always exists on npm. prepack builds dist before publishing. + npm version "$version" --no-git-tag-version --workspace @arabold/docs-mcp-server-transformers + npm publish --workspace @arabold/docs-mcp-server-transformers + - name: Commit manual version bump if: steps.release-mode.outputs.manual_release == 'true' run: | git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add package.json package-lock.json + git add package.json package-lock.json packages/transformers/package.json git commit -m "chore(release): ${{ steps.release-mode.outputs.release_version }} [skip ci]" git push origin "HEAD:${GITHUB_REF_NAME}" diff --git a/.releaserc.json b/.releaserc.json index da6fc6dc..f9491ab2 100644 --- a/.releaserc.json +++ b/.releaserc.json @@ -9,6 +9,13 @@ "changelogFile": "CHANGELOG.md" } ], + [ + "@semantic-release/npm", + { + "npmPublish": true, + "pkgRoot": "packages/transformers" + } + ], [ "@semantic-release/npm", { @@ -19,7 +26,11 @@ [ "@semantic-release/git", { - "assets": ["package.json", "CHANGELOG.md"], + "assets": [ + "package.json", + "packages/transformers/package.json", + "CHANGELOG.md" + ], "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" } ], diff --git a/Dockerfile b/Dockerfile index 8cb88d37..e66fd38f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,8 +18,9 @@ FROM base AS builder ARG POSTHOG_API_KEY ENV POSTHOG_API_KEY=$POSTHOG_API_KEY -# Copy package files +# Copy package files (root + workspaces so `npm ci` can link the companion package) COPY package*.json ./ +COPY packages/transformers/package.json ./packages/transformers/package.json # Install all dependencies (including dev dependencies for building) RUN npm ci @@ -51,21 +52,27 @@ COPY db db COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/public ./public COPY --from=builder /app/dist ./dist +# Include the built Transformers.js companion package so the workspace symlink in +# node_modules resolves and local (offline) embeddings work out of the box. +COPY --from=builder /app/packages ./packages # Set data directory for the container ENV DOCS_MCP_STORE_PATH=/data ENV XDG_CONFIG_HOME=/config +# Cache directory for Transformers.js models (downloaded on first use). +ENV TRANSFORMERS_CACHE=/models # Create the writable runtime directories and hand ownership to the # unprivileged `node` user that ships with the base image (uid 1000). # `/app` is intentionally left root-owned so the runtime user cannot # tamper with code or `node_modules` if it is ever compromised. -RUN mkdir -p /data /config \ - && chown node:node /data /config +RUN mkdir -p /data /config /models \ + && chown node:node /data /config /models # Define volumes VOLUME /data VOLUME /config +VOLUME /models # Expose the default port of the application EXPOSE 6280 diff --git a/README.md b/README.md index 25880864..8f2b2575 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,7 @@ Using an embedding model is **optional** but dramatically improves search qualit OPENAI_API_KEY="sk-proj-..." npx @arabold/docs-mcp-server@latest ``` -See **[Embedding Models](docs/guides/embedding-models.md)** for configuring **Ollama**, **Gemini**, **Azure**, and others. +See **[Embedding Models](docs/guides/embedding-models.md)** for configuring **Ollama**, **Gemini**, **Azure**, fully **offline/local** embeddings, and others. --- @@ -142,7 +142,7 @@ See **[Embedding Models](docs/guides/embedding-models.md)** for configuring **Ol - **[Basic Usage](docs/guides/basic-usage.md)**: Using the Web UI, CLI, and scraping local files. - **[Configuration](docs/setup/configuration.md)**: Full reference for config files and environment variables. - **[Supported Formats](docs/concepts/supported-formats.md)**: Complete file format and MIME type reference. -- **[Embedding Models](docs/guides/embedding-models.md)**: Configure OpenAI, Ollama, Gemini, and other providers. +- **[Embedding Models](docs/guides/embedding-models.md)**: Configure OpenAI, Ollama, Gemini, local/offline, and other providers. - **[Search Quality Benchmark](docs/guides/benchmarking.md)**: Measure retrieval quality with IR metrics + LLM-judged scores; prerequisites, how to run, how to interpret results. ### Hash-Routed SPAs diff --git a/docs/guides/embedding-models.md b/docs/guides/embedding-models.md index ab40c2c6..3b79afc2 100644 --- a/docs/guides/embedding-models.md +++ b/docs/guides/embedding-models.md @@ -14,6 +14,7 @@ If you leave the model empty but provide `OPENAI_API_KEY`, the server defaults t - `gemini:embedding-001` (Google Gemini) - `aws:amazon.titan-embed-text-v1` (AWS Bedrock) - `microsoft:text-embedding-ada-002` (Azure OpenAI) +- `transformers:BAAI/bge-small-en-v1.5` (local, offline — requires the optional companion package) - Or any OpenAI-compatible model name ## Provider Configuration @@ -34,6 +35,8 @@ Provider credentials use the provider-specific environment variables listed belo | `AZURE_OPENAI_API_INSTANCE_NAME` | Azure OpenAI instance name. | | `AZURE_OPENAI_API_DEPLOYMENT_NAME` | Azure OpenAI deployment name. | | `AZURE_OPENAI_API_VERSION` | Azure OpenAI API version. | +| `TRANSFORMERS_DEVICE` | Device for local embeddings: `cpu` (default) or `webgpu`. | +| `TRANSFORMERS_CACHE` | Directory for caching downloaded local models. | ### Examples @@ -114,7 +117,29 @@ DOCS_MCP_EMBEDDING_MODEL="microsoft:text-embedding-ada-002" \ npx @arabold/docs-mcp-server@latest ``` -## Changing the Embedding Model +#### Local / Offline (Transformers.js) + +Generate embeddings entirely on your machine — no API key, no network calls, no data leaving your host. This uses [Transformers.js](https://huggingface.co/docs/transformers.js) with the ONNX runtime. + +Because the runtime is large, it ships as an optional **companion package** that you install alongside the server: + +```bash +npm install -g @arabold/docs-mcp-server @arabold/docs-mcp-server-transformers + +DOCS_MCP_EMBEDDING_MODEL="transformers:BAAI/bge-small-en-v1.5" \ +docs-mcp-server +``` + +Or run both with `npx` in a single command: + +```bash +DOCS_MCP_EMBEDDING_MODEL="transformers:BAAI/bge-small-en-v1.5" \ +npx -p @arabold/docs-mcp-server -p @arabold/docs-mcp-server-transformers docs-mcp-server +``` + +The model is downloaded on first use and cached (set `TRANSFORMERS_CACHE` to choose the directory). Any sentence-transformers model on Hugging Face works; the vector dimension is detected automatically. Set `TRANSFORMERS_DEVICE=webgpu` to enable GPU acceleration on supported hardware. + +> The official Docker image already bundles the companion package, so `transformers:` models work out of the box there with no extra install. When you change the embedding model or vector dimension after initial setup, existing embedding vectors become semantically incompatible with the new configuration. The server detects this automatically by tracking the active model identity in a metadata table. diff --git a/openspec/changes/add-transformers-companion/.openspec.yaml b/openspec/changes/add-transformers-companion/.openspec.yaml new file mode 100644 index 00000000..b4c82a0a --- /dev/null +++ b/openspec/changes/add-transformers-companion/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-06-06 diff --git a/openspec/changes/add-transformers-companion/design.md b/openspec/changes/add-transformers-companion/design.md new file mode 100644 index 00000000..dfddd786 --- /dev/null +++ b/openspec/changes/add-transformers-companion/design.md @@ -0,0 +1,69 @@ +## Context + +Embeddings power the server's semantic vector search. Providers so far (`openai`, `gemini`, `vertex`, `aws`, `microsoft`) are all hosted APIs: they need credentials and send chunk text off-host. Some users cannot or will not do that (air-gapped, regulated, privacy-sensitive, or simply cost-averse) and want fully local embeddings. + +Transformers.js (`@huggingface/transformers`) can run sentence-transformer models locally on the ONNX runtime. The blocker is size: `@huggingface/transformers` plus `onnxruntime-node` (which bundles native binaries for win32 + linux + darwin) and `onnxruntime-web` totals **~370 MB** installed. Adding that to the base package would roughly 10× the install for the majority of users who use hosted APIs and never touch local embeddings. + +The project ships to npm (`@arabold/docs-mcp-server`, currently a single package) and to a Docker image, and releases via semantic-release with a manual-dispatch fallback. It targets Node 22 (ESM), builds with Vite/Rollup (SSR config externalizes `dependencies`), and uses npm as the package manager. + +## Goals / Non-Goals + +**Goals:** +- Offer a fully local, offline `transformers:` embedding provider — no credentials, no network at inference time. +- Keep the default install of `@arabold/docs-mcp-server` free of the ~370 MB ONNX runtime. +- Make enabling local embeddings a single explicit action (`npm i -g @arabold/docs-mcp-server-transformers`) with a clear error when it's missing. +- Keep all integration logic in the main repo (testable, reviewable here); the external package carries only the dependency. +- Guarantee a version-compatible companion is always available on npm and pre-installed in Docker. +- Avoid any behavior or footprint change for existing non-`transformers` users. + +**Non-Goals:** +- A general plugin/extension system. This is a single, purpose-built companion, not a registry of providers. +- Bundling/vendoring the ONNX runtime into the main package or the npm tarball in any form. +- GPU support beyond exposing the existing `webgpu` device toggle. +- Pre-downloading models (they download on first use, like Playwright browsers already do). + +## Decisions + +### 1. Companion package that re-exports, loaded by name via dynamic import +The heavy dependency lives in a separate package, `@arabold/docs-mcp-server-transformers` (`packages/transformers/`), whose entire job is `export { pipeline, env } from "@huggingface/transformers"`. The main server imports **the companion by name**, never `@huggingface/transformers` directly. + +- *Why re-export instead of importing transformers directly with it marked optional?* Module resolution is deterministic: the server resolves the companion as a sibling package, and the companion resolves `@huggingface/transformers` from its own dependency subtree, regardless of how npm hoists things. The server never needs to know transformers' resolution path. +- *Why a companion at all vs. `optionalDependencies`?* `optionalDependencies` are still installed by default; they only add install-failure resilience, not size savings. They would not keep the runtime out of the base install. +- *Alternative considered — runtime auto-install (`npm i` on demand):* rejected as fragile (no network in air-gapped installs, permission issues for global installs, surprising side effects). + +### 2. Lazy dynamic import with a literal specifier +`transformersLoader.ts` does `import("@arabold/docs-mcp-server-transformers")` only on first use, caching the promise. The specifier is a **string literal** so Rollup can externalize it (a variable specifier cannot be externalized) and the import survives bundling as a real runtime `import()`. The companion is added to `vite.config.ts`'s `external` list explicitly because it is a `devDependency`, not a runtime `dependency`, so it isn't covered by the automatic `Object.keys(dependencies)` externalization. + +### 3. Structural types instead of importing transformers' types +The loader declares minimal structural types (`FeatureExtractionPipeline`, `TransformersModule`, etc.) and `TransformersJSEmbeddings.ts` imports those from the loader (a local file). The main package therefore never imports `@huggingface/transformers` even at the type level, so `tsc` and the bundler never pull it in. The companion is only required to be installed at runtime, and only when local embeddings are actually used. + +### 4. Reuse `main`'s generic dimension resolution; no transformers-specific store logic +Recent work (#431) resolves vector dimensions generically: known models via a lookup table, unknown models via a one-shot `embedQuery("test").length` probe before the vector table is created. `TransformersJSEmbeddings.embedQuery` returns a plain `number[]`, so this path handles it with no `DocumentStore` changes. We deliberately do **not** add a `transformers`-specific branch to the store. We only correct the known dimension for `BAAI/bge-small-en-v1.5` (512 → 384, its real hidden size). + +### 5. Provider requires no credentials; companion presence checked lazily +`areCredentialsAvailable("transformers")` returns `true` (local models need none). The companion's availability is verified when the model is first loaded, not at credential-check or startup time. For known-dimension models this means a missing companion surfaces at first embed rather than at init — an accepted trade-off (documented below). + +### 6. Lockstep versioning, published by the release pipeline +The companion is versioned and published in lockstep with the main package. Implementation: a second `@semantic-release/npm` instance with `pkgRoot: packages/transformers` (and the same in the manual path) publishes the companion at the release version; `prepack` builds its `dist` first; `publishConfig.access: public` covers the first publish. The main package's `devDependency` on the companion is the wildcard `"*"` so lockstep version bumps never break npm-workspace linking in dev/CI/Docker. + +- *Why lockstep vs. independent versioning?* It gives users a trivial compatibility rule ("install the same version of both") and guarantees that every server release has a matching companion on npm. + +### 7. Docker bakes the companion in +The image installs the whole workspace (`npm ci`) and copies `packages/` into the runtime stage so the workspace symlink resolves. `TRANSFORMERS_CACHE=/models` with a `/models` volume caches downloaded models across runs. + +## Risks / Trade-offs + +- **Late failure for known models** → For models in the dimension lookup table, a missing companion isn't detected at init (no probe runs), so the error appears at first actual embed. Mitigated by a clear, actionable `TransformersCompanionMissingError`. A future refinement could do a cheap existence check (e.g. `import.meta.resolve`) at init. +- **Misclassifying load errors as "companion missing"** → A broken file inside an installed companion could be mistaken for the package being absent. Mitigated: `isCompanionMissingError` only matches `ERR_MODULE_NOT_FOUND`/`MODULE_NOT_FOUND` whose message quotes the bare package specifier (`'@arabold/...'`), not an internal file path; everything else is rethrown unchanged. +- **`npx` isolation** → `npx @arabold/docs-mcp-server` will not see a globally-installed companion (separate trees). Mitigated by documenting `npm i -g ` or `npx -p `. +- **CI/dev now pulls ~370 MB** → `npm ci` installs the companion's transformers dependency for the workspace. This is necessary to build/test the companion and is acceptable (it affects CI, not end users). End-user install size is unchanged. +- **Two-package release complexity** → A second publish step adds release surface. Mitigated by the wildcard devDependency (no version-sync script needed) and `prepack` guaranteeing `dist` is built before publish. + +## Migration Plan + +- Purely additive; no data migration. Existing databases and non-`transformers` configs are unaffected. +- Rollback: revert the change. Because the companion is a `devDependency` and loaded lazily, removing it has no effect on any other provider. Users who installed the companion can simply uninstall it. + +## Open Questions + +- None blocking. Possible future refinements: an init-time companion existence check to convert late failures to early ones, and an optional Docker variant without the companion for size-sensitive deployments. diff --git a/openspec/changes/add-transformers-companion/proposal.md b/openspec/changes/add-transformers-companion/proposal.md new file mode 100644 index 00000000..8da6a4f6 --- /dev/null +++ b/openspec/changes/add-transformers-companion/proposal.md @@ -0,0 +1,32 @@ +## Why + +Users who want semantic search without sending document content to a third-party API (air-gapped, privacy-sensitive, or cost-averse setups) currently have no offline embedding option. Transformers.js can run sentence-transformer models locally, but its ONNX runtime weighs ~370 MB across all platform binaries — far too heavy to add to every install of a tool whose default users rely on hosted APIs. We need offline embeddings available to those who want them without inflating the install footprint for everyone else. + +## What Changes + +- Add a new `transformers` embedding provider that generates embeddings locally and offline, with no API key or network access required. +- Externalize the heavy `@huggingface/transformers` dependency into an **optional companion package**, `@arabold/docs-mcp-server-transformers`, kept in this repo as an npm workspace. The main server depends on it only as a wildcard `devDependency`, so a default end-user install does **not** pull in the ONNX runtime. +- Load the companion lazily via a dynamic import the first time a `transformers:` model is used. When the companion is not installed, surface an actionable error telling the user to install it; never fail at startup or for non-transformers users. +- Keep the integration code (the embeddings class, provider wiring, loader) in the main repo; the companion is a thin re-exporter only. +- Publish the companion in lockstep with the main package from the release pipeline (semantic-release and manual paths) so a version-compatible companion always exists on npm. +- Bundle the companion into the official Docker image so `transformers:` models work out of the box there, caching models under `TRANSFORMERS_CACHE=/models`. +- Fix the known vector dimension for `BAAI/bge-small-en-v1.5` from 512 to its actual 384. + +## Capabilities + +### New Capabilities +- `local-embeddings`: Offline, local embedding generation via the optional Transformers.js companion package — provider behavior, lazy companion loading, missing-companion handling, device/cache configuration, and how the companion is distributed (workspace, lockstep release, Docker). + +### Modified Capabilities +- `embedding-resolution`: The supported-provider list and credential-validation rules change — `transformers` becomes a fully supported provider (not parse-only) that requires no credentials, and the `BAAI/bge-small-en-v1.5` known-dimension entry is corrected to 384. + +## Impact + +- **New package:** `packages/transformers/` (`@arabold/docs-mcp-server-transformers`), an npm workspace re-exporting `@huggingface/transformers`. +- **New source:** `src/store/embeddings/transformersLoader.ts`, `src/store/embeddings/TransformersJSEmbeddings.ts`. +- **Modified source:** `EmbeddingFactory.ts` and `EmbeddingConfig.ts` (provider plumbing + dimension fix). +- **Build/dist:** `vite.config.ts` externalizes the companion; root `package.json` gains `workspaces` and a wildcard companion `devDependency`; the `build` script compiles the companion first. +- **Release/CI:** `.releaserc.json` and `.github/workflows/release.yml` publish the companion in lockstep. Existing `ci.yml`/`eval.yml` need no change (npm workspaces + build cover it). +- **Docker:** `Dockerfile` copies the companion and adds the `/models` cache volume. +- **Docs:** `docs/guides/embedding-models.md`, `README.md`. +- **No breaking changes** for existing users: no new runtime dependency in the default install, no behavior change for non-`transformers` providers. diff --git a/openspec/changes/add-transformers-companion/specs/embedding-resolution/spec.md b/openspec/changes/add-transformers-companion/specs/embedding-resolution/spec.md new file mode 100644 index 00000000..cf5be53d --- /dev/null +++ b/openspec/changes/add-transformers-companion/specs/embedding-resolution/spec.md @@ -0,0 +1,92 @@ +## MODIFIED Requirements + +### Requirement: Model Specification Parsing +The system SHALL parse an embedding model specification string in the format `provider:model`, splitting on the first colon only. When no colon is present, the system SHALL default to the `openai` provider and treat the entire string as the model name. This ensures model identifiers containing colons (e.g., `aws:amazon.titan-embed-text-v2:0`) are handled correctly, with only the first colon separating provider from model. + +**Supported providers:** `openai`, `vertex`, `gemini`, `aws`, `microsoft`, `transformers`, and `sagemaker` (`sagemaker` is parse-only; model creation not yet implemented). The `transformers` provider is fully supported (parsing and model creation) and produces embeddings locally via the optional companion package (see the `local-embeddings` capability). + +**Code reference:** `src/store/embeddings/EmbeddingConfig.ts:342-369` + +#### Scenario: Model string without provider prefix +- **WHEN** the model specification is `text-embedding-3-small` (no colon) +- **THEN** the system SHALL use `openai` as the provider and `text-embedding-3-small` as the model name + +#### Scenario: Model string with provider prefix +- **WHEN** the model specification is `gemini:embedding-001` +- **THEN** the system SHALL use `gemini` as the provider and `embedding-001` as the model name + +#### Scenario: Model string with multiple colons +- **WHEN** the model specification is `aws:amazon.titan-embed-text-v2:0` +- **THEN** the system SHALL split on the first colon only, using `aws` as the provider and `amazon.titan-embed-text-v2:0` as the model name + +#### Scenario: Transformers provider with a Hugging Face model +- **WHEN** the model specification is `transformers:BAAI/bge-small-en-v1.5` +- **THEN** the system SHALL use `transformers` as the provider and `BAAI/bge-small-en-v1.5` as the model name + +### Requirement: Credential Validation +The system SHALL validate that the required provider-specific credentials are available before attempting to create the embedding model. Each provider has specific required environment variables: + +| Provider | Required Variables | +|----------|-------------------| +| `openai` | `OPENAI_API_KEY` | +| `vertex` | `GOOGLE_APPLICATION_CREDENTIALS` | +| `gemini` | `GOOGLE_API_KEY` | +| `aws` | (`BEDROCK_AWS_REGION` or `AWS_REGION`) and (`AWS_PROFILE` or `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY`) | +| `microsoft` | `AZURE_OPENAI_API_KEY`, `AZURE_OPENAI_API_INSTANCE_NAME`, `AZURE_OPENAI_API_DEPLOYMENT_NAME`, `AZURE_OPENAI_API_VERSION` | +| `transformers` | None. Local embeddings require no credentials; credential validation SHALL always succeed. Availability of the optional companion package is verified lazily when the model is first loaded, not during credential validation. | +| `sagemaker` | `AWS_REGION` and (`AWS_PROFILE` or `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY`). Note: credential check is implemented but model creation is not; using this provider will result in an `UnsupportedProviderError`. | + +When credentials are missing, the system SHALL log a warning and fall back to FTS-only mode rather than raising a hard error. + +**Code reference:** `src/store/embeddings/EmbeddingFactory.ts:61-106` + +#### Scenario: Valid credentials for selected provider +- **WHEN** the embedding model is `gemini:embedding-001` +- **AND** `GOOGLE_API_KEY` is set +- **THEN** the system SHALL proceed with embedding model initialization + +#### Scenario: Missing credentials for selected provider +- **WHEN** the embedding model is `gemini:embedding-001` +- **AND** `GOOGLE_API_KEY` is not set +- **THEN** the system SHALL log a warning indicating missing credentials +- **AND** the system SHALL fall back to FTS-only mode + +#### Scenario: OpenAI with custom endpoint +- **WHEN** the embedding model uses the `openai` provider +- **AND** `OPENAI_API_KEY` is set +- **AND** `OPENAI_API_BASE` is set to a custom URL (e.g., Ollama or LM Studio) +- **THEN** the system SHALL use the custom endpoint for embedding requests + +#### Scenario: Transformers provider requires no credentials +- **WHEN** the embedding model is `transformers:BAAI/bge-small-en-v1.5` +- **AND** no provider credentials are present in the environment +- **THEN** credential validation SHALL succeed +- **AND** the system SHALL proceed with embedding model initialization + +### Requirement: Known Dimensions Lookup +The system SHALL maintain a lookup table mapping well-known embedding model names to their vector dimensions. The lookup SHALL be case-insensitive. When a model is found in the lookup table, the system SHALL use the known dimensions directly without making an API call. + +When a model is not found in the lookup table, the system SHALL generate a test embedding using the string `"test"` to detect the model's output dimensions. This detection SHALL have a configurable timeout (default 30 seconds, `embeddings.initTimeoutMs`). The detected dimensions SHALL be cached for the duration of the session. + +The lookup table is provider-agnostic (keyed by model name), so `transformers` models that appear in it resolve without a probe. The known dimension for `BAAI/bge-small-en-v1.5` SHALL be 384 (its actual hidden size). + +**Code reference:** `src/store/embeddings/EmbeddingConfig.ts:70-260`, `src/store/DocumentStore.ts:508-537` + +#### Scenario: Well-known model dimensions +- **WHEN** the embedding model is `text-embedding-3-small` +- **THEN** the system SHALL resolve the dimensions to 1536 without making any API call + +#### Scenario: Well-known transformers model dimensions +- **WHEN** the embedding model is `transformers:BAAI/bge-small-en-v1.5` +- **THEN** the system SHALL resolve the dimensions to 384 from the lookup table + +#### Scenario: Unknown model dimension detection +- **WHEN** the embedding model is not in the known dimensions lookup table +- **THEN** the system SHALL generate a test embedding of `"test"` to detect the output dimensions +- **AND** the system SHALL cache the detected dimensions for the session + +#### Scenario: Dimension detection timeout +- **WHEN** the embedding model is not in the known dimensions lookup table +- **AND** the test embedding request exceeds the initialization timeout +- **THEN** the system SHALL fail embedding initialization +- **AND** the system SHALL fall back to FTS-only mode diff --git a/openspec/changes/add-transformers-companion/specs/local-embeddings/spec.md b/openspec/changes/add-transformers-companion/specs/local-embeddings/spec.md new file mode 100644 index 00000000..c4b7c181 --- /dev/null +++ b/openspec/changes/add-transformers-companion/specs/local-embeddings/spec.md @@ -0,0 +1,84 @@ +## ADDED Requirements + +### Requirement: Local Offline Embedding Provider +The system SHALL provide a `transformers` embedding provider that generates embeddings locally using Transformers.js (the ONNX runtime), without any API key, credentials, or network access at inference time. A model is selected with the specification `transformers:` (default model `BAAI/bge-small-en-v1.5`). The provider SHALL support any sentence-transformers feature-extraction model available on Hugging Face. + +The provider SHALL produce embeddings as plain numeric vectors compatible with the generic embedding and dimension-resolution pipeline: a single query yields one vector, and a batch of documents yields one vector per document. + +**Code reference:** `src/store/embeddings/TransformersJSEmbeddings.ts`, `src/store/embeddings/EmbeddingFactory.ts:274` + +#### Scenario: Selecting a local model +- **WHEN** the embedding model is `transformers:BAAI/bge-small-en-v1.5` +- **AND** the companion package is installed +- **THEN** the system SHALL create a local Transformers.js embedding model +- **AND** the system SHALL NOT require any provider credentials or network access to embed text + +#### Scenario: Embedding a batch of documents +- **WHEN** the provider embeds an array of N documents +- **THEN** the system SHALL return N vectors, each of the model's dimension + +#### Scenario: Empty document batch +- **WHEN** the provider is asked to embed an empty array of documents +- **THEN** the system SHALL return an empty array without loading the model + +### Requirement: Optional Companion Package Distribution +The heavy `@huggingface/transformers` dependency SHALL be provided by a separate, optional companion package (`@arabold/docs-mcp-server-transformers`) rather than being a runtime dependency of the main server. A default installation of the main server SHALL NOT install the companion package or the ONNX runtime, keeping the base install small. The companion package SHALL exist as an npm workspace within this repository and SHALL only re-export the dependency surface the server needs. + +#### Scenario: Default install excludes the heavy dependency +- **WHEN** a user installs `@arabold/docs-mcp-server` without the companion +- **THEN** the installation SHALL NOT include `@huggingface/transformers` or the ONNX runtime +- **AND** all non-`transformers` providers SHALL continue to function + +#### Scenario: Enabling local embeddings +- **WHEN** a user installs `@arabold/docs-mcp-server-transformers` alongside the server in the same `node_modules` tree +- **THEN** the `transformers` provider SHALL be able to generate embeddings + +### Requirement: Lazy Companion Loading and Missing-Companion Handling +The system SHALL load the companion package lazily — only when a `transformers:` model is first used — via a dynamic import, and SHALL cache the loaded module. Importing or type-checking the main server SHALL NOT pull in the companion or its dependencies. + +When the companion package is not installed, the system SHALL raise an actionable error that instructs the user to install `@arabold/docs-mcp-server-transformers`. The system SHALL only treat the package itself being absent as a missing-companion condition; a load failure caused by a broken file inside an installed companion or a failing transitive dependency SHALL be propagated unchanged rather than reported as "companion missing". + +**Code reference:** `src/store/embeddings/transformersLoader.ts` + +#### Scenario: Companion not installed +- **WHEN** a `transformers:` model is used +- **AND** the companion package `@arabold/docs-mcp-server-transformers` is not installed +- **THEN** the system SHALL raise an error identifying the missing companion package +- **AND** the error message SHALL include installation instructions + +#### Scenario: Non-transformers usage is unaffected by a missing companion +- **WHEN** the configured provider is not `transformers` +- **THEN** the system SHALL never attempt to load the companion package +- **AND** startup SHALL not fail due to the companion being absent + +#### Scenario: Broken companion is not misreported as missing +- **WHEN** the companion package is installed but its load fails for a reason other than the package being absent (e.g., a missing internal file) +- **THEN** the system SHALL propagate the original error rather than instructing the user to install the companion + +### Requirement: Local Embedding Device and Cache Configuration +The system SHALL allow the inference device for local embeddings to be selected via the `TRANSFORMERS_DEVICE` environment variable, defaulting to `cpu` and supporting `webgpu` for GPU acceleration on compatible hardware. The system SHALL allow the model cache location to be set via the `TRANSFORMERS_CACHE` environment variable; models SHALL be downloaded on first use and reused from the cache on subsequent runs. + +**Code reference:** `src/store/embeddings/TransformersJSEmbeddings.ts`, `src/store/embeddings/EmbeddingFactory.ts:274` + +#### Scenario: Default device +- **WHEN** `TRANSFORMERS_DEVICE` is not set +- **THEN** the system SHALL run local inference on the CPU + +#### Scenario: GPU device selection +- **WHEN** `TRANSFORMERS_DEVICE` is set to `webgpu` +- **THEN** the system SHALL create the local pipeline targeting the `webgpu` device + +#### Scenario: Model cache directory +- **WHEN** `TRANSFORMERS_CACHE` is set to a directory +- **THEN** the system SHALL download and cache models under that directory before the first inference + +### Requirement: Companion Version Compatibility and Bundled Image +The companion package SHALL be published to npm in lockstep with the main server package, so that a version-compatible companion is always available for any released server version. The official Docker image SHALL include the companion package pre-installed so that `transformers:` models work without any additional installation, with a model cache directory configured. + +#### Scenario: Companion published with each release +- **WHEN** a new version of `@arabold/docs-mcp-server` is released +- **THEN** the matching version of `@arabold/docs-mcp-server-transformers` SHALL be published to npm + +#### Scenario: Docker image works offline out of the box +- **WHEN** the official Docker image runs with a `transformers:` model +- **THEN** local embeddings SHALL function without installing any additional package diff --git a/openspec/changes/add-transformers-companion/tasks.md b/openspec/changes/add-transformers-companion/tasks.md new file mode 100644 index 00000000..02f09700 --- /dev/null +++ b/openspec/changes/add-transformers-companion/tasks.md @@ -0,0 +1,45 @@ +# Tasks + +> Note: This change was implemented before the OpenSpec proposal was written, to capture the rationale and contract retroactively. All tasks below are already complete on the branch and are checked off to reflect the implemented state. + +## 1. Companion package + +- [x] 1.1 Create `packages/transformers/` npm workspace package `@arabold/docs-mcp-server-transformers` (version in lockstep with main) with `package.json`, `tsconfig.json`, and `README.md` +- [x] 1.2 Implement `src/index.ts` re-exporting `pipeline`, `env`, and the `FeatureExtractionPipeline` type from `@huggingface/transformers` +- [x] 1.3 Declare `@huggingface/transformers` as the companion's only runtime dependency; add `prepack` build and `publishConfig.access: public` + +## 2. Workspace and bundling wiring + +- [x] 2.1 Add `workspaces: ["packages/*"]` and a wildcard `devDependency` on the companion to the root `package.json` +- [x] 2.2 Make the root `build` script compile the companion before building the server +- [x] 2.3 Externalize the companion in `vite.config.ts` so the dynamic import is preserved and never bundled + +## 3. Loader and embeddings provider + +- [x] 3.1 Implement `src/store/embeddings/transformersLoader.ts`: lazy literal-string dynamic import with caching, minimal structural types, `TransformersCompanionMissingError`, and `isCompanionMissingError` (only matches the bare package specifier) +- [x] 3.2 Implement `src/store/embeddings/TransformersJSEmbeddings.ts` on top of the loader (lint-clean; sets `env.cacheDir` from `TRANSFORMERS_CACHE` before first use; empty-batch early return) + +## 4. Provider plumbing and dimension fix + +- [x] 4.1 Add `transformers` to the `EmbeddingProvider` union in `EmbeddingFactory.ts` and `EmbeddingConfig.ts`, and to the `UnsupportedProviderError` message +- [x] 4.2 Make `areCredentialsAvailable("transformers")` return true and add the `createEmbeddingModel` case (reads `TRANSFORMERS_DEVICE`) +- [x] 4.3 Correct the `BAAI/bge-small-en-v1.5` known dimension from 512 to 384 +- [x] 4.4 Confirm no `transformers`-specific branch is added to `DocumentStore` (generic dimension resolution handles it) + +## 5. Tests + +- [x] 5.1 `transformersLoader.test.ts`: error mapping (`isCompanionMissingError`) including the broken-internal-file case, and `TransformersCompanionMissingError` shape +- [x] 5.2 `TransformersJSEmbeddings.test.ts`: query/batch embedding, empty batch, dimension detection, single init, newline stripping, cache dir, device selection (loader mocked) +- [x] 5.3 Extend `EmbeddingFactory.test.ts` (transformers provider, no credentials) and `EmbeddingConfig.test.ts` (parse + 384 dimension) + +## 6. Distribution: release and Docker + +- [x] 6.1 Add a second `@semantic-release/npm` (`pkgRoot: packages/transformers`) and include the companion `package.json` in the git-commit assets in `.releaserc.json` +- [x] 6.2 Add companion bump + `npm publish --workspace` to the manual-release path in `.github/workflows/release.yml` +- [x] 6.3 Update `Dockerfile` to install the workspace, copy `packages/` into the runtime stage, and configure `TRANSFORMERS_CACHE=/models` with a `/models` volume +- [x] 6.4 Confirm `ci.yml`/`eval.yml` need no changes (verified via clean `npm ci` + build) + +## 7. Docs and verification + +- [x] 7.1 Document the local/offline provider and companion install in `docs/guides/embedding-models.md`, `README.md` +- [x] 7.2 Verify: typecheck, lint, targeted tests, full build (companion externalized, ONNX not bundled), and a companion-resolution smoke test diff --git a/package-lock.json b/package-lock.json index 59ebec5a..c2d789d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,17 @@ { "name": "@arabold/docs-mcp-server", - "version": "2.3.0", + "version": "2.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@arabold/docs-mcp-server", - "version": "2.3.0", + "version": "2.4.0", "hasInstallScript": true, "license": "MIT", + "workspaces": [ + "packages/*" + ], "dependencies": { "@alpinejs/collapse": "^3.15.8", "@fastify/formbody": "^8.0.2", @@ -79,6 +82,7 @@ "docs-mcp-server": "dist/index.js" }, "devDependencies": { + "@arabold/docs-mcp-server-transformers": "*", "@biomejs/biome": "^2.4.7", "@commitlint/cli": "^21.0.1", "@commitlint/config-conventional": "^21.0.1", @@ -193,6 +197,10 @@ "integrity": "sha512-BKNANLtNXuWYOSAnajSKLPjTsmHRNrv0ALFTbpmqt2/klHFooPhctSwkhFVPQb7rZ8BjEKHmNaBwnSbgtpk6xg==", "license": "MIT" }, + "node_modules/@arabold/docs-mcp-server-transformers": { + "resolved": "packages/transformers", + "link": true + }, "node_modules/@asamuzakjp/css-color": { "version": "5.1.11", "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", @@ -932,8 +940,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.1.1.tgz", "integrity": "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@colors/colors": { "version": "1.5.0", @@ -1307,7 +1314,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" }, @@ -1354,7 +1360,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" } @@ -2054,6 +2059,499 @@ "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/tokenizers": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@huggingface/tokenizers/-/tokenizers-0.1.3.tgz", + "integrity": "sha512-8rF/RRT10u+kn7YuUbUg0OF30K8rjTc78aHpxT+qJ1uWSqxT1MHi8+9ltwYfkFYJzT/oS+qw3JVfHtNMGAdqyA==", + "license": "Apache-2.0" + }, + "node_modules/@huggingface/transformers": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@huggingface/transformers/-/transformers-4.2.0.tgz", + "integrity": "sha512-8BRCoBMH0XsWaEIamuR0LrJGAfftgHAfb2Vrffy0VKlSAE/MnUJ5/h/zTfEP3fDIft+nk7TqB8xXEyABGitBjQ==", + "license": "Apache-2.0", + "dependencies": { + "@huggingface/jinja": "^0.5.6", + "@huggingface/tokenizers": "^0.1.3", + "onnxruntime-node": "1.24.3", + "onnxruntime-web": "1.26.0-dev.20260416-b7804b056c", + "sharp": "^0.34.5" + } + }, + "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/@inquirer/ansi": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.5.tgz", @@ -2661,7 +3159,6 @@ "resolved": "https://registry.npmjs.org/@kitajs/html/-/html-4.2.13.tgz", "integrity": "sha512-o+8e61EsoLDPTP7rsPkYolca1YFybHuxU2Lr5fWDZCUkYT/6uBlVkvnZUdCXMQKentJL9dxwpR8/xK2Q+U4LhA==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.1.3" }, @@ -2808,7 +3305,6 @@ "resolved": "https://registry.npmjs.org/@langchain/core/-/core-1.1.46.tgz", "integrity": "sha512-i8rDC83BpItxChCw4Lf+6tAr+k+OUcbirc5ZkrhI9ywYWmvxegUljLGOGYvtJNTbEAIFkhYIODPE5QRqyjF6sA==", "license": "MIT", - "peer": true, "dependencies": { "@cfworker/json-schema": "^4.0.2", "@standard-schema/spec": "^1.1.0", @@ -3267,7 +3763,6 @@ "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.3", @@ -3525,6 +4020,69 @@ "integrity": "sha512-K7STCnRG/WBE1q0BwEkIcrJB5OqECaymsQj6Hp4Ntvaek4dqHkZGfp6hxwIPqQPjlOXwidwPLo+XGsn+CoZUyw==", "license": "MIT" }, + "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/inquire": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.2.tgz", + "integrity": "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==", + "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.1", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", + "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", + "license": "BSD-3-Clause" + }, "node_modules/@rollup/plugin-node-resolve": { "version": "15.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz", @@ -5061,7 +5619,6 @@ "https://trpc.io/sponsor" ], "license": "MIT", - "peer": true, "bin": { "intent": "bin/intent.js" }, @@ -5200,9 +5757,7 @@ "version": "24.12.4", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.4.tgz", "integrity": "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -5222,7 +5777,6 @@ "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "dev": true, "license": "MIT" }, "node_modules/@types/normalize-package-data": { @@ -6140,7 +6694,6 @@ "integrity": "sha512-HdUm8EMQBLaJvGUdidNNbqpA1kYkwNcb+MYxkxCLAPJGQzlv9J0C24h8V65Z4c5GLd/JEALDvpFCQgpLJqc0zw==", "dev": true, "license": "Apache-2.0", - "peer": true, "peerDependencies": { "bare-abort-controller": "*" }, @@ -6391,6 +6944,13 @@ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "license": "ISC" }, + "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/bottleneck": { "version": "2.19.5", "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", @@ -6448,7 +7008,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -7372,7 +7931,6 @@ "integrity": "sha512-tQMagCOC59EVgNZcC5zl7XqO30Wki9i9J3acbUvkaosCT6JX3EeFwJD7Qqp4MCikRnzS18WXV3BLIQ66ytu6+Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -7470,7 +8028,6 @@ "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", @@ -7864,7 +8421,6 @@ "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==", - "dev": true, "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", @@ -7882,7 +8438,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, "license": "MIT", "dependencies": { "define-data-property": "^1.0.1", @@ -7950,6 +8505,12 @@ "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/devlop": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", @@ -8636,6 +9197,12 @@ "benchmarks" ] }, + "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.27.7", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", @@ -8851,7 +9418,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -9294,6 +9860,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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/flowbite": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/flowbite/-/flowbite-4.0.2.tgz", @@ -9797,6 +10369,23 @@ "node": ">=16 || 14 >=14.17" } }, + "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/global-directory": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-5.0.0.tgz", @@ -9817,7 +10406,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dev": true, "license": "MIT", "dependencies": { "define-properties": "^1.2.1", @@ -9921,6 +10509,12 @@ "js-yaml": "bin/js-yaml.js" } }, + "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/handlebars": { "version": "4.7.9", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", @@ -9970,7 +10564,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" @@ -10143,7 +10736,6 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.19.tgz", "integrity": "sha512-xa3eYXYXx68XTT4hZ7dRzsXBhaq85ToSrlUJNoR0gwz/1Ap/CNwX47wfvV7pc/xWhjKVVkLT7zBJy8chhNguqQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=16.9.0" } @@ -11238,7 +11830,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "dev": true, "license": "ISC" }, "node_modules/json-with-bigint": { @@ -11999,6 +12590,12 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "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/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", @@ -12081,7 +12678,6 @@ "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", "dev": true, "license": "MIT", - "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -12111,6 +12707,30 @@ "marked": ">=1 <16" } }, + "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/matcher/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/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -16319,7 +16939,6 @@ "dev": true, "inBundle": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -16608,7 +17227,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -16692,6 +17310,49 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/onnxruntime-common": { + "version": "1.24.3", + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.24.3.tgz", + "integrity": "sha512-GeuPZO6U/LBJXvwdaqHbuUmoXiEdeCjWi/EG7Y1HNnDwJYuk6WUbNXpF6luSUY8yASul3cmUlLGrCCL1ZgVXqA==", + "license": "MIT" + }, + "node_modules/onnxruntime-node": { + "version": "1.24.3", + "resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.24.3.tgz", + "integrity": "sha512-JH7+czbc8ALA819vlTgcV+Q214/+VjGeBHDjX81+ZCD0PCVCIFGFNtT0V4sXG/1JXypKPgScQcB3ij/hk3YnTg==", + "hasInstallScript": true, + "license": "MIT", + "os": [ + "win32", + "darwin", + "linux" + ], + "dependencies": { + "adm-zip": "^0.5.16", + "global-agent": "^3.0.0", + "onnxruntime-common": "1.24.3" + } + }, + "node_modules/onnxruntime-web": { + "version": "1.26.0-dev.20260416-b7804b056c", + "resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.26.0-dev.20260416-b7804b056c.tgz", + "integrity": "sha512-MD6Ss4GSpQBo6zqoJzyT9LRbKYs7x/JVN23FT24EcEvlqF4VuzPOeH6X38orZPKHQDbprn7K+SBpu0/mj2CQiw==", + "license": "MIT", + "dependencies": { + "flatbuffers": "^25.1.24", + "guid-typescript": "^1.0.9", + "long": "^5.2.3", + "onnxruntime-common": "1.24.0-dev.20251116-b39e144322", + "platform": "^1.3.6", + "protobufjs": "^7.2.4" + } + }, + "node_modules/onnxruntime-web/node_modules/onnxruntime-common": { + "version": "1.24.0-dev.20251116-b39e144322", + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.24.0-dev.20251116-b39e144322.tgz", + "integrity": "sha512-BOoomdHYmNRL5r4iQ4bMvsl2t0/hzVQ3OM3PHD0gxeXu1PmggqBv3puZicEUVOA3AtHHYmqZtjMj9FOfGrATTw==", + "license": "MIT" + }, "node_modules/openai": { "version": "6.38.0", "resolved": "https://registry.npmjs.org/openai/-/openai-6.38.0.tgz", @@ -17338,6 +17999,12 @@ "pathe": "^2.0.3" } }, + "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/playwright": { "version": "1.60.0", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", @@ -17397,7 +18064,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -17537,6 +18203,30 @@ "dev": true, "license": "ISC" }, + "node_modules/protobufjs": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.2.tgz", + "integrity": "sha512-N9EiLovGEQOJSPF26Ij7qUGvahfEnq0eeYZ02aigIedkmz1qZSwjnP9SBITHJuF/6MYbIW4HDN8zdYjsjqJKXQ==", + "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/inquire": "^1.1.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", @@ -18060,13 +18750,35 @@ "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", "license": "MIT" }, + "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/roarr/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/rollup": { "version": "4.60.4", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -18298,7 +19010,6 @@ "integrity": "sha512-WRgl5GcypwramYX4HV+eQGzUbD7UUbljVmS+5G1uMwX/wLgYuJAxGeerXJDMO2xshng4+FXqCgyB5QfClV6WjA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@semantic-release/commit-analyzer": "^13.0.1", "@semantic-release/error": "^4.0.0", @@ -18540,6 +19251,12 @@ "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/semver-regex": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-4.0.5.tgz", @@ -18579,6 +19296,33 @@ "url": "https://opencollective.com/express" } }, + "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/serialize-error/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/serve-static": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", @@ -18659,6 +19403,50 @@ "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", @@ -20016,7 +20804,6 @@ "integrity": "sha512-7dxoA6kYvtgWw80265MyqJlkRl4yawIjO7S5MigytjELkX43fV2WsAXzsNfO7sBpPPCF5Gp0+XzHk0DwLCq3xQ==", "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "node-addon-api": "^8.0.0", "node-gyp-build": "^4.8.0" @@ -20110,8 +20897,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/tunnel": { "version": "0.0.6", @@ -20278,7 +21064,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -20633,7 +21418,6 @@ "integrity": "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -21140,7 +21924,6 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", "license": "MIT", - "peer": true, "engines": { "node": ">=10.0.0" }, @@ -21356,6 +22139,14 @@ "type": "github", "url": "https://github.com/sponsors/wooorm" } + }, + "packages/transformers": { + "name": "@arabold/docs-mcp-server-transformers", + "version": "2.4.0", + "license": "MIT", + "dependencies": { + "@huggingface/transformers": "^4.2.0" + } } } } diff --git a/package.json b/package.json index ac921f6a..611f28aa 100644 --- a/package.json +++ b/package.json @@ -20,9 +20,12 @@ "LICENSE", "package.json" ], + "workspaces": [ + "packages/*" + ], "scripts": { "prepare": "husky || true", - "build": "vite build --config vite.config.web.ts && vite build", + "build": "npm run build --workspace @arabold/docs-mcp-server-transformers && vite build --config vite.config.web.ts && vite build", "start": "node --enable-source-maps dist/index.js", "cli": "node --enable-source-maps dist/index.js", "server": "node --enable-source-maps dist/index.ts", @@ -30,6 +33,7 @@ "dev": "npm-run-all --parallel dev:server dev:web", "dev:server": "vite-node --watch src/index.ts", "dev:web": "vite build --config vite.config.web.ts --watch", + "pretest": "npm run build --workspace @arabold/docs-mcp-server-transformers", "test": "vitest run --exclude test/docker-e2e.test.ts", "test:watch": "vitest", "test:coverage": "vitest run --coverage --exclude test/docker-e2e.test.ts", @@ -115,6 +119,7 @@ "zod": "^4.3.6" }, "devDependencies": { + "@arabold/docs-mcp-server-transformers": "*", "@biomejs/biome": "^2.4.7", "@commitlint/cli": "^21.0.1", "@commitlint/config-conventional": "^21.0.1", diff --git a/packages/transformers/README.md b/packages/transformers/README.md new file mode 100644 index 00000000..b89417f3 --- /dev/null +++ b/packages/transformers/README.md @@ -0,0 +1,21 @@ +# @arabold/docs-mcp-server-transformers + +Optional companion package for [`@arabold/docs-mcp-server`](https://www.npmjs.com/package/@arabold/docs-mcp-server). + +It bundles [`@huggingface/transformers`](https://www.npmjs.com/package/@huggingface/transformers) (Transformers.js + ONNX runtime) so the main server can generate embeddings **locally and offline**, without any API keys or external services. + +Because this dependency is large (the ONNX runtime ships native binaries for all platforms), it is kept out of the default server install. Install this companion only if you want to use the `transformers:` embedding provider: + +```bash +npm install -g @arabold/docs-mcp-server @arabold/docs-mcp-server-transformers +``` + +Then select a local model: + +```bash +DOCS_MCP_EMBEDDING_MODEL="transformers:BAAI/bge-small-en-v1.5" docs-mcp-server +``` + +The official Docker image already includes this package, so no extra step is needed there. + +> Install both packages into the **same** `node_modules` tree (e.g. both global, or both local in the same project). The server resolves this companion as a sibling package. diff --git a/packages/transformers/package.json b/packages/transformers/package.json new file mode 100644 index 00000000..0b4a6ae6 --- /dev/null +++ b/packages/transformers/package.json @@ -0,0 +1,28 @@ +{ + "name": "@arabold/docs-mcp-server-transformers", + "version": "2.4.0", + "description": "Optional companion package providing local, offline embeddings (Transformers.js / ONNX runtime) for @arabold/docs-mcp-server. Install this alongside the server to enable the `transformers:` embedding provider.", + "type": "module", + "main": "./dist/index.js", + "types": "./src/index.ts", + "exports": { + ".": { + "types": "./src/index.ts", + "import": "./dist/index.js" + } + }, + "files": ["dist", "src"], + "publishConfig": { + "access": "public" + }, + "scripts": { + "build": "tsc -p tsconfig.json", + "prepack": "npm run build", + "clean": "rm -rf dist" + }, + "keywords": ["docs-mcp-server", "embeddings", "transformers.js", "onnx", "offline"], + "license": "MIT", + "dependencies": { + "@huggingface/transformers": "^4.2.0" + } +} diff --git a/packages/transformers/src/index.ts b/packages/transformers/src/index.ts new file mode 100644 index 00000000..5fcc9319 --- /dev/null +++ b/packages/transformers/src/index.ts @@ -0,0 +1,16 @@ +/** + * Optional companion package for `@arabold/docs-mcp-server`. + * + * This package exists only to carry and re-export the heavy `@huggingface/transformers` + * dependency (ONNX runtime, ~hundreds of MB) so that the main server install stays small for + * users who do not need local embeddings. The main server imports this package lazily and + * only when a `transformers:` embedding model is selected. + * + * Re-exporting (rather than having the server depend on `@huggingface/transformers` directly) + * keeps module resolution deterministic: the server imports this companion by name, and the + * companion resolves `@huggingface/transformers` from its own dependency tree regardless of + * how the package manager hoists or nests it. + */ + +export type { FeatureExtractionPipeline } from "@huggingface/transformers"; +export { env, pipeline } from "@huggingface/transformers"; diff --git a/packages/transformers/tsconfig.json b/packages/transformers/tsconfig.json new file mode 100644 index 00000000..15671786 --- /dev/null +++ b/packages/transformers/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "declaration": true, + "outDir": "dist", + "rootDir": "src", + "strict": true, + "skipLibCheck": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src"] +} diff --git a/src/store/embeddings/EmbeddingConfig.test.ts b/src/store/embeddings/EmbeddingConfig.test.ts index 7b3a054f..5f0da125 100644 --- a/src/store/embeddings/EmbeddingConfig.test.ts +++ b/src/store/embeddings/EmbeddingConfig.test.ts @@ -75,6 +75,17 @@ describe("EmbeddingConfig", () => { expect(result.dimensions).toBe(1536); // Should find the lowercase version expect(result.modelSpec).toBe("openai:TEXT-EMBEDDING-3-SMALL"); }); + + it("should parse the transformers provider with its local model dimensions", () => { + const config = new EmbeddingConfig(); + const result = config.parse("transformers:BAAI/bge-small-en-v1.5"); + + expect(result.provider).toBe("transformers"); + expect(result.model).toBe("BAAI/bge-small-en-v1.5"); + // bge-small-en-v1.5 has a hidden size of 384, not 512. + expect(result.dimensions).toBe(384); + expect(result.modelSpec).toBe("transformers:BAAI/bge-small-en-v1.5"); + }); }); describe("getKnownDimensions", () => { diff --git a/src/store/embeddings/EmbeddingConfig.ts b/src/store/embeddings/EmbeddingConfig.ts index 515960a1..7bf89870 100644 --- a/src/store/embeddings/EmbeddingConfig.ts +++ b/src/store/embeddings/EmbeddingConfig.ts @@ -18,7 +18,8 @@ export type EmbeddingProvider = | "gemini" | "aws" | "microsoft" - | "sagemaker"; + | "sagemaker" + | "transformers"; /** * Embedding model configuration parsed from environment variables. @@ -203,7 +204,7 @@ export class EmbeddingConfig { "intfloat/e5-base": 768, "sentence-transformers/static-similarity-mrl-multilingual-v1": 1024, "manu/sentence_croissant_alpha_v0.3": 2048, - "BAAI/bge-small-en-v1.5": 512, + "BAAI/bge-small-en-v1.5": 384, "thenlper/gte-small": 384, "sdadas/mmlw-e5-small": 384, "manu/sentence_croissant_alpha_v0.4": 2048, @@ -333,6 +334,7 @@ export class EmbeddingConfig { * - aws: AWS Bedrock models * - microsoft: Azure OpenAI * - sagemaker: AWS SageMaker hosted models + * - transformers: Local, offline embeddings via the Transformers.js companion package * * @param modelSpec Model specification (e.g., "openai:text-embedding-3-small"), defaults to "text-embedding-3-small" * @returns Parsed embedding model configuration diff --git a/src/store/embeddings/EmbeddingFactory.test.ts b/src/store/embeddings/EmbeddingFactory.test.ts index f78547ec..69f56e4b 100644 --- a/src/store/embeddings/EmbeddingFactory.test.ts +++ b/src/store/embeddings/EmbeddingFactory.test.ts @@ -8,6 +8,7 @@ import { sanitizeEnvironment } from "../../utils/env"; import { MissingCredentialsError } from "../errors"; import { createEmbeddingModel, UnsupportedProviderError } from "./EmbeddingFactory"; import { FixedDimensionEmbeddings } from "./FixedDimensionEmbeddings"; +import { TransformersJSEmbeddings } from "./TransformersJSEmbeddings"; // Suppress logger output during tests @@ -148,6 +149,16 @@ describe("createEmbeddingModel", () => { ); }); + test("should create Transformers.js embeddings without requiring credentials", () => { + // Local embeddings need no API keys, so an empty environment must still work. + vi.stubGlobal("process", { env: {} }); + const model = createEmbeddingModel( + "transformers:BAAI/bge-small-en-v1.5", + runtimeConfig, + ); + expect(model).toBeInstanceOf(TransformersJSEmbeddings); + }); + test("should throw MissingCredentialsError for Azure OpenAI without required env vars", () => { // Override env to simulate missing Azure variables vi.stubGlobal("process", { diff --git a/src/store/embeddings/EmbeddingFactory.ts b/src/store/embeddings/EmbeddingFactory.ts index 95c3108d..a4139ee9 100644 --- a/src/store/embeddings/EmbeddingFactory.ts +++ b/src/store/embeddings/EmbeddingFactory.ts @@ -11,6 +11,7 @@ import { import type { AppConfig } from "../../utils/config"; import { MissingCredentialsError } from "../errors"; import { FixedDimensionEmbeddings } from "./FixedDimensionEmbeddings"; +import { TransformersJSEmbeddings } from "./TransformersJSEmbeddings"; /** * Supported embedding model providers. Each provider requires specific environment @@ -22,7 +23,8 @@ export type EmbeddingProvider = | "gemini" | "aws" | "microsoft" - | "sagemaker"; + | "sagemaker" + | "transformers"; /** * Error thrown when an invalid or unsupported embedding provider is specified. @@ -31,7 +33,7 @@ export class UnsupportedProviderError extends Error { constructor(provider: string) { super( `❌ Unsupported embedding provider: ${provider}\n` + - " Supported providers: openai, vertex, gemini, aws, microsoft, sagemaker\n" + + " Supported providers: openai, vertex, gemini, aws, microsoft, sagemaker, transformers\n" + " See README.md for configuration options or run with --help for more details.", ); this.name = "UnsupportedProviderError"; @@ -93,6 +95,11 @@ export function areCredentialsAvailable(provider: EmbeddingProvider): boolean { ); } + case "transformers": + // Local, offline embeddings require no credentials. Availability of the optional + // companion package is verified lazily when the model is first used. + return true; + default: return false; } @@ -264,6 +271,16 @@ export function createEmbeddingModel( }); } + case "transformers": { + // Local, offline embeddings via the Transformers.js companion package. + // The device can be overridden with the TRANSFORMERS_DEVICE environment variable. + const envDevice = process.env.TRANSFORMERS_DEVICE?.toLowerCase(); + return new TransformersJSEmbeddings({ + modelName: model, + device: envDevice === "webgpu" ? "webgpu" : "cpu", + }); + } + default: throw new UnsupportedProviderError(provider); } diff --git a/src/store/embeddings/TransformersJSEmbeddings.test.ts b/src/store/embeddings/TransformersJSEmbeddings.test.ts new file mode 100644 index 00000000..123a723d --- /dev/null +++ b/src/store/embeddings/TransformersJSEmbeddings.test.ts @@ -0,0 +1,129 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +// Mock the lazy loader so tests never touch the real (heavy) companion package. +const pipelineFactory = vi.fn(); +const env: { cacheDir?: string } = {}; + +vi.mock("./transformersLoader", () => ({ + loadTransformers: vi.fn(async () => ({ + pipeline: pipelineFactory, + env, + })), +})); + +import { TransformersJSEmbeddings } from "./TransformersJSEmbeddings"; + +const DIM = 4; + +/** + * Builds a fake feature-extraction pipeline that returns deterministic vectors. + * For a single string it returns one vector; for an array it returns a flat tensor + * of `inputs.length * DIM` numbers with dims `[n, DIM]`, mirroring Transformers.js. + */ +function makeFakeEncoder() { + return vi.fn(async (input: string | string[]) => { + const count = Array.isArray(input) ? input.length : 1; + const data = new Float32Array(count * DIM); + for (let i = 0; i < data.length; i++) { + data[i] = i; // deterministic, easy to assert against + } + return { data, dims: [count, DIM] }; + }); +} + +beforeEach(() => { + pipelineFactory.mockReset(); + env.cacheDir = undefined; + delete process.env.TRANSFORMERS_CACHE; +}); + +describe("TransformersJSEmbeddings", () => { + it("embeds a single query into a vector of the model dimension", async () => { + const encoder = makeFakeEncoder(); + pipelineFactory.mockResolvedValue(encoder); + + const embeddings = new TransformersJSEmbeddings({ modelName: "test-model" }); + const vector = await embeddings.embedQuery("hello"); + + expect(vector).toEqual([0, 1, 2, 3]); + expect(pipelineFactory).toHaveBeenCalledWith("feature-extraction", "test-model", { + device: "cpu", + }); + }); + + it("embeds multiple documents into separate vectors", async () => { + const encoder = makeFakeEncoder(); + pipelineFactory.mockResolvedValue(encoder); + + const embeddings = new TransformersJSEmbeddings(); + const vectors = await embeddings.embedDocuments(["a", "b"]); + + expect(vectors).toEqual([ + [0, 1, 2, 3], + [4, 5, 6, 7], + ]); + }); + + it("returns an empty array for no documents without invoking the model", async () => { + pipelineFactory.mockResolvedValue(makeFakeEncoder()); + + const embeddings = new TransformersJSEmbeddings(); + const vectors = await embeddings.embedDocuments([]); + + expect(vectors).toEqual([]); + expect(pipelineFactory).not.toHaveBeenCalled(); + }); + + it("auto-detects the vector dimension from the model", async () => { + pipelineFactory.mockResolvedValue(makeFakeEncoder()); + + const embeddings = new TransformersJSEmbeddings(); + await expect(embeddings.getVectorDimension()).resolves.toBe(DIM); + }); + + it("initializes the pipeline only once across calls", async () => { + pipelineFactory.mockResolvedValue(makeFakeEncoder()); + + const embeddings = new TransformersJSEmbeddings(); + await embeddings.embedQuery("one"); + await embeddings.embedQuery("two"); + + expect(pipelineFactory).toHaveBeenCalledTimes(1); + }); + + it("strips newlines from input by default", async () => { + const encoder = makeFakeEncoder(); + pipelineFactory.mockResolvedValue(encoder); + + const embeddings = new TransformersJSEmbeddings(); + await embeddings.embedQuery("line1\nline2"); + + expect(encoder).toHaveBeenCalledWith("line1 line2", expect.anything()); + }); + + it("honors TRANSFORMERS_CACHE by setting the shared cacheDir before first use", async () => { + process.env.TRANSFORMERS_CACHE = "/tmp/models"; + pipelineFactory.mockResolvedValue(makeFakeEncoder()); + + const embeddings = new TransformersJSEmbeddings(); + await embeddings.embedQuery("hello"); + + expect(env.cacheDir).toBe("/tmp/models"); + }); + + it("selects the webgpu device when TRANSFORMERS_DEVICE is set", async () => { + process.env.TRANSFORMERS_DEVICE = "webgpu"; + try { + pipelineFactory.mockResolvedValue(makeFakeEncoder()); + const embeddings = new TransformersJSEmbeddings(); + await embeddings.embedQuery("hello"); + expect(pipelineFactory).toHaveBeenCalledWith( + "feature-extraction", + expect.any(String), + { device: "webgpu" }, + ); + } finally { + delete process.env.TRANSFORMERS_DEVICE; + } + }); +}); diff --git a/src/store/embeddings/TransformersJSEmbeddings.ts b/src/store/embeddings/TransformersJSEmbeddings.ts new file mode 100644 index 00000000..35d842ae --- /dev/null +++ b/src/store/embeddings/TransformersJSEmbeddings.ts @@ -0,0 +1,169 @@ +import { Embeddings } from "@langchain/core/embeddings"; +import { type FeatureExtractionPipeline, loadTransformers } from "./transformersLoader"; + +/** + * Configuration options for Transformers.js embeddings. + */ +export interface TransformersJSEmbeddingsParams { + /** + * Model to use for embeddings. Defaults to BAAI/bge-small-en-v1.5. + * Supports any sentence-transformers model available on HuggingFace. + */ + modelName?: string; + + /** + * Device to run inference on. Defaults to "cpu". + * Set to "webgpu" to enable GPU acceleration (requires compatible hardware). + * Can also be set via TRANSFORMERS_DEVICE environment variable. + */ + device?: "cpu" | "webgpu"; + + /** + * Whether to normalize embeddings to unit length. Defaults to true. + */ + normalize?: boolean; + + /** + * Whether to strip newlines from input text. Defaults to true. + */ + stripNewLines?: boolean; +} + +/** + * Transformers.js-based embeddings implementation using the ONNX runtime. + * Provides offline, local embedding generation without external API dependencies. + * + * The heavy `@huggingface/transformers` dependency is provided by the optional + * `@arabold/docs-mcp-server-transformers` companion package and loaded lazily on first use. + * + * @example + * ```typescript + * const embeddings = new TransformersJSEmbeddings({ + * modelName: "BAAI/bge-small-en-v1.5", + * normalize: true, + * }); + * + * const vector = await embeddings.embedQuery("Hello world"); + * const vectors = await embeddings.embedDocuments(["Doc 1", "Doc 2"]); + * ``` + */ +export class TransformersJSEmbeddings extends Embeddings { + private readonly modelName: string; + private readonly device: "cpu" | "webgpu"; + private readonly normalize: boolean; + private readonly stripNewLines: boolean; + + private encoderPromise: Promise | null = null; + private vectorDimension: number | null = null; + + constructor(params: TransformersJSEmbeddingsParams = {}) { + super({}); + + this.modelName = params.modelName || "BAAI/bge-small-en-v1.5"; + + const envDevice = process.env.TRANSFORMERS_DEVICE?.toLowerCase(); + this.device = params.device ?? (envDevice === "webgpu" ? "webgpu" : "cpu"); + + this.normalize = params.normalize ?? true; + this.stripNewLines = params.stripNewLines ?? true; + } + + /** + * Lazily initializes the embedding pipeline. + * Loads the companion package and downloads/caches the model on first use. + * + * @throws {TransformersCompanionMissingError} If the companion package is not installed. + */ + private async getEncoder(): Promise { + if (this.encoderPromise === null) { + this.encoderPromise = (async () => { + const { env, pipeline } = await loadTransformers(); + + // `env.cacheDir` is a singleton shared across all pipeline instances, so it must be + // set before any `pipeline()` call. Done lazily so importing this module never eagerly + // loads Transformers.js when local embeddings are not used. + const cacheDir = process.env.TRANSFORMERS_CACHE; + if (cacheDir) { + env.cacheDir = cacheDir; + } + + return pipeline("feature-extraction", this.modelName, { + device: this.device, + }); + })(); + } + + return this.encoderPromise; + } + + /** + * Gets the vector dimension for this model. Auto-detects on first inference. + */ + async getVectorDimension(): Promise { + if (this.vectorDimension !== null) { + return this.vectorDimension; + } + + const encoder = await this.getEncoder(); + const testOutput = await encoder("test", { + pooling: "mean", + normalize: this.normalize, + }); + + this.vectorDimension = testOutput.dims[1]; + return this.vectorDimension; + } + + /** + * Embeds a single query text. + * + * @param text The text to embed. + * @returns Promise resolving to the embedding vector. + */ + async embedQuery(text: string): Promise { + const processedText = this.stripNewLines ? text.replaceAll(/\n/g, " ") : text; + const encoder = await this.getEncoder(); + + const output = await encoder(processedText, { + pooling: "mean", + normalize: this.normalize, + }); + + return Array.from(output.data); + } + + /** + * Embeds multiple documents in a single batch. + * + * @param documents Array of texts to embed. + * @returns Promise resolving to an array of embedding vectors. + */ + async embedDocuments(documents: string[]): Promise { + if (documents.length === 0) { + return []; + } + + const processedDocs = this.stripNewLines + ? documents.map((doc) => doc.replaceAll(/\n/g, " ")) + : documents; + + const encoder = await this.getEncoder(); + + const output = await encoder(processedDocs, { + pooling: "mean", + normalize: this.normalize, + }); + + const tensor = output.data; + const dimension = output.dims[1]; + const vectors: number[][] = []; + + for (let i = 0; i < documents.length; i++) { + const start = i * dimension; + const end = start + dimension; + vectors.push(Array.from(tensor.slice(start, end))); + } + + return vectors; + } +} diff --git a/src/store/embeddings/transformersLoader.test.ts b/src/store/embeddings/transformersLoader.test.ts new file mode 100644 index 00000000..1cc9effa --- /dev/null +++ b/src/store/embeddings/transformersLoader.test.ts @@ -0,0 +1,73 @@ +import { describe, expect, it } from "vitest"; +import { + isCompanionMissingError, + TransformersCompanionMissingError, +} from "./transformersLoader"; + +describe("isCompanionMissingError", () => { + const COMPANION = "@arabold/docs-mcp-server-transformers"; + + it("returns true for ERR_MODULE_NOT_FOUND referencing the companion", () => { + const error = Object.assign( + new Error(`Cannot find package '${COMPANION}' imported from ...`), + { code: "ERR_MODULE_NOT_FOUND" }, + ); + expect(isCompanionMissingError(error)).toBe(true); + }); + + it("returns true for MODULE_NOT_FOUND referencing the companion", () => { + const error = Object.assign(new Error(`Cannot find module '${COMPANION}'`), { + code: "MODULE_NOT_FOUND", + }); + expect(isCompanionMissingError(error)).toBe(true); + }); + + it("returns false when a different module is missing", () => { + // A broken transitive dependency must not be reported as the companion missing. + const error = Object.assign(new Error("Cannot find module 'some-other-dep'"), { + code: "ERR_MODULE_NOT_FOUND", + }); + expect(isCompanionMissingError(error)).toBe(false); + }); + + it("returns false when an internal companion file is missing (not the package)", () => { + // A broken/missing file inside an installed companion quotes a file path, not the bare + // package specifier, and must be rethrown rather than reported as "install the companion". + const error = Object.assign( + new Error( + `Cannot find module '/app/node_modules/${COMPANION}/dist/index.js' imported from ...`, + ), + { code: "ERR_MODULE_NOT_FOUND" }, + ); + expect(isCompanionMissingError(error)).toBe(false); + }); + + it("returns false for unrelated error codes even if message mentions the companion", () => { + const error = Object.assign(new Error(`boom in ${COMPANION}`), { + code: "ERR_SOMETHING_ELSE", + }); + expect(isCompanionMissingError(error)).toBe(false); + }); + + it("returns false for non-object inputs", () => { + expect(isCompanionMissingError(undefined)).toBe(false); + expect(isCompanionMissingError(null)).toBe(false); + expect(isCompanionMissingError("error")).toBe(false); + }); +}); + +describe("TransformersCompanionMissingError", () => { + it("has a helpful name and install instructions", () => { + const error = new TransformersCompanionMissingError(); + expect(error).toBeInstanceOf(Error); + expect(error.name).toBe("TransformersCompanionMissingError"); + expect(error.message).toContain("@arabold/docs-mcp-server-transformers"); + expect(error.message).toContain("npm install"); + }); + + it("preserves the underlying cause", () => { + const cause = new Error("original"); + const error = new TransformersCompanionMissingError(cause); + expect(error.cause).toBe(cause); + }); +}); diff --git a/src/store/embeddings/transformersLoader.ts b/src/store/embeddings/transformersLoader.ts new file mode 100644 index 00000000..53864db0 --- /dev/null +++ b/src/store/embeddings/transformersLoader.ts @@ -0,0 +1,107 @@ +/** + * Lazy loader for the optional `@arabold/docs-mcp-server-transformers` companion package. + * + * Local embeddings rely on `@huggingface/transformers`, whose native ONNX runtime is large. + * To keep the default server install lightweight, that dependency lives in a separate + * companion package which is only loaded when a `transformers:` model is selected. + * + * The main server never imports `@huggingface/transformers` (or the companion) at module load + * time, not even its types: the structural types below describe the minimal surface we use, so + * type-checking and bundling never pull in the heavy package. + */ + +/** Minimal structural type for a feature-extraction tensor returned by Transformers.js. */ +export interface FeatureExtractionTensor { + data: Float32Array | number[]; + dims: number[]; +} + +/** Minimal structural type for the feature-extraction pipeline used by this server. */ +export type FeatureExtractionPipeline = ( + input: string | string[], + options?: { pooling?: "mean" | "cls" | "none"; normalize?: boolean }, +) => Promise; + +/** Minimal surface of the companion module consumed by this server. */ +export interface TransformersModule { + pipeline: ( + task: "feature-extraction", + model: string, + options?: { device?: "cpu" | "webgpu" }, + ) => Promise; + /** Shared, mutable Transformers.js environment singleton (used to configure `cacheDir`). */ + env: { cacheDir?: string } & Record; +} + +const COMPANION_PACKAGE = "@arabold/docs-mcp-server-transformers"; + +/** + * Error thrown when the optional Transformers.js companion package is not installed. + */ +export class TransformersCompanionMissingError extends Error { + constructor(cause?: unknown) { + super( + `❌ Local embeddings require the optional companion package "${COMPANION_PACKAGE}".\n` + + " Install it alongside the server, e.g.:\n" + + ` npm install -g @arabold/docs-mcp-server ${COMPANION_PACKAGE}\n` + + " (The official Docker image already includes it.)", + { cause }, + ); + this.name = "TransformersCompanionMissingError"; + } +} + +let modulePromise: Promise | null = null; + +/** + * Dynamically loads the Transformers.js companion package. + * Caches the import promise so the heavy module is initialized at most once. + * + * @returns The minimal companion module surface (`pipeline`, `env`). + * @throws {TransformersCompanionMissingError} If the companion package is not installed. + */ +export async function loadTransformers(): Promise { + if (modulePromise === null) { + // The specifier must be a string literal so the bundler can externalize it and preserve + // the dynamic import instead of trying to bundle the companion. + modulePromise = import("@arabold/docs-mcp-server-transformers") + .then((mod) => mod as unknown as TransformersModule) + .catch((error: unknown) => { + modulePromise = null; // allow a later retry (e.g. after the user installs it) + if (isCompanionMissingError(error)) { + throw new TransformersCompanionMissingError(error); + } + throw error; + }); + } + return modulePromise; +} + +/** + * Resets the cached companion import. Intended for tests only. + */ +export function resetTransformersLoader(): void { + modulePromise = null; +} + +/** + * Determines whether a dynamic-import error means the companion package itself is missing, + * as opposed to a broken file inside an installed companion or a failing transitive + * dependency. Only a truly-missing package should be reported as "install the companion". + * + * A missing bare specifier produces a Node error whose message quotes the exact package name + * (e.g. `Cannot find package '@arabold/...'`). A broken internal file instead quotes a file + * path, and a missing export path uses a different code (`ERR_PACKAGE_PATH_NOT_EXPORTED`), + * so both are correctly excluded here and rethrown unchanged. + */ +export function isCompanionMissingError(error: unknown): boolean { + if (typeof error !== "object" || error === null) { + return false; + } + const code = (error as { code?: string }).code; + const message = (error as { message?: string }).message ?? ""; + const isModuleNotFound = code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND"; + // Require the package name to appear as a quoted bare specifier, not merely as a substring + // (which would also match an internal file path under the companion's directory). + return isModuleNotFound && message.includes(`'${COMPANION_PACKAGE}'`); +} diff --git a/vite.config.ts b/vite.config.ts index 6b7ac05b..35613037 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -54,6 +54,10 @@ export default defineConfig({ external: [ /^node:/, // Externalize all node built-ins (e.g., 'node:fs', 'node:path') ...Object.keys(packageJson.dependencies || {}), + // Optional Transformers.js companion package. It is loaded via a dynamic import only + // when local embeddings are used, and must never be bundled. It is a devDependency + // (not a runtime dependency) so it is not covered by the dependencies list above. + /^@arabold\/docs-mcp-server-transformers(\/.*)?$/, // Explicitly externalize potentially problematic packages if needed 'fingerprint-generator', 'header-generator',