Эх сурвалжийг харах

Enforce commercial-only QMD model policy

root 3 долоо хоног өмнө
parent
commit
332ba839b2

+ 41 - 38
README.md

@@ -1,10 +1,8 @@
 # QMD - Query Markup Documents
 
-An on-device search engine for everything you need to remember. Index your markdown notes, meeting transcripts, documentation, and knowledge bases. Search with keywords or natural language. Ideal for your agentic flows.
+Search everything you need to remember. QMD indexes markdown notes, meeting transcripts, documentation, and knowledge bases, then exposes deterministic keyword search and optional semantic retrieval to agentic workflows.
 
-QMD combines BM25 full-text search, vector semantic search, and LLM re-ranking—all running locally via node-llama-cpp with GGUF models.
-
-![QMD Architecture](assets/qmd-architecture.png)
+QMD runs BM25 and SQLite indexing locally. Every learned operation, including embeddings, requires an approved commercial HTTPS API. QMD never downloads model weights, loads GGUF files, or falls back to local inference; missing or invalid commercial configuration returns a typed `QMD_COMMERCIAL_API_HOLD`.
 
 You can read more about QMD's progress in the [CHANGELOG](CHANGELOG.md).
 
@@ -30,7 +28,13 @@ qmd context add qmd://notes "Personal notes and ideas"
 qmd context add qmd://meetings "Meeting transcripts and notes"
 qmd context add qmd://docs "Work documentation"
 
-# Generate embeddings for semantic search
+# Configure the approved commercial embedding gateway
+export QMD_EMBED_ENDPOINT="https://gateway.example.com/v1/embeddings"
+export QMD_EMBED_API_KEY="..."
+export QMD_EMBED_MODEL_ID="approved-embedding-contract-v1"
+export QMD_EMBED_UPSTREAM_MODEL="gemini-embedding-001"
+
+# Generate embeddings for semantic search through that gateway
 qmd embed
 
 # Search across everything
@@ -114,7 +118,7 @@ Or configure MCP manually in `~/.claude/settings.json`:
 
 #### HTTP Transport
 
-By default, QMD's MCP server uses stdio (launched as a subprocess by each client). For a shared, long-lived server that avoids repeated model loading, use the HTTP transport:
+By default, QMD's MCP server uses stdio (launched as a subprocess by each client). For a shared, long-lived process with connection reuse, use the HTTP transport:
 
 ```sh
 # Foreground (Ctrl-C to stop)
@@ -131,7 +135,7 @@ The HTTP server exposes two endpoints:
 - `POST /mcp` — MCP Streamable HTTP (JSON responses, stateless)
 - `GET /health` — liveness check with uptime
 
-LLM models stay loaded in VRAM across requests. Embedding/reranking contexts are disposed after 5 min idle and transparently recreated on the next request (~1s penalty, models remain loaded).
+The MCP process does not load model weights. It resolves the commercial provider lazily: health, BM25, status, and document retrieval remain available without credentials, while semantic operations return typed `HOLD` until an approved provider contract is configured.
 
 Point any MCP client at `http://localhost:8181/mcp` to connect.
 
@@ -481,36 +485,32 @@ The `query` command uses **Reciprocal Rank Fusion (RRF)** with position-aware bl
   brew install sqlite
   ```
 
-### GGUF Models (via node-llama-cpp)
+### Commercial Model API
 
-QMD uses three local GGUF models (auto-downloaded on first use):
+QMD does not support local, self-hosted, or on-prem model execution. Semantic retrieval requires a purpose-approved commercial API reachable through HTTPS. Configure at least:
 
-| Model | Purpose | Size |
-|-------|---------|------|
-| `embeddinggemma-300M-Q8_0` | Vector embeddings (default) | ~300MB |
-| `qwen3-reranker-0.6b-q8_0` | Re-ranking | ~640MB |
-| `qmd-query-expansion-1.7B-q4_k_m` | Query expansion (fine-tuned) | ~1.1GB |
+```sh
+export QMD_EMBED_ENDPOINT="https://gateway.example.com/v1/embeddings"
+export QMD_EMBED_API_KEY="..."
+export QMD_EMBED_MODEL_ID="approved-embedding-contract-v1"
+export QMD_EMBED_UPSTREAM_MODEL="gemini-embedding-001"
+```
 
-Models are downloaded from HuggingFace and cached in `~/.cache/qmd/models/`.
+The endpoint must be public HTTPS and must represent a contracted commercial provider or central organizational gateway. Loopback, private-network, non-TLS, and local fallback endpoints are rejected.
 
 ### Custom Embedding Model
 
-Override the default embedding model via the `QMD_EMBED_MODEL` environment variable.
-This is useful for multilingual corpora (e.g. Chinese, Japanese, Korean) where
-`embeddinggemma-300M` has limited coverage.
+Select the registered upstream commercial model with `QMD_EMBED_UPSTREAM_MODEL` and bind its stable vector identity with `QMD_EMBED_MODEL_ID`.
 
 ```sh
-# Use Qwen3-Embedding-0.6B for better multilingual (CJK) support
-export QMD_EMBED_MODEL="hf:Qwen/Qwen3-Embedding-0.6B-GGUF/Qwen3-Embedding-0.6B-Q8_0.gguf"
+# Example: paid Gemini through the approved gateway
+export QMD_EMBED_MODEL_ID="gemini-embedding-001-v1"
+export QMD_EMBED_UPSTREAM_MODEL="gemini-embedding-001"
 
 # After changing the model, re-embed all collections:
 qmd embed -f
 ```
 
-Supported model families:
-- **embeddinggemma** (default) — English-optimized, small footprint
-- **Qwen3-Embedding** — Multilingual (119 languages including CJK), MTEB top-ranked
-
 > **Note:** When switching embedding models, you must re-index with `qmd embed -f`
 > since vectors are not cross-compatible between models. The prompt format is
 > automatically adjusted for each model family.
@@ -560,6 +560,11 @@ qmd ls notes/subfolder
 ### Generate Vector Embeddings
 
 ```sh
+# Required commercial provider contract
+export QMD_EMBED_ENDPOINT="https://gateway.example.com/v1/embeddings"
+export QMD_EMBED_API_KEY="..."
+export QMD_EMBED_MODEL_ID="approved-embedding-contract-v1"
+
 # Embed all indexed documents (900 tokens/chunk, 15% overlap)
 qmd embed
 
@@ -797,6 +802,12 @@ llm_cache       -- Cached LLM responses (query expansion, rerank scores)
 | Variable | Default | Description |
 |----------|---------|-------------|
 | `XDG_CACHE_HOME` | `~/.cache` | Cache directory location |
+| `QMD_EMBED_ENDPOINT` | none | Approved commercial HTTPS embedding endpoint; required for semantic operations |
+| `QMD_EMBED_API_KEY` | none | Commercial API credential |
+| `QMD_EMBED_MODEL_ID` | `embeddinggemma` | Stable vector identity stored in the index; pin explicitly in production |
+| `QMD_EMBED_UPSTREAM_MODEL` | model ID | Registered provider model sent to the commercial gateway |
+| `QMD_EMBED_BATCH_SIZE` | `64` | Maximum texts per commercial API batch |
+| `QMD_EMBED_TIMEOUT_MS` | `30000` | Commercial API request timeout |
 
 ## How It Works
 
@@ -820,8 +831,8 @@ Collection ──► Glob Pattern ──► Markdown Files ──► Parse Title
 Documents are chunked into ~900-token pieces with 15% overlap using smart boundary detection:
 
 ```
-Document ──► Smart Chunk (~900 tokens) ──► Format each chunk ──► node-llama-cpp ──► Store Vectors
-                │                           "title | text"        embedBatch()
+Document ──► Smart Chunk (~900 tokens) ──► Format each chunk ──► Commercial HTTPS API ──► Store Vectors
+                │                           "title | text"        purpose-bound request
                 └─► Chunks stored with:
                     - hash: document hash
@@ -913,13 +924,9 @@ Query ──► LLM Expansion ──► [Original, Variant 1, Variant 2]
 
 ## Model Configuration
 
-Models are configured in `src/llm.ts` as HuggingFace URIs:
+Commercial provider selection is configured with `QMD_EMBED_ENDPOINT`, `QMD_EMBED_API_KEY`, `QMD_EMBED_MODEL_ID`, and `QMD_EMBED_UPSTREAM_MODEL`. Production deployments should supply these through a central secrets and purpose-contract system rather than shell history or repository files.
 
-```typescript
-const DEFAULT_EMBED_MODEL = "hf:ggml-org/embeddinggemma-300M-GGUF/embeddinggemma-300M-Q8_0.gguf";
-const DEFAULT_RERANK_MODEL = "hf:ggml-org/Qwen3-Reranker-0.6B-Q8_0-GGUF/qwen3-reranker-0.6b-q8_0.gguf";
-const DEFAULT_GENERATE_MODEL = "hf:tobil/qmd-query-expansion-1.7B-gguf/qmd-query-expansion-1.7B-q4_k_m.gguf";
-```
+Provider or contract errors fail closed as typed `HOLD`. There is no local model, weight download, GGUF cache, self-hosted endpoint, or automatic fallback path.
 
 ### EmbeddingGemma Prompt Format
 
@@ -931,13 +938,9 @@ const DEFAULT_GENERATE_MODEL = "hf:tobil/qmd-query-expansion-1.7B-gguf/qmd-query
 "title: {title} | text: {content}"
 ```
 
-### Qwen3-Reranker
-
-Uses node-llama-cpp's `createRankingContext()` and `rankAndSort()` API for cross-encoder reranking. Returns documents sorted by relevance score (0.0 - 1.0).
-
-### Qwen3 (Query Expansion)
+### Learned Reranking and Expansion
 
-Used for generating query variations via `LlamaChatSession`.
+These stages require a separately registered commercial API adapter. Builds without such an adapter return typed `HOLD`; they never substitute a local model.
 
 ## License
 

+ 8 - 263
bun.lock

@@ -1,6 +1,5 @@
 {
   "lockfileVersion": 1,
-  "configVersion": 1,
   "workspaces": {
     "": {
       "name": "2025-12-07-bm25-q",
@@ -8,7 +7,6 @@
         "@modelcontextprotocol/sdk": "1.29.0",
         "better-sqlite3": "12.8.0",
         "fast-glob": "3.3.3",
-        "node-llama-cpp": "3.18.1",
         "picomatch": "4.0.4",
         "sqlite-vec": "0.1.9",
         "web-tree-sitter": "0.26.7",
@@ -17,6 +15,7 @@
       },
       "devDependencies": {
         "@types/better-sqlite3": "7.6.13",
+        "@types/node": "25.6.0",
         "tsx": "4.21.0",
         "vitest": "3.2.4",
       },
@@ -93,68 +92,16 @@
 
     "@hono/node-server": ["@hono/node-server@1.19.12", "", { "peerDependencies": { "hono": "^4" } }, "sha512-txsUW4SQ1iilgE0l9/e9VQWmELXifEFvmdA1j6WFh/aFPj99hIntrSsq/if0UWyGVkmrRPKA1wCeP+UCr1B9Uw=="],
 
-    "@huggingface/jinja": ["@huggingface/jinja@0.5.6", "", {}, "sha512-MyMWyLnjqo+KRJYSH7oWNbsOn5onuIvfXYPcc0WOGxU0eHUV7oAYUoQTl2BMdu7ml+ea/bu11UM+EshbeHwtIA=="],
-
-    "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="],
-
     "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
 
-    "@kwsites/file-exists": ["@kwsites/file-exists@1.1.1", "", { "dependencies": { "debug": "^4.1.1" } }, "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw=="],
-
-    "@kwsites/promise-deferred": ["@kwsites/promise-deferred@1.1.1", "", {}, "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw=="],
-
     "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="],
 
-    "@node-llama-cpp/linux-arm64": ["@node-llama-cpp/linux-arm64@3.18.1", "", { "os": "linux", "cpu": [ "x64", "arm64", ] }, "sha512-rXMgZxUay78FOJV/fJ67apYP9eElH5jd4df5YRKPlLhLHHchuOSyDn+qtyW/L/EnPzpogoLkmULqCkdXU39XsQ=="],
-
-    "@node-llama-cpp/linux-armv7l": ["@node-llama-cpp/linux-armv7l@3.18.1", "", { "os": "linux", "cpu": [ "arm", "x64", ] }, "sha512-BrJL2cGo0pN5xd5nw+CzTn2rFMpz9MJyZZPUY81ptGkF2uIuXT2hdCVh56i9ImQrTwBfq1YcZL/l/Qe/1+HR/Q=="],
-
-    "@node-llama-cpp/linux-x64": ["@node-llama-cpp/linux-x64@3.18.1", "", { "os": "linux", "cpu": "x64" }, "sha512-tRmWcsyvAcqJHQHXHsaOkx6muGbcirA9nRdNgH6n7bjGUw4VuoBD3dChyNF3/Ktt7ohB9kz+XhhyZjbDHpXyMA=="],
-
-    "@node-llama-cpp/linux-x64-cuda": ["@node-llama-cpp/linux-x64-cuda@3.18.1", "", { "os": "linux", "cpu": "x64" }, "sha512-qOaYP4uwsUoBHQ/7xSOvyJIuXapS57Al+Sudgi00f96ldNZLKe1vuSGptAi5LTM2lIj66PKm6h8PlRWctwsZ2g=="],
-
-    "@node-llama-cpp/linux-x64-cuda-ext": ["@node-llama-cpp/linux-x64-cuda-ext@3.18.1", "", { "os": "linux", "cpu": "x64" }, "sha512-VqyKhAVHPCpFzh0f1koCBgpThL+04QOXwv0oDQ8s8YcpfMMOXQlBhTB0plgTh0HrPExoObfTS4ohkrbyGgmztQ=="],
-
-    "@node-llama-cpp/linux-x64-vulkan": ["@node-llama-cpp/linux-x64-vulkan@3.18.1", "", { "os": "linux", "cpu": "x64" }, "sha512-SIaNTK5pUPhwJD0gmiQfHa8OrRctVMmnqu+slJrz2Mzgg/XrwFndJlS9hvc+jSjTXCouwf7sYeQaaJWvQgBh/A=="],
-
-    "@node-llama-cpp/mac-arm64-metal": ["@node-llama-cpp/mac-arm64-metal@3.18.1", "", { "os": "darwin", "cpu": [ "x64", "arm64", ] }, "sha512-cyZTdsUMlvuRlGmkkoBbN3v/DT6NuruEqoQYd9CqIrPyLa1xLNBTSKIZ9SgRnw23iCOj4URfITvRP+2pu63LuQ=="],
-
-    "@node-llama-cpp/mac-x64": ["@node-llama-cpp/mac-x64@3.18.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-GfCPgdltaIpBhEnQ7WfsrRXrZO9r9pBtDUAQMXRuJwOPP5q7xKrQZUXI6J6mpc8tAG0//CTIuGn4hTKoD/8V8w=="],
-
-    "@node-llama-cpp/win-arm64": ["@node-llama-cpp/win-arm64@3.18.1", "", { "os": "win32", "cpu": [ "x64", "arm64", ] }, "sha512-S05YUzBMVSRS5KNbOS26cDYugeQHqogI3uewtTUBVC0tPbTHRSKjsdicmgWru1eNAry399LWWhzOf/3St/qsAw=="],
-
-    "@node-llama-cpp/win-x64": ["@node-llama-cpp/win-x64@3.18.1", "", { "os": "win32", "cpu": "x64" }, "sha512-QLDVphPl+YDI+x/VYYgIV1N9g0GMXk3PqcoopOUG3cBRUtce7FO+YX903YdRJezs4oKbIp8YaO+xYBgeUSqhpA=="],
-
-    "@node-llama-cpp/win-x64-cuda": ["@node-llama-cpp/win-x64-cuda@3.18.1", "", { "os": "win32", "cpu": "x64" }, "sha512-drgJmBhnxGQtB/SLo4sf4PPSuxRv3MdNP0FF6rKPY9TtzEOV293bRQyYEu/JYwvXfVApAIsRaJUTGvCkA9Qobw=="],
-
-    "@node-llama-cpp/win-x64-cuda-ext": ["@node-llama-cpp/win-x64-cuda-ext@3.18.1", "", { "os": "win32", "cpu": "x64" }, "sha512-u0FzJBQsJA355ksKERxwPJhlcWl3ZJSNkU2ZUwDEiKNOCbv3ybvSCIEyDvB63wdtkfVUuCRJWijZnpDZxrCGqg=="],
-
-    "@node-llama-cpp/win-x64-vulkan": ["@node-llama-cpp/win-x64-vulkan@3.18.1", "", { "os": "win32", "cpu": "x64" }, "sha512-PjmxrnPToi7y0zlP7l+hRIhvOmuEv94P6xZ11vjqICEJu8XdAJpvTfPKgDW4W0p0v4+So8ZiZYLUuwIHcsseyQ=="],
-
     "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
 
     "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
 
     "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
 
-    "@reflink/reflink": ["@reflink/reflink@0.1.19", "", { "optionalDependencies": { "@reflink/reflink-darwin-arm64": "0.1.19", "@reflink/reflink-darwin-x64": "0.1.19", "@reflink/reflink-linux-arm64-gnu": "0.1.19", "@reflink/reflink-linux-arm64-musl": "0.1.19", "@reflink/reflink-linux-x64-gnu": "0.1.19", "@reflink/reflink-linux-x64-musl": "0.1.19", "@reflink/reflink-win32-arm64-msvc": "0.1.19", "@reflink/reflink-win32-x64-msvc": "0.1.19" } }, "sha512-DmCG8GzysnCZ15bres3N5AHCmwBwYgp0As6xjhQ47rAUTUXxJiK+lLUxaGsX3hd/30qUpVElh05PbGuxRPgJwA=="],
-
-    "@reflink/reflink-darwin-arm64": ["@reflink/reflink-darwin-arm64@0.1.19", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ruy44Lpepdk1FqDz38vExBY/PVUsjxZA+chd9wozjUH9JjuDT/HEaQYA6wYN9mf041l0yLVar6BCZuWABJvHSA=="],
-
-    "@reflink/reflink-darwin-x64": ["@reflink/reflink-darwin-x64@0.1.19", "", { "os": "darwin", "cpu": "x64" }, "sha512-By85MSWrMZa+c26TcnAy8SDk0sTUkYlNnwknSchkhHpGXOtjNDUOxJE9oByBnGbeuIE1PiQsxDG3Ud+IVV9yuA=="],
-
-    "@reflink/reflink-linux-arm64-gnu": ["@reflink/reflink-linux-arm64-gnu@0.1.19", "", { "os": "linux", "cpu": "arm64" }, "sha512-7P+er8+rP9iNeN+bfmccM4hTAaLP6PQJPKWSA4iSk2bNvo6KU6RyPgYeHxXmzNKzPVRcypZQTpFgstHam6maVg=="],
-
-    "@reflink/reflink-linux-arm64-musl": ["@reflink/reflink-linux-arm64-musl@0.1.19", "", { "os": "linux", "cpu": "arm64" }, "sha512-37iO/Dp6m5DDaC2sf3zPtx/hl9FV3Xze4xoYidrxxS9bgP3S8ALroxRK6xBG/1TtfXKTvolvp+IjrUU6ujIGmA=="],
-
-    "@reflink/reflink-linux-x64-gnu": ["@reflink/reflink-linux-x64-gnu@0.1.19", "", { "os": "linux", "cpu": "x64" }, "sha512-jbI8jvuYCaA3MVUdu8vLoLAFqC+iNMpiSuLbxlAgg7x3K5bsS8nOpTRnkLF7vISJ+rVR8W+7ThXlXlUQ93ulkw=="],
-
-    "@reflink/reflink-linux-x64-musl": ["@reflink/reflink-linux-x64-musl@0.1.19", "", { "os": "linux", "cpu": "x64" }, "sha512-e9FBWDe+lv7QKAwtKOt6A2W/fyy/aEEfr0g6j/hWzvQcrzHCsz07BNQYlNOjTfeytrtLU7k449H1PI95jA4OjQ=="],
-
-    "@reflink/reflink-win32-arm64-msvc": ["@reflink/reflink-win32-arm64-msvc@0.1.19", "", { "os": "win32", "cpu": "arm64" }, "sha512-09PxnVIQcd+UOn4WAW73WU6PXL7DwGS6wPlkMhMg2zlHHG65F3vHepOw06HFCq+N42qkaNAc8AKIabWvtk6cIQ=="],
-
-    "@reflink/reflink-win32-x64-msvc": ["@reflink/reflink-win32-x64-msvc@0.1.19", "", { "os": "win32", "cpu": "x64" }, "sha512-E//yT4ni2SyhwP8JRjVGWr3cbnhWDiPLgnQ66qqaanjjnMiu3O/2tjCPQXlcGc/DEYofpDc9fvhv6tALQsMV9w=="],
-
     "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.57.1", "", { "os": "android", "cpu": "arm" }, "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg=="],
 
     "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.57.1", "", { "os": "android", "cpu": "arm64" }, "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w=="],
@@ -205,8 +152,6 @@
 
     "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA=="],
 
-    "@tinyhttp/content-disposition": ["@tinyhttp/content-disposition@2.2.2", "", {}, "sha512-crXw1txzrS36huQOyQGYFvhTeLeG0Si1xu+/l6kXUVYpE0TjFjEZRqTbuadQLfKGZ0jaI+jJoRyqaWwxOSHW2g=="],
-
     "@tree-sitter-grammars/tree-sitter-kotlin": ["@tree-sitter-grammars/tree-sitter-kotlin@1.1.0", "", { "dependencies": { "node-addon-api": "^8.3.0", "node-gyp-build": "^4.8.4", "npm-check-updates": "^17.1.13" }, "peerDependencies": { "tree-sitter": "^0.22.4" }, "optionalPeers": ["tree-sitter"] }, "sha512-vlVXaxEE8t2kpJgfZpa8XVvxcnKw9AYtRTgy7KWjsDmAsadk06RxAT80IXOgGQnmM9i/orQn1nD84gPNUHu6DQ=="],
 
     "@types/better-sqlite3": ["@types/better-sqlite3@7.6.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA=="],
@@ -217,7 +162,7 @@
 
     "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
 
-    "@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
+    "@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="],
 
     "@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="],
 
@@ -239,16 +184,8 @@
 
     "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
 
-    "ansi-escapes": ["ansi-escapes@6.2.1", "", {}, "sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig=="],
-
-    "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
-
-    "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
-
     "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
 
-    "async-retry": ["async-retry@1.3.3", "", { "dependencies": { "retry": "0.13.1" } }, "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw=="],
-
     "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
 
     "better-sqlite3": ["better-sqlite3@12.8.0", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ=="],
@@ -273,30 +210,10 @@
 
     "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="],
 
-    "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
-
     "check-error": ["check-error@2.1.3", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="],
 
-    "chmodrp": ["chmodrp@1.0.2", "", {}, "sha512-TdngOlFV1FLTzU0o1w8MB6/BFywhtLC0SzRTGJU7T9lmdjlCWeMRt1iVo0Ki+ldwNk0BqNiKoc8xpLZEQ8mY1w=="],
-
     "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="],
 
-    "ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="],
-
-    "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="],
-
-    "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="],
-
-    "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
-
-    "cmake-js": ["cmake-js@8.0.0", "", { "dependencies": { "debug": "^4.4.3", "fs-extra": "^11.3.3", "node-api-headers": "^1.8.0", "rc": "1.2.8", "semver": "^7.7.3", "tar": "^7.5.6", "url-join": "^4.0.1", "which": "^6.0.0", "yargs": "^17.7.2" }, "bin": { "cmake-js": "bin/cmake-js" } }, "sha512-YbUP88RDwCvoQkZhRtGURYm9RIpWdtvZuhT87fKNoLjk8kIFIFeARpKfuZQGdwfH99GZpUmqSfcDrK62X7lTgg=="],
-
-    "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
-
-    "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
-
-    "commander": ["commander@10.0.1", "", {}, "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug=="],
-
     "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="],
 
     "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
@@ -325,14 +242,10 @@
 
     "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
 
-    "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
-
     "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
 
     "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
 
-    "env-var": ["env-var@7.5.0", "", {}, "sha512-mKZOzLRN0ETzau2W2QXefbFjo5EF4yWq28OyKb9ICdeNhHJlOE/pHHnz4hdYJ9cNZXcJHo5xN4OT4pzuSHSNvA=="],
-
     "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
 
     "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
@@ -343,16 +256,12 @@
 
     "esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="],
 
-    "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
-
     "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
 
     "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
 
     "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
 
-    "eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="],
-
     "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="],
 
     "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="],
@@ -377,10 +286,6 @@
 
     "file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="],
 
-    "filename-reserved-regex": ["filename-reserved-regex@3.0.0", "", {}, "sha512-hn4cQfU6GOT/7cFHXBqeBg2TbrMBgdD0kcjLhvSQYYwm3s4B6cjvBfb7nBALJLAXqmU5xajSa7X2NnUud/VCdw=="],
-
-    "filenamify": ["filenamify@6.0.0", "", { "dependencies": { "filename-reserved-regex": "^3.0.0" } }, "sha512-vqIlNogKeyD3yzrm0yhRMQg8hOVwYcYRfjEoODd49iCprMn4HL85gK3HcykQE53EPIpX3HcAbGA5ELQv216dAQ=="],
-
     "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
 
     "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
@@ -391,16 +296,10 @@
 
     "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="],
 
-    "fs-extra": ["fs-extra@11.3.4", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA=="],
-
     "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
 
     "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
 
-    "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
-
-    "get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="],
-
     "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
 
     "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
@@ -413,8 +312,6 @@
 
     "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
 
-    "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
-
     "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
 
     "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
@@ -427,8 +324,6 @@
 
     "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
 
-    "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
-
     "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
 
     "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
@@ -437,23 +332,15 @@
 
     "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
 
-    "ipull": ["ipull@3.9.5", "", { "dependencies": { "@tinyhttp/content-disposition": "^2.2.0", "async-retry": "^1.3.3", "chalk": "^5.3.0", "ci-info": "^4.0.0", "cli-spinners": "^2.9.2", "commander": "^10.0.0", "eventemitter3": "^5.0.1", "filenamify": "^6.0.0", "fs-extra": "^11.1.1", "is-unicode-supported": "^2.0.0", "lifecycle-utils": "^2.0.1", "lodash.debounce": "^4.0.8", "lowdb": "^7.0.1", "pretty-bytes": "^6.1.0", "pretty-ms": "^8.0.0", "sleep-promise": "^9.1.0", "slice-ansi": "^7.1.0", "stdout-update": "^4.0.1", "strip-ansi": "^7.1.0" }, "optionalDependencies": { "@reflink/reflink": "^0.1.16" }, "bin": { "ipull": "dist/cli/cli.js" } }, "sha512-5w/yZB5lXmTfsvNawmvkCjYo4SJNuKQz/av8TC1UiOyfOHyaM+DReqbpU2XpWYfmY+NIUbRRH8PUAWsxaS+IfA=="],
-
     "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
 
-    "is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="],
-
     "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
 
-    "is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="],
-
     "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
 
     "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
 
-    "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="],
-
-    "isexe": ["isexe@4.0.0", "", {}, "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw=="],
+    "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
 
     "jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="],
 
@@ -463,18 +350,8 @@
 
     "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
 
-    "jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="],
-
-    "lifecycle-utils": ["lifecycle-utils@3.1.1", "", {}, "sha512-gNd3OvhFNjHykJE3uGntz7UuPzWlK9phrIdXxU9Adis0+ExkwnZibfxCJWiWWZ+a6VbKiZrb+9D9hCQWd4vjTg=="],
-
-    "lodash.debounce": ["lodash.debounce@4.0.8", "", {}, "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="],
-
-    "log-symbols": ["log-symbols@7.0.1", "", { "dependencies": { "is-unicode-supported": "^2.0.0", "yoctocolors": "^2.1.1" } }, "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg=="],
-
     "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="],
 
-    "lowdb": ["lowdb@7.0.1", "", { "dependencies": { "steno": "^4.0.2" } }, "sha512-neJAj8GwF0e8EpycYIDFqEPcx9Qz4GUho20jWFR7YiFeXzF1YMLdxB36PypcTSPMA+4+LvgyMacYhlr18Zlymw=="],
-
     "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
 
     "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
@@ -491,21 +368,15 @@
 
     "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
 
-    "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="],
-
     "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="],
 
     "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
 
-    "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
-
-    "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="],
-
     "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="],
 
     "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
 
-    "nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="],
+    "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
 
     "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="],
 
@@ -515,12 +386,8 @@
 
     "node-addon-api": ["node-addon-api@8.7.0", "", {}, "sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA=="],
 
-    "node-api-headers": ["node-api-headers@1.8.0", "", {}, "sha512-jfnmiKWjRAGbdD1yQS28bknFM1tbHC1oucyuMPjmkEs+kpiu76aRs40WlTmBmyEgzDM76ge1DQ7XJ3R5deiVjQ=="],
-
     "node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="],
 
-    "node-llama-cpp": ["node-llama-cpp@3.18.1", "", { "dependencies": { "@huggingface/jinja": "^0.5.6", "async-retry": "^1.3.3", "bytes": "^3.1.2", "chalk": "^5.6.2", "chmodrp": "^1.0.2", "cmake-js": "^8.0.0", "cross-spawn": "^7.0.6", "env-var": "^7.5.0", "filenamify": "^6.0.0", "fs-extra": "^11.3.4", "ignore": "^7.0.4", "ipull": "^3.9.5", "is-unicode-supported": "^2.1.0", "lifecycle-utils": "^3.1.1", "log-symbols": "^7.0.1", "nanoid": "^5.1.6", "node-addon-api": "^8.6.0", "ora": "^9.3.0", "pretty-ms": "^9.3.0", "proper-lockfile": "^4.1.2", "semver": "^7.7.1", "simple-git": "^3.33.0", "slice-ansi": "^8.0.0", "stdout-update": "^4.0.1", "strip-ansi": "^7.2.0", "validate-npm-package-name": "^7.0.2", "which": "^6.0.1", "yargs": "^17.7.2" }, "optionalDependencies": { "@node-llama-cpp/linux-arm64": "3.18.1", "@node-llama-cpp/linux-armv7l": "3.18.1", "@node-llama-cpp/linux-x64": "3.18.1", "@node-llama-cpp/linux-x64-cuda": "3.18.1", "@node-llama-cpp/linux-x64-cuda-ext": "3.18.1", "@node-llama-cpp/linux-x64-vulkan": "3.18.1", "@node-llama-cpp/mac-arm64-metal": "3.18.1", "@node-llama-cpp/mac-x64": "3.18.1", "@node-llama-cpp/win-arm64": "3.18.1", "@node-llama-cpp/win-x64": "3.18.1", "@node-llama-cpp/win-x64-cuda": "3.18.1", "@node-llama-cpp/win-x64-cuda-ext": "3.18.1", "@node-llama-cpp/win-x64-vulkan": "3.18.1" }, "peerDependencies": { "typescript": ">=5.0.0" }, "optionalPeers": ["typescript"], "bin": { "node-llama-cpp": "dist/cli/cli.js", "nlc": "dist/cli/cli.js" } }, "sha512-w0zfuy/IKS2fhrbed5SylZDXJHTVz4HnkwZ4UrFPgSNwJab3QIPwIl4lyCKHHy9flLrtxsAuV5kXfH3HZ6bb8w=="],
-
     "npm-check-updates": ["npm-check-updates@17.1.18", "", { "bin": { "ncu": "build/cli.js", "npm-check-updates": "build/cli.js" } }, "sha512-bkUy2g4v1i+3FeUf5fXMLbxmV95eG4/sS7lYE32GrUeVgQRfQEk39gpskksFunyaxQgTIdrvYbnuNbO/pSUSqw=="],
 
     "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
@@ -531,12 +398,6 @@
 
     "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
 
-    "onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
-
-    "ora": ["ora@9.3.0", "", { "dependencies": { "chalk": "^5.6.2", "cli-cursor": "^5.0.0", "cli-spinners": "^3.2.0", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.1.0", "log-symbols": "^7.0.1", "stdin-discarder": "^0.3.1", "string-width": "^8.1.0" } }, "sha512-lBX72MWFduWEf7v7uWf5DHp9Jn5BI8bNPGuFgtXMmr2uDz2Gz2749y3am3agSDdkhHPHYmmxEGSKH85ZLGzgXw=="],
-
-    "parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="],
-
     "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
 
     "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
@@ -557,12 +418,6 @@
 
     "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="],
 
-    "pretty-bytes": ["pretty-bytes@6.1.1", "", {}, "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ=="],
-
-    "pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="],
-
-    "proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="],
-
     "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
 
     "pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="],
@@ -579,16 +434,10 @@
 
     "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
 
-    "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
-
     "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
 
     "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
 
-    "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="],
-
-    "retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="],
-
     "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
 
     "rollup": ["rollup@4.57.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.1", "@rollup/rollup-android-arm64": "4.57.1", "@rollup/rollup-darwin-arm64": "4.57.1", "@rollup/rollup-darwin-x64": "4.57.1", "@rollup/rollup-freebsd-arm64": "4.57.1", "@rollup/rollup-freebsd-x64": "4.57.1", "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", "@rollup/rollup-linux-arm-musleabihf": "4.57.1", "@rollup/rollup-linux-arm64-gnu": "4.57.1", "@rollup/rollup-linux-arm64-musl": "4.57.1", "@rollup/rollup-linux-loong64-gnu": "4.57.1", "@rollup/rollup-linux-loong64-musl": "4.57.1", "@rollup/rollup-linux-ppc64-gnu": "4.57.1", "@rollup/rollup-linux-ppc64-musl": "4.57.1", "@rollup/rollup-linux-riscv64-gnu": "4.57.1", "@rollup/rollup-linux-riscv64-musl": "4.57.1", "@rollup/rollup-linux-s390x-gnu": "4.57.1", "@rollup/rollup-linux-x64-gnu": "4.57.1", "@rollup/rollup-linux-x64-musl": "4.57.1", "@rollup/rollup-openbsd-x64": "4.57.1", "@rollup/rollup-openharmony-arm64": "4.57.1", "@rollup/rollup-win32-arm64-msvc": "4.57.1", "@rollup/rollup-win32-ia32-msvc": "4.57.1", "@rollup/rollup-win32-x64-gnu": "4.57.1", "@rollup/rollup-win32-x64-msvc": "4.57.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A=="],
@@ -623,18 +472,10 @@
 
     "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
 
-    "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
-
     "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="],
 
     "simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="],
 
-    "simple-git": ["simple-git@3.33.0", "", { "dependencies": { "@kwsites/file-exists": "^1.1.1", "@kwsites/promise-deferred": "^1.1.1", "debug": "^4.4.0" } }, "sha512-D4V/tGC2sjsoNhoMybKyGoE+v8A60hRawKQ1iFRA1zwuDgGZCBJ4ByOzZ5J8joBbi4Oam0qiPH+GhzmSBwbJng=="],
-
-    "sleep-promise": ["sleep-promise@9.1.0", "", {}, "sha512-UHYzVpz9Xn8b+jikYSD6bqvf754xL2uBUzDFwiU6NcdZeifPr6UfgU43xpkPu67VMS88+TI2PSI7Eohgqf2fKA=="],
-
-    "slice-ansi": ["slice-ansi@8.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg=="],
-
     "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
 
     "sqlite-vec": ["sqlite-vec@0.1.9", "", { "optionalDependencies": { "sqlite-vec-darwin-arm64": "0.1.9", "sqlite-vec-darwin-x64": "0.1.9", "sqlite-vec-linux-arm64": "0.1.9", "sqlite-vec-linux-x64": "0.1.9", "sqlite-vec-windows-x64": "0.1.9" } }, "sha512-L7XJWRIBNvR9O5+vh1FQ+IGkh/3D2AzVksW5gdtk28m78Hy8skFD0pqReKH1Yp0/BUKRGcffgKvyO/EON5JXpA=="],
@@ -655,24 +496,12 @@
 
     "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
 
-    "stdin-discarder": ["stdin-discarder@0.3.1", "", {}, "sha512-reExS1kSGoElkextOcPkel4NE99S0BWxjUHQeDFnR8S993JxpPX7KU4MNmO19NXhlJp+8dmdCbKQVNgLJh2teA=="],
-
-    "stdout-update": ["stdout-update@4.0.1", "", { "dependencies": { "ansi-escapes": "^6.2.0", "ansi-styles": "^6.2.1", "string-width": "^7.1.0", "strip-ansi": "^7.1.0" } }, "sha512-wiS21Jthlvl1to+oorePvcyrIkiG/6M3D3VTmDUlJm7Cy6SbFhKkAvX+YBuHLxck/tO3mrdpC/cNesigQc3+UQ=="],
-
-    "steno": ["steno@4.0.2", "", {}, "sha512-yhPIQXjrlt1xv7dyPQg2P17URmXbuM5pdGkpiMB3RenprfiBlvK415Lctfe0eshk90oA7/tNq7WEiMK8RSP39A=="],
-
-    "string-width": ["string-width@8.2.0", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw=="],
-
     "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
 
-    "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
-
     "strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],
 
     "strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="],
 
-    "tar": ["tar@7.5.10", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-8mOPs1//5q/rlkNSPcCegA6hiHJYDmSLEI8aMH/CdSQJNWztHC9WHNam5zdQlfpTwB9Xp7IBEsHfV5LKMJGVAw=="],
-
     "tar-fs": ["tar-fs@2.1.4", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="],
 
     "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="],
@@ -713,18 +542,12 @@
 
     "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
 
-    "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
-
-    "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
+    "undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="],
 
     "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
 
-    "url-join": ["url-join@4.0.1", "", {}, "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA=="],
-
     "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
 
-    "validate-npm-package-name": ["validate-npm-package-name@7.0.2", "", {}, "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A=="],
-
     "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
 
     "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
@@ -735,68 +558,22 @@
 
     "web-tree-sitter": ["web-tree-sitter@0.26.7", "", {}, "sha512-KiZhelTvBA/ziUHEO7Emb75cGVAq8iGZNabYaZm53Zpy50NsXyOW+xSHlwHt5CVg/TRPZBfeVLTTobF0LjFJ1w=="],
 
-    "which": ["which@6.0.1", "", { "dependencies": { "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg=="],
+    "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
 
     "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
 
-    "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
-
     "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
 
-    "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
-
-    "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
-
     "yaml": ["yaml@2.8.3", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="],
 
-    "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
-
-    "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
-
-    "yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="],
-
     "zod": ["zod@4.2.1", "", {}, "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw=="],
 
     "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="],
 
-    "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
-
-    "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
-
-    "cmake-js/fs-extra": ["fs-extra@11.3.3", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg=="],
-
-    "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
-
-    "ipull/fs-extra": ["fs-extra@11.3.3", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg=="],
-
-    "ipull/lifecycle-utils": ["lifecycle-utils@2.1.0", "", {}, "sha512-AnrXnE2/OF9PHCyFg0RSqsnQTzV991XaZA/buhFDoc58xU7rhSCDgCz/09Lqpsn4MpoPHt7TRAXV1kWZypFVsA=="],
-
-    "ipull/pretty-ms": ["pretty-ms@8.0.0", "", { "dependencies": { "parse-ms": "^3.0.0" } }, "sha512-ASJqOugUF1bbzI35STMBUpZqdfYKlJugy6JBziGi2EE+AL5JPJGSzvpeVXojxrr0ViUYoToUjb5kjSEGf7Y83Q=="],
-
-    "ipull/slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="],
-
-    "ipull/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
-
-    "is-fullwidth-code-point/get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="],
+    "@types/better-sqlite3/@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
 
     "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
 
-    "ora/cli-spinners": ["cli-spinners@3.4.0", "", {}, "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw=="],
-
-    "postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
-
-    "proper-lockfile/retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="],
-
-    "restore-cursor/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
-
-    "stdout-update/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
-
-    "stdout-update/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
-
-    "string-width/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
-
-    "tar/chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
-
     "tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
 
     "tree-sitter-go/node-addon-api": ["node-addon-api@8.5.0", "", {}, "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A=="],
@@ -813,38 +590,6 @@
 
     "vitest/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
 
-    "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
-
-    "wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
-
-    "wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
-
-    "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
-
-    "cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
-
-    "cliui/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
-
-    "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
-
-    "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
-
-    "ipull/pretty-ms/parse-ms": ["parse-ms@3.0.0", "", {}, "sha512-Tpb8Z7r7XbbtBTrM9UhpkzzaMrqA2VXMT3YChzYltwV3P3pM6t8wl7TvpMnSTosz1aQAdVib7kdoys7vYOPerw=="],
-
-    "stdout-update/string-width/get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="],
-
-    "wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
-
-    "wrap-ansi/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
-
-    "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
-
-    "yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
-
-    "yargs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
-
-    "yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
-
-    "yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
+    "@types/better-sqlite3/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
   }
 }

+ 50 - 136
dist/cli/qmd.js

@@ -8,11 +8,12 @@ import { parseArgs } from "util";
 import { readFileSync, realpathSync, statSync, existsSync, unlinkSync, writeFileSync, openSync, closeSync, mkdirSync, lstatSync, rmSync, symlinkSync, readlinkSync } from "fs";
 import { createInterface } from "readline/promises";
 import { getPwd, getRealPath, homedir, resolve, enableProductionMode, searchFTS, extractSnippet, getContextForFile, getContextForPath, listCollections, removeCollection, renameCollection, findSimilarFiles, findDocumentByDocid, isDocid, matchFilesByGlob, getHashesNeedingEmbedding, clearAllEmbeddings, insertEmbedding, getStatus, hashContent, extractTitle, formatDocForEmbedding, chunkDocumentByTokens, clearCache, getCacheKey, getCachedResult, setCachedResult, getIndexHealth, parseVirtualPath, buildVirtualPath, isVirtualPath, resolveVirtualPath, toVirtualPath, insertContent, insertDocument, findActiveDocument, updateDocumentTitle, updateDocument, deactivateDocument, getActiveDocumentPaths, cleanupOrphanedContent, deleteLLMCache, deleteInactiveDocuments, cleanupOrphanedVectors, vacuumDatabase, getCollectionsWithoutContext, getTopLevelPathsWithoutContext, handelize, hybridQuery, vectorSearchQuery, structuredSearch, addLineNumbers, DEFAULT_EMBED_MODEL, DEFAULT_EMBED_MAX_BATCH_BYTES, DEFAULT_EMBED_MAX_DOCS_PER_BATCH, DEFAULT_RERANK_MODEL, DEFAULT_GLOB, DEFAULT_MULTI_GET_MAX_BYTES, createStore, getDefaultDbPath, reindexCollection, generateEmbeddings, syncConfigToDb, } from "../store.js";
-import { disposeDefaultLlamaCpp, getDefaultLlamaCpp, setDefaultLlamaCpp, LlamaCpp, withLLMSession, pullModels, DEFAULT_EMBED_MODEL_URI, DEFAULT_GENERATE_MODEL_URI, DEFAULT_RERANK_MODEL_URI, DEFAULT_MODEL_CACHE_DIR } from "../llm.js";
+import { disposeDefaultLlamaCpp, getDefaultLlamaCpp, setDefaultLlamaCpp, LlamaCpp, withLLMSession, DEFAULT_EMBED_MODEL_URI, DEFAULT_GENERATE_MODEL_URI, DEFAULT_RERANK_MODEL_URI } from "../llm.js";
 import { formatSearchResults, formatDocuments, escapeXml, escapeCSV, } from "./formatter.js";
 import { getCollection as getCollectionFromYaml, listCollections as yamlListCollections, getDefaultCollectionNames, addContext as yamlAddContext, removeContext as yamlRemoveContext, removeCollection as yamlRemoveCollectionFn, renameCollection as yamlRenameCollectionFn, setGlobalContext, listAllContexts, setConfigIndexName, loadConfig, } from "../collections.js";
 import { getEmbeddedQmdSkillContent, getEmbeddedQmdSkillFiles } from "../embedded-skills.js";
-import { createEmbeddingProvider, resolveProviderKind, ModelMismatchError, } from "../embedding/index.js";
+import { createEmbeddingProvider, ModelMismatchError, } from "../embedding/index.js";
+import { commercialApiHold } from "../model-policy.js";
 // Enable production mode - allows using default database path
 // Tests must set INDEX_PATH or use createStore() with explicit path
 enableProductionMode();
@@ -344,49 +345,10 @@ async function showStatus() {
     else {
         console.log(`\n${c.dim}No collections. Run 'qmd collection add .' to index markdown files.${c.reset}`);
     }
-    // Models
-    {
-        // hf:org/repo/file.gguf → https://huggingface.co/org/repo
-        const hfLink = (uri) => {
-            const match = uri.match(/^hf:([^/]+\/[^/]+)\//);
-            return match ? `https://huggingface.co/${match[1]}` : uri;
-        };
-        console.log(`\n${c.bold}Models${c.reset}`);
-        console.log(`  Embedding:   ${hfLink(DEFAULT_EMBED_MODEL_URI)}`);
-        console.log(`  Reranking:   ${hfLink(DEFAULT_RERANK_MODEL_URI)}`);
-        console.log(`  Generation:  ${hfLink(DEFAULT_GENERATE_MODEL_URI)}`);
-    }
-    // Device / GPU info
-    try {
-        const llm = getDefaultLlamaCpp();
-        const device = await llm.getDeviceInfo();
-        console.log(`\n${c.bold}Device${c.reset}`);
-        if (device.gpu) {
-            console.log(`  GPU:      ${c.green}${device.gpu}${c.reset} (offloading: ${device.gpuOffloading ? 'yes' : 'no'})`);
-            if (device.gpuDevices.length > 0) {
-                // Deduplicate and count GPUs
-                const counts = new Map();
-                for (const name of device.gpuDevices) {
-                    counts.set(name, (counts.get(name) || 0) + 1);
-                }
-                const deviceStr = Array.from(counts.entries())
-                    .map(([name, count]) => count > 1 ? `${count}× ${name}` : name)
-                    .join(', ');
-                console.log(`  Devices:  ${deviceStr}`);
-            }
-            if (device.vram) {
-                console.log(`  VRAM:     ${formatBytes(device.vram.free)} free / ${formatBytes(device.vram.total)} total`);
-            }
-        }
-        else {
-            console.log(`  GPU:      ${c.yellow}none${c.reset} (running on CPU — models will be slow)`);
-            console.log(`  ${c.dim}Tip: Install CUDA, Vulkan, or Metal support for GPU acceleration.${c.reset}`);
-        }
-        console.log(`  CPU:      ${device.cpuCores} math cores`);
-    }
-    catch {
-        // Don't fail status if LLM init fails
-    }
+    console.log(`\n${c.bold}Learned model policy${c.reset}`);
+    console.log("  Runtime:     commercial API only");
+    console.log(`  Embeddings:  ${process.env.QMD_EMBED_ENDPOINT ? "configured" : "HOLD (QMD_EMBED_ENDPOINT missing)"}`);
+    console.log("  Local model: disabled");
     // Tips section
     const tips = [];
     // Check for collections without context
@@ -424,7 +386,7 @@ async function updateCollections(collectionFilter) {
     const db = getDb();
     const storeInstance = getStore();
     // Collections are defined in YAML; no duplicate cleanup needed.
-    // Clear Ollama cache on update
+    // Clear legacy learned-response cache on update.
     clearCache(db);
     const allCollections = listCollections(db);
     if (allCollections.length === 0) {
@@ -1307,7 +1269,7 @@ async function indexFiles(pwd, globPattern = DEFAULT_GLOB, collectionName, suppr
     const resolvedPwd = pwd || getPwd();
     const now = new Date().toISOString();
     const excludeDirs = ["node_modules", ".git", ".cache", "vendor", "dist", "build"];
-    // Clear Ollama cache on index
+    // Clear legacy learned-response cache on index.
     clearCache(db);
     // Collection name must be provided (from YAML)
     if (!collectionName) {
@@ -1449,9 +1411,12 @@ function parseProviderKind(value) {
     if (value === undefined)
         return undefined;
     const s = String(value).toLowerCase();
-    if (s === "local" || s === "openai")
+    if (s === "local") {
+        throw commercialApiHold('--provider local is forbidden; use an approved commercial API');
+    }
+    if (s === "openai")
         return s;
-    throw new Error(`--provider must be "local" or "openai" (got "${s}")`);
+    throw commercialApiHold(`unsupported commercial provider kind "${s}"`);
 }
 function parseOptionalPositiveInt(name, value) {
     if (value === undefined)
@@ -1463,44 +1428,13 @@ function parseOptionalPositiveInt(name, value) {
     return parsed;
 }
 /**
- * Build an `EmbeddingProvider` for the QUERY-side path (vsearch / query)
- * if and only if the user has opted into a non-local provider via flags or
- * env vars. Returns `undefined` for the zero-config case so the legacy
- * `getDefaultLlamaCpp().embed(...)` path is used unchanged — preserving
- * pre-patch behavior for callers that have not configured remote embedding
- * (i-loazq6ze DoD #5: backward compat).
- *
- * Resolution mirrors `qmd embed` (factory.resolveProviderKind):
- *   1. Explicit `--provider` flag → build provider
- *   2. Any `--embed-*` flag / `QMD_EMBED_*` env / `embedProvider.endpoint`
- *      in `~/.config/qmd/config.json` → build provider
- *   3. Otherwise → return `undefined` (legacy path)
- *
- * Returns `null` on construction failure (e.g. malformed flags) so the
- * caller can warn + fall back to the legacy path.
+ * Build the commercial query-side provider. Missing or malformed config is a
+ * typed HOLD; it must never select the legacy local path.
  */
 function buildQueryEmbedProvider(values) {
     const providerCliKind = parseProviderKind(values["provider"]);
     const opts = buildProviderOpts(values, providerCliKind);
-    // Determine whether the user opted into a provider. The factory's resolve
-    // step returns "local" by default; without explicit opt-in (flag/env/
-    // config), we keep the legacy path with no construction overhead.
-    const resolved = resolveProviderKind(opts);
-    const hasProviderFlag = providerCliKind !== undefined;
-    const hasOpenAiOverride = !!opts.openai && Object.keys(opts.openai).length > 0;
-    const envOptIn = !!(process.env.QMD_EMBED_PROVIDER ||
-        process.env.QMD_EMBED_ENDPOINT ||
-        process.env.QMD_EMBED_AUTO_FALLBACK);
-    if (!hasProviderFlag && !hasOpenAiOverride && !envOptIn && resolved === "local") {
-        return undefined;
-    }
-    try {
-        return createEmbeddingProvider(opts);
-    }
-    catch (err) {
-        process.stderr.write(`${c.yellow}Warning: failed to build query embedding provider — using local fallback (${err instanceof Error ? err.message : String(err)})${c.reset}\n`);
-        return undefined;
-    }
+    return createEmbeddingProvider(opts);
 }
 /**
  * Translate `cli.values` into `CreateEmbeddingProviderOptions`. CLI flags
@@ -1524,7 +1458,7 @@ function buildProviderOpts(values, providerCliKind) {
             ...(timeoutMs !== undefined ? { timeoutMs } : {}),
         }
         : undefined;
-    // CLI flag for auto-fallback wrapping (only meaningful when kind === openai)
+    // Historical flag is passed through so the factory can reject it as typed HOLD.
     const autoFallback = values["embed-auto-fallback"] === true ? true : undefined;
     return {
         ...(providerCliKind ? { kind: providerCliKind } : {}),
@@ -1538,34 +1472,31 @@ function optionalString(v) {
     const s = String(v);
     return s === "" ? undefined : s;
 }
+function validateEmbedCollectionSelection(collection, force) {
+    if (collection === undefined)
+        return;
+    const allCollections = listCollections(getDb());
+    const match = allCollections.find(col => col.name === collection);
+    if (!match) {
+        const known = allCollections.map(col => col.name).sort().join(", ");
+        console.error(`${c.red}Collection not found: "${collection}"${c.reset}`);
+        console.error(`${c.dim}Available collections: ${known || "(none)"}${c.reset}`);
+        console.error(`${c.dim}Run 'qmd embed --all' (or 'qmd embed' with no args) to embed every collection.${c.reset}`);
+        closeDb();
+        process.exit(1);
+    }
+    if (force) {
+        console.error(`${c.red}--force cannot be combined with a positional collection name.${c.reset}`);
+        console.error(`${c.dim}--force clears ALL vectors fleet-wide before re-embedding; restricting it to one collection would corrupt the others.${c.reset}`);
+        console.error(`${c.dim}Use 'qmd embed --all -f' to force-re-embed every collection, OR drop -f and run 'qmd embed ${collection}' to embed only this collection's pending hashes.${c.reset}`);
+        closeDb();
+        process.exit(1);
+    }
+}
 async function vectorIndex(model = DEFAULT_EMBED_MODEL_URI, force = false, batchOptions) {
     const storeInstance = getStore();
     const db = storeInstance.db;
-    // i-ofojj7dy — validate the collection filter against the known list before
-    // doing any work. Mirrors `qmd update <name>` ergonomics.
-    if (batchOptions?.collection !== undefined) {
-        const allCollections = listCollections(db);
-        const match = allCollections.find(col => col.name === batchOptions.collection);
-        if (!match) {
-            const known = allCollections.map(c => c.name).sort().join(", ");
-            console.error(`${c.red}Collection not found: "${batchOptions.collection}"${c.reset}`);
-            console.error(`${c.dim}Available collections: ${known || "(none)"}${c.reset}`);
-            console.error(`${c.dim}Run 'qmd embed --all' (or 'qmd embed' with no args) to embed every collection.${c.reset}`);
-            closeDb();
-            process.exit(1);
-        }
-        // i-ofojj7dy — `--force` is fleet-wide (nukes all content_vectors).
-        // Combining it with a single-collection filter would silently break
-        // every OTHER collection's embeddings. Per-collection force-clear is a
-        // distinct feature (out of scope here). Refuse and steer the user.
-        if (force) {
-            console.error(`${c.red}--force cannot be combined with a positional collection name.${c.reset}`);
-            console.error(`${c.dim}--force clears ALL vectors fleet-wide before re-embedding; restricting it to one collection would corrupt the others.${c.reset}`);
-            console.error(`${c.dim}Use 'qmd embed --all -f' to force-re-embed every collection, OR drop -f and run 'qmd embed ${batchOptions.collection}' to embed only this collection's pending hashes.${c.reset}`);
-            closeDb();
-            process.exit(1);
-        }
-    }
+    validateEmbedCollectionSelection(batchOptions?.collection, force);
     if (force) {
         console.log(`${c.yellow}Force re-indexing: clearing all vectors...${c.reset}`);
     }
@@ -2069,10 +2000,7 @@ async function vectorSearch(query, opts, _model = DEFAULT_EMBED_MODEL) {
     const singleCollection = collectionNames.length === 1 ? collectionNames[0] : undefined;
     checkIndexHealth(store.db);
     // Build embedding provider for query encoding (i-loazq6ze).
-    // Same precedence as `qmd embed`: explicit `--provider` flag → env vars →
-    // `~/.config/qmd/config.json` → default LocalLlamaCppProvider. The local
-    // default keeps zero-config callers on the legacy llama-cpp path with no
-    // observable change.
+    // Commercial provider only; missing configuration remains typed HOLD.
     const embedProvider = opts.embedProvider;
     await withLLMSession(async () => {
         let results = await vectorSearchQuery(store, query, {
@@ -2495,14 +2423,14 @@ function showHelp() {
     console.log("                                  -f clears + re-embeds ALL vectors fleet-wide, incompatible with <collection>)");
     console.log("    --max-docs-per-batch <n>    - Cap docs loaded into memory per embedding batch");
     console.log("    --max-batch-mb <n>          - Cap UTF-8 MB loaded into memory per embedding batch");
-    console.log("    --provider {local,openai}   - Embedding backend (default: local llama.cpp)");
+    console.log("    --provider openai           - Commercial OpenAI-compatible API backend");
     console.log("    --embed-endpoint <url>      - OpenAI-compatible endpoint (or QMD_EMBED_ENDPOINT)");
     console.log("    --embed-api-key <key>       - Bearer token (or QMD_EMBED_API_KEY)");
     console.log("    --embed-model-id <id>       - Stable model id stored in DB (default: embeddinggemma)");
     console.log("    --embed-upstream-model <m>  - Model name sent in HTTP body (default: same as model-id)");
     console.log("    --embed-batch-size <n>      - Batch size for HTTP provider (default: 64)");
     console.log("    --embed-timeout-ms <n>      - Per-request timeout in ms (default: 30000)");
-    console.log("    --embed-auto-fallback       - Wrap openai provider in local fallback (or QMD_EMBED_AUTO_FALLBACK)");
+    console.log("    --embed-auto-fallback       - Forbidden legacy option; returns typed HOLD");
     console.log("  qmd cleanup [--no-vacuum]     - Clear caches and orphaned data; VACUUM unless --no-vacuum");
     console.log("");
     console.log("Query syntax (qmd query):");
@@ -2882,10 +2810,10 @@ if (isMain) {
                     process.exit(1);
                 }
                 const embedCollectionFilter = embedAllFlag ? undefined : embedCollectionArg;
-                // Build embedding provider from CLI flags + env + config file.
-                // Backward compat: with no flags / env vars, the factory returns
-                // a LocalLlamaCppProvider that delegates to the default LlamaCpp
-                // singleton — identical to pre-patch behavior.
+                // Validate deterministic CLI arguments before resolving credentials.
+                // Invalid collection intent must not be masked by a provider HOLD.
+                validateEmbedCollectionSelection(embedCollectionFilter, !!cli.values.force);
+                // Build the commercial embedding provider. No endpoint means typed HOLD.
                 const providerCliKind = parseProviderKind(cli.values["provider"]);
                 const providerOpts = buildProviderOpts(cli.values, providerCliKind);
                 const embedProvider = createEmbeddingProvider(providerOpts);
@@ -2911,22 +2839,8 @@ if (isMain) {
             break;
         case "pull": {
             const refresh = cli.values.refresh === undefined ? false : Boolean(cli.values.refresh);
-            const models = [
-                DEFAULT_EMBED_MODEL_URI,
-                DEFAULT_GENERATE_MODEL_URI,
-                DEFAULT_RERANK_MODEL_URI,
-            ];
-            console.log(`${c.bold}Pulling models${c.reset}`);
-            const results = await pullModels(models, {
-                refresh,
-                cacheDir: DEFAULT_MODEL_CACHE_DIR,
-            });
-            for (const result of results) {
-                const size = formatBytes(result.sizeBytes);
-                const note = result.refreshed ? "refreshed" : "cached/checked";
-                console.log(`- ${result.model} -> ${result.path} (${size}, ${note})`);
-            }
-            break;
+            void refresh;
+            throw commercialApiHold("qmd pull is disabled because GGUF downloads are forbidden");
         }
         case "search":
             if (!cli.query) {
@@ -2946,7 +2860,7 @@ if (isMain) {
                 cli.opts.minScore = 0.3;
             }
             // Build query-side embedding provider (i-loazq6ze).
-            // Returns undefined for zero-config callers (legacy local path).
+            // Missing commercial configuration fails closed as typed HOLD.
             cli.opts.embedProvider = buildQueryEmbedProvider(cli.values);
             await vectorSearch(cli.query, cli.opts);
             break;

+ 7 - 28
dist/embedding/factory.d.ts

@@ -4,16 +4,10 @@
  * Resolution order (first match wins):
  *   1. Explicit `kind` argument or `--provider` CLI flag → forces a kind
  *   2. `QMD_EMBED_ENDPOINT` env var present and non-empty → "openai"
- *   3. Config file (`~/.config/qmd/config.json`) `embedProvider.kind` → that kind
- *   4. Otherwise → "local" (legacy / backward-compat)
- *
- * Backward compat invariant: when neither `QMD_EMBED_ENDPOINT` nor
- * `~/.config/qmd/config.json` mentions a provider, callers get the same
- * `LocalLlamaCppProvider` they had before this change.
+ *   3. Config file (`~/.config/qmd/config.json`) commercial endpoint
+ *   4. Otherwise → typed HOLD (there is no local or self-hosted fallback)
  */
-import { type LocalLlamaCppProviderConfig } from "./local.js";
 import { type OpenAIProviderConfig } from "./openai.js";
-import { type AutoFallbackProviderConfig } from "./autofallback.js";
 import type { EmbeddingProvider, ProviderKind } from "./provider.js";
 export type EmbedProviderConfigFile = {
     embedProvider?: {
@@ -30,14 +24,14 @@ export type EmbedProviderConfigFile = {
          */
         concurrency?: number;
         timeoutMs?: number;
-        /** When true, wrap the openai provider in AutoFallback (local fallback). */
+        /** Historical only. `true` is rejected because local fallback is forbidden. */
         autoFallback?: boolean;
     };
 };
 export declare function defaultConfigPath(): string;
 /**
  * Load `~/.config/qmd/config.json` if present. Returns an empty object on
- * any read/parse error so we silently fall back to env/local.
+ * any read/parse error; provider construction then fails closed without an endpoint.
  */
 export declare function loadConfigFile(path?: string): EmbedProviderConfigFile;
 export type CreateEmbeddingProviderOptions = {
@@ -45,29 +39,13 @@ export type CreateEmbeddingProviderOptions = {
     kind?: ProviderKind;
     /** Override config file path (mostly for tests) */
     configPath?: string;
-    /** Local-provider overrides */
-    local?: LocalLlamaCppProviderConfig;
     /** OpenAI-provider overrides — merged on top of env/config */
     openai?: Partial<OpenAIProviderConfig>;
     /**
-     * Wrap the chosen provider in `AutoFallbackEmbeddingProvider` so that a
-     * remote outage transparently falls back to local llama.cpp. Default:
-     * `false` — opt-in, since the wrapper requires both backends to be
-     * available and the local one will warm node-llama-cpp on first call.
-     *
-     * Resolution: explicit `autoFallback` wins → env `QMD_EMBED_AUTO_FALLBACK`
-     * (`1`/`true`) → config-file `embedProvider.autoFallback` → false.
-     *
-     * Only applies when the resolved kind is `openai` (no fallback wrap when
-     * the primary IS local already).
+     * Historical compatibility input. Any truthy value produces typed HOLD;
+     * commercial provider failures must never fall back to a local model.
      */
     autoFallback?: boolean;
-    /**
-     * Override config for `AutoFallbackEmbeddingProvider` (failureStreak,
-     * cooldownMs, etc.). Only used when `autoFallback` resolves true.
-     * Primary + fallback are constructed automatically.
-     */
-    autoFallbackOverrides?: Omit<AutoFallbackProviderConfig, "primary" | "fallback">;
     /**
      * Custom env source (mostly for tests). Defaults to `process.env`.
      * Read keys: QMD_EMBED_PROVIDER, QMD_EMBED_ENDPOINT, QMD_EMBED_API_KEY,
@@ -86,3 +64,4 @@ export declare function resolveProviderKind(opts?: CreateEmbeddingProviderOption
  * Throws if `openai` kind is requested but no endpoint is configured.
  */
 export declare function createEmbeddingProvider(opts?: CreateEmbeddingProviderOptions): EmbeddingProvider;
+export declare function assertCommercialEndpoint(endpoint: string): void;

+ 43 - 25
dist/embedding/factory.js

@@ -4,19 +4,14 @@
  * Resolution order (first match wins):
  *   1. Explicit `kind` argument or `--provider` CLI flag → forces a kind
  *   2. `QMD_EMBED_ENDPOINT` env var present and non-empty → "openai"
- *   3. Config file (`~/.config/qmd/config.json`) `embedProvider.kind` → that kind
- *   4. Otherwise → "local" (legacy / backward-compat)
- *
- * Backward compat invariant: when neither `QMD_EMBED_ENDPOINT` nor
- * `~/.config/qmd/config.json` mentions a provider, callers get the same
- * `LocalLlamaCppProvider` they had before this change.
+ *   3. Config file (`~/.config/qmd/config.json`) commercial endpoint
+ *   4. Otherwise → typed HOLD (there is no local or self-hosted fallback)
  */
 import { existsSync, readFileSync } from "node:fs";
 import { homedir } from "node:os";
 import { join } from "node:path";
-import { LocalLlamaCppProvider } from "./local.js";
 import { OpenAIEmbeddingsProvider, } from "./openai.js";
-import { AutoFallbackEmbeddingProvider, } from "./autofallback.js";
+import { commercialApiHold } from "../model-policy.js";
 export function defaultConfigPath() {
     const xdg = process.env.XDG_CONFIG_HOME;
     const base = xdg ? xdg : join(homedir(), ".config");
@@ -24,7 +19,7 @@ export function defaultConfigPath() {
 }
 /**
  * Load `~/.config/qmd/config.json` if present. Returns an empty object on
- * any read/parse error so we silently fall back to env/local.
+ * any read/parse error; provider construction then fails closed without an endpoint.
  */
 export function loadConfigFile(path = defaultConfigPath()) {
     if (!existsSync(path))
@@ -48,25 +43,34 @@ export function resolveProviderKind(opts = {}) {
     const env = opts.env ?? process.env;
     const cfg = loadConfigFile(opts.configPath);
     // 1. Explicit kind argument
-    if (opts.kind)
+    if (opts.kind === "local") {
+        throw commercialApiHold('provider kind "local" is disabled; configure an approved commercial API');
+    }
+    if (opts.kind === "openai")
         return opts.kind;
     // 2a. Explicit env override
     const envKind = env.QMD_EMBED_PROVIDER?.trim().toLowerCase();
-    if (envKind === "local" || envKind === "openai")
+    if (envKind === "local") {
+        throw commercialApiHold("QMD_EMBED_PROVIDER=local is forbidden");
+    }
+    if (envKind === "openai")
         return envKind;
     // 2b. Endpoint env present → openai
     if (env.QMD_EMBED_ENDPOINT && env.QMD_EMBED_ENDPOINT.trim() !== "") {
         return "openai";
     }
     // 3. Config file
-    if (cfg.embedProvider?.kind === "local" || cfg.embedProvider?.kind === "openai") {
-        return cfg.embedProvider.kind;
+    if (cfg.embedProvider?.kind === "local") {
+        throw commercialApiHold("embedProvider.kind=local is forbidden");
+    }
+    if (cfg.embedProvider?.kind === "openai") {
+        return "openai";
     }
     if (cfg.embedProvider?.endpoint && cfg.embedProvider.endpoint.trim() !== "") {
         return "openai";
     }
-    // 4. Default
-    return "local";
+    // Commercial-only default. Missing endpoint is handled as typed HOLD by the factory.
+    return "openai";
 }
 /**
  * Factory entry point — returns the appropriate `EmbeddingProvider`.
@@ -77,17 +81,18 @@ export function createEmbeddingProvider(opts = {}) {
     const cfg = loadConfigFile(opts.configPath);
     const kind = resolveProviderKind(opts);
     if (kind === "local") {
-        return new LocalLlamaCppProvider(opts.local ?? {});
+        throw commercialApiHold('provider kind "local" is disabled');
     }
     // OpenAI
     const endpoint = opts.openai?.endpoint ??
         env.QMD_EMBED_ENDPOINT ??
         cfg.embedProvider?.endpoint;
     if (!endpoint || endpoint.trim() === "") {
-        throw new Error('createEmbeddingProvider: kind="openai" requires an endpoint. ' +
+        throw commercialApiHold('commercial provider requires an endpoint. ' +
             "Set QMD_EMBED_ENDPOINT env var, or `embedProvider.endpoint` in " +
             "~/.config/qmd/config.json, or pass `openai.endpoint`.");
     }
+    assertCommercialEndpoint(endpoint);
     const apiKey = opts.openai?.apiKey ??
         env.QMD_EMBED_API_KEY ??
         cfg.embedProvider?.apiKey;
@@ -120,15 +125,28 @@ export function createEmbeddingProvider(opts = {}) {
         sleep: opts.openai?.sleep,
         now: opts.openai?.now,
     });
-    // Should we wrap with AutoFallback? Resolution: arg → env → config → false.
+    // Historical fallback inputs are rejected instead of silently weakening policy.
     const autoFallback = resolveAutoFallback(opts, env, cfg);
-    if (!autoFallback)
-        return openaiProvider;
-    return new AutoFallbackEmbeddingProvider({
-        primary: openaiProvider,
-        fallback: new LocalLlamaCppProvider(opts.local ?? { modelId }),
-        ...(opts.autoFallbackOverrides ?? {}),
-    });
+    if (autoFallback) {
+        throw commercialApiHold("local auto-fallback is forbidden; commercial API failures must remain HOLD");
+    }
+    return openaiProvider;
+}
+export function assertCommercialEndpoint(endpoint) {
+    let parsed;
+    try {
+        parsed = new URL(endpoint);
+    }
+    catch {
+        throw commercialApiHold("commercial provider endpoint is malformed");
+    }
+    const host = parsed.hostname.toLowerCase();
+    const privateIpv4 = /^(?:10\.|127\.|169\.254\.|192\.168\.|172\.(?:1[6-9]|2\d|3[01])\.)/;
+    const localHost = host === "localhost" || host === "models" || host.endsWith(".local");
+    const localIpv6 = host === "::1" || host.startsWith("fe80:") || host.startsWith("fc") || host.startsWith("fd");
+    if (parsed.protocol !== "https:" || localHost || privateIpv4.test(host) || localIpv6) {
+        throw commercialApiHold(`endpoint ${parsed.protocol}//${host} is local, private, or non-TLS; use an approved commercial HTTPS API`);
+    }
 }
 function resolveAutoFallback(opts, env, cfg) {
     if (typeof opts.autoFallback === "boolean")

+ 1 - 1
dist/embedding/index.d.ts

@@ -4,5 +4,5 @@
 export { type EmbeddingProvider, type ProviderKind, type ProviderEmbedding, type ProviderEmbedOptions, type ProviderHealth, ModelMismatchError, assertModelCompatible, } from "./provider.js";
 export { LocalLlamaCppProvider, type LocalLlamaCppProviderConfig, } from "./local.js";
 export { OpenAIEmbeddingsProvider, CircuitBreaker, CircuitOpenError, HttpError, isRetryableStatus, chunkArray, type OpenAIProviderConfig, type CircuitState, DEFAULT_BATCH_SIZE, DEFAULT_TIMEOUT_MS, RETRY_BACKOFFS_MS, } from "./openai.js";
-export { createEmbeddingProvider, resolveProviderKind, loadConfigFile, defaultConfigPath, type CreateEmbeddingProviderOptions, type EmbedProviderConfigFile, } from "./factory.js";
+export { createEmbeddingProvider, resolveProviderKind, assertCommercialEndpoint, loadConfigFile, defaultConfigPath, type CreateEmbeddingProviderOptions, type EmbedProviderConfigFile, } from "./factory.js";
 export { AutoFallbackEmbeddingProvider, type AutoFallbackProviderConfig, type FallbackState, } from "./autofallback.js";

+ 1 - 1
dist/embedding/index.js

@@ -4,5 +4,5 @@
 export { ModelMismatchError, assertModelCompatible, } from "./provider.js";
 export { LocalLlamaCppProvider, } from "./local.js";
 export { OpenAIEmbeddingsProvider, CircuitBreaker, CircuitOpenError, HttpError, isRetryableStatus, chunkArray, DEFAULT_BATCH_SIZE, DEFAULT_TIMEOUT_MS, RETRY_BACKOFFS_MS, } from "./openai.js";
-export { createEmbeddingProvider, resolveProviderKind, loadConfigFile, defaultConfigPath, } from "./factory.js";
+export { createEmbeddingProvider, resolveProviderKind, assertCommercialEndpoint, loadConfigFile, defaultConfigPath, } from "./factory.js";
 export { AutoFallbackEmbeddingProvider, } from "./autofallback.js";

+ 5 - 25
dist/embedding/local.d.ts

@@ -1,38 +1,18 @@
-/**
- * local.ts - Local llama.cpp adapter implementing EmbeddingProvider.
- *
- * Wraps an existing `LlamaCpp` instance so the legacy GGUF path looks like
- * any other EmbeddingProvider to upstream callers. Used as the default and
- * as the fallback target when `OpenAIEmbeddingsProvider` trips its breaker.
- */
-import { type LlamaCpp } from "../llm.js";
+import type { LlamaCpp } from "../llm.js";
 import type { EmbeddingProvider, ProviderEmbedOptions, ProviderEmbedding, ProviderHealth, ProviderKind } from "./provider.js";
 export type LocalLlamaCppProviderConfig = {
-    /** Pre-built LlamaCpp instance (optional — falls back to global singleton). */
     llm?: LlamaCpp;
-    /**
-     * Stable model id reported via `getModelId()`. Defaults to "embeddinggemma"
-     * to match the value in `content_vectors.model` for existing qmd installs.
-     */
     modelId?: string;
 };
+/** Historical SDK symbol. Construction is blocked by the commercial-only policy. */
 export declare class LocalLlamaCppProvider implements EmbeddingProvider {
     readonly kind: ProviderKind;
-    private readonly llm;
-    private readonly modelId;
-    private dimensions;
-    private lastError;
-    constructor(config?: LocalLlamaCppProviderConfig);
+    constructor(_config?: LocalLlamaCppProviderConfig);
     getModelId(): string;
     getDimensions(): number | undefined;
-    /**
-     * Most recent thrown error from `llm.embed` / `llm.embedBatch`. Returns
-     * `undefined` after a successful call or before the first call. See
-     * `EmbeddingProvider.getLastError`.
-     */
     getLastError(): string | undefined;
     healthcheck(_signal?: AbortSignal): Promise<ProviderHealth>;
-    embed(text: string, options?: ProviderEmbedOptions): Promise<ProviderEmbedding | null>;
-    embedBatch(texts: string[], options?: ProviderEmbedOptions): Promise<(ProviderEmbedding | null)[]>;
+    embed(_text: string, _options?: ProviderEmbedOptions): Promise<ProviderEmbedding | null>;
+    embedBatch(_texts: string[], _options?: ProviderEmbedOptions): Promise<(ProviderEmbedding | null)[]>;
     dispose(): Promise<void>;
 }

+ 13 - 120
dist/embedding/local.js

@@ -1,128 +1,21 @@
-/**
- * local.ts - Local llama.cpp adapter implementing EmbeddingProvider.
- *
- * Wraps an existing `LlamaCpp` instance so the legacy GGUF path looks like
- * any other EmbeddingProvider to upstream callers. Used as the default and
- * as the fallback target when `OpenAIEmbeddingsProvider` trips its breaker.
- */
-import { getDefaultLlamaCpp, } from "../llm.js";
+import { commercialApiHold } from "../model-policy.js";
+/** Historical SDK symbol. Construction is blocked by the commercial-only policy. */
 export class LocalLlamaCppProvider {
     kind = "local";
-    llm;
-    modelId;
-    dimensions = undefined;
-    lastError = undefined;
-    constructor(config = {}) {
-        this.llm = config.llm ?? getDefaultLlamaCpp();
-        this.modelId = config.modelId ?? "embeddinggemma";
-    }
-    getModelId() {
-        return this.modelId;
-    }
-    getDimensions() {
-        return this.dimensions;
-    }
-    /**
-     * Most recent thrown error from `llm.embed` / `llm.embedBatch`. Returns
-     * `undefined` after a successful call or before the first call. See
-     * `EmbeddingProvider.getLastError`.
-     */
-    getLastError() {
-        return this.lastError;
+    constructor(_config = {}) {
+        throw commercialApiHold("LocalLlamaCppProvider is disabled; configure an approved commercial API");
     }
+    getModelId() { return "disabled-local-provider"; }
+    getDimensions() { return undefined; }
+    getLastError() { return "QMD_COMMERCIAL_API_HOLD"; }
     async healthcheck(_signal) {
-        // For the local provider, "healthy" means the embed model loads.
-        // We probe with a single embed call.
-        try {
-            const result = await this.llm.embed("healthcheck", { model: this.modelId });
-            if (!result) {
-                return {
-                    ok: false,
-                    model: this.modelId,
-                    detail: "embed probe returned null",
-                };
-            }
-            this.dimensions = result.embedding.length;
-            return {
-                ok: true,
-                model: this.modelId,
-                dimensions: this.dimensions,
-                detail: `local llama.cpp ready, ${this.dimensions}-d`,
-            };
-        }
-        catch (err) {
-            return {
-                ok: false,
-                model: this.modelId,
-                detail: err instanceof Error ? err.message : String(err),
-            };
-        }
-    }
-    async embed(text, options = {}) {
-        if (options.signal?.aborted) {
-            this.lastError = `aborted by caller${options.signal.reason ? `: ${String(options.signal.reason)}` : ""}`;
-            return null;
-        }
-        let result;
-        try {
-            result = await this.llm.embed(text, { model: options.model ?? this.modelId });
-        }
-        catch (err) {
-            this.lastError = `provider=local error="${err instanceof Error ? err.message : String(err)}"`;
-            return null;
-        }
-        if (!result) {
-            this.lastError = `provider=local error="llm.embed returned null/undefined"`;
-            return null;
-        }
-        if (this.dimensions === undefined) {
-            this.dimensions = result.embedding.length;
-        }
-        this.lastError = undefined;
-        return {
-            embedding: result.embedding,
-            model: this.modelId,
-        };
+        throw commercialApiHold("local provider healthcheck is disabled");
     }
-    async embedBatch(texts, options = {}) {
-        if (texts.length === 0)
-            return [];
-        if (options.signal?.aborted) {
-            this.lastError = `aborted by caller${options.signal.reason ? `: ${String(options.signal.reason)}` : ""}`;
-            return texts.map(() => null);
-        }
-        let raw;
-        try {
-            raw = await this.llm.embedBatch(texts, {
-                model: options.model ?? this.modelId,
-            });
-        }
-        catch (err) {
-            this.lastError = `provider=local error="${err instanceof Error ? err.message : String(err)}"`;
-            return texts.map(() => null);
-        }
-        const out = raw.map((r) => {
-            if (!r)
-                return null;
-            if (this.dimensions === undefined && r.embedding.length > 0) {
-                this.dimensions = r.embedding.length;
-            }
-            return {
-                embedding: r.embedding,
-                model: this.modelId,
-            };
-        });
-        if (out.every((r) => r !== null)) {
-            this.lastError = undefined;
-        }
-        else if (out.some((r) => r === null)) {
-            this.lastError = `provider=local error="llm.embedBatch returned null entries (${out.filter((r) => r === null).length}/${out.length})"`;
-        }
-        return out;
+    async embed(_text, _options = {}) {
+        throw commercialApiHold("local embedding is disabled");
     }
-    async dispose() {
-        // We do NOT dispose the underlying LlamaCpp here because the singleton
-        // is shared with rerank/generate/expansion paths. Disposal is handled
-        // by the existing `disposeDefaultLlamaCpp()` global hook.
+    async embedBatch(_texts, _options = {}) {
+        throw commercialApiHold("local batch embedding is disabled");
     }
+    async dispose() { }
 }

+ 2 - 4
dist/embedding/openai.d.ts

@@ -5,16 +5,14 @@
  * shape: request `{model, input: string|string[]}`, response
  * `{data: [{embedding: number[], index: number}, ...]}`.
  *
- * Used by qmd to delegate embeddings to a GPU worker (e.g. ai.mm.mk →
- * qmd-embed-worker on `models` LXC, RTX 4090) instead of running
- * node-llama-cpp locally.
+ * Used by qmd to delegate embeddings to an approved commercial HTTPS API.
  *
  * Features:
  *   - Batches input in groups of ≤64 (configurable via QMD_EMBED_BATCH_SIZE)
  *   - Retries 429 / 503 with exponential backoff (1s, 4s, 16s)
  *   - 4xx (non-429) → no retry, count as failure
  *   - Circuit breaker: >50% failures in a 60s window → OPEN for 5 min,
- *     callers can use this to fall back to a local provider
+ *     callers receive failures; local fallback is forbidden
  *   - Per-call timeout via AbortSignal (default QMD_EMBED_TIMEOUT_MS=30000)
  *   - Healthcheck via `GET /health` if available, else a probe embed call
  */

+ 2 - 4
dist/embedding/openai.js

@@ -5,16 +5,14 @@
  * shape: request `{model, input: string|string[]}`, response
  * `{data: [{embedding: number[], index: number}, ...]}`.
  *
- * Used by qmd to delegate embeddings to a GPU worker (e.g. ai.mm.mk →
- * qmd-embed-worker on `models` LXC, RTX 4090) instead of running
- * node-llama-cpp locally.
+ * Used by qmd to delegate embeddings to an approved commercial HTTPS API.
  *
  * Features:
  *   - Batches input in groups of ≤64 (configurable via QMD_EMBED_BATCH_SIZE)
  *   - Retries 429 / 503 with exponential backoff (1s, 4s, 16s)
  *   - 4xx (non-429) → no retry, count as failure
  *   - Circuit breaker: >50% failures in a 60s window → OPEN for 5 min,
- *     callers can use this to fall back to a local provider
+ *     callers receive failures; local fallback is forbidden
  *   - Per-call timeout via AbortSignal (default QMD_EMBED_TIMEOUT_MS=30000)
  *   - Healthcheck via `GET /health` if available, else a probe embed call
  */

+ 3 - 5
dist/embedding/provider.d.ts

@@ -1,9 +1,8 @@
 /**
  * provider.ts - Embedding provider abstraction
  *
- * Defines the EmbeddingProvider interface that allows qmd to use either:
- *   - LocalLlamaCppProvider (legacy, GGUF via node-llama-cpp)
- *   - OpenAIEmbeddingsProvider (HTTP, OpenAI-compatible endpoint like ai.mm.mk)
+ * Production embeddings use a commercial OpenAI-compatible API. The `local`
+ * kind remains readable only for historical config and is rejected by the factory.
  *
  * The factory in `./factory.ts` selects an implementation based on env vars,
  * a CLI flag, or `~/.config/qmd/config.json`.
@@ -42,7 +41,7 @@ export type ProviderEmbedOptions = {
     signal?: AbortSignal;
 };
 /**
- * Provider interface — both LocalLlamaCppProvider and OpenAIEmbeddingsProvider implement this.
+ * Provider interface for commercial embedding adapters and historical readers.
  *
  * Implementations MUST:
  *   - Return `null` (not throw) for individual texts that fail to embed;
@@ -70,7 +69,6 @@ export interface EmbeddingProvider {
      * Should NOT throw — return `{ ok: false, detail: ... }` on failure.
      *
      * For HTTP providers: ping `/health` endpoint.
-     * For local provider: ensure model loads.
      */
     healthcheck(signal?: AbortSignal): Promise<ProviderHealth>;
     /**

+ 2 - 3
dist/embedding/provider.js

@@ -1,9 +1,8 @@
 /**
  * provider.ts - Embedding provider abstraction
  *
- * Defines the EmbeddingProvider interface that allows qmd to use either:
- *   - LocalLlamaCppProvider (legacy, GGUF via node-llama-cpp)
- *   - OpenAIEmbeddingsProvider (HTTP, OpenAI-compatible endpoint like ai.mm.mk)
+ * Production embeddings use a commercial OpenAI-compatible API. The `local`
+ * kind remains readable only for historical config and is rejected by the factory.
  *
  * The factory in `./factory.ts` selects an implementation based on env vars,
  * a CLI flag, or `~/.config/qmd/config.json`.

+ 2 - 1
dist/index.d.ts

@@ -25,7 +25,8 @@ export type { ChunkStrategy } from "./store.js";
 export { getDefaultDbPath } from "./store.js";
 export { Maintenance } from "./maintenance.js";
 import type { EmbeddingProvider } from "./embedding/index.js";
-export { createEmbeddingProvider, resolveProviderKind, LocalLlamaCppProvider, OpenAIEmbeddingsProvider, CircuitBreaker, CircuitOpenError, HttpError, ModelMismatchError, assertModelCompatible, type EmbeddingProvider, type ProviderKind, type ProviderEmbedding, type ProviderEmbedOptions, type ProviderHealth, type CreateEmbeddingProviderOptions, type OpenAIProviderConfig, type LocalLlamaCppProviderConfig, type EmbedProviderConfigFile, DEFAULT_BATCH_SIZE as DEFAULT_PROVIDER_BATCH_SIZE, DEFAULT_TIMEOUT_MS as DEFAULT_PROVIDER_TIMEOUT_MS, RETRY_BACKOFFS_MS as PROVIDER_RETRY_BACKOFFS_MS, } from "./embedding/index.js";
+export { createEmbeddingProvider, resolveProviderKind, assertCommercialEndpoint, LocalLlamaCppProvider, OpenAIEmbeddingsProvider, CircuitBreaker, CircuitOpenError, HttpError, ModelMismatchError, assertModelCompatible, type EmbeddingProvider, type ProviderKind, type ProviderEmbedding, type ProviderEmbedOptions, type ProviderHealth, type CreateEmbeddingProviderOptions, type OpenAIProviderConfig, type LocalLlamaCppProviderConfig, type EmbedProviderConfigFile, DEFAULT_BATCH_SIZE as DEFAULT_PROVIDER_BATCH_SIZE, DEFAULT_TIMEOUT_MS as DEFAULT_PROVIDER_TIMEOUT_MS, RETRY_BACKOFFS_MS as PROVIDER_RETRY_BACKOFFS_MS, } from "./embedding/index.js";
+export { CommercialApiHoldError, COMMERCIAL_API_HOLD_CODE, commercialApiHold, } from "./model-policy.js";
 export { getDistinctEmbeddingModels } from "./store.js";
 /**
  * Progress info emitted during update() for each file processed.

+ 4 - 4
dist/index.js

@@ -26,10 +26,10 @@ export { getDefaultDbPath } from "./store.js";
 // Re-export Maintenance class for CLI housekeeping operations
 export { Maintenance } from "./maintenance.js";
 // Re-export embedding provider abstraction for SDK consumers (i-qkarfffa).
-// `createEmbeddingProvider` honors QMD_EMBED_ENDPOINT / config-file / kind
-// arg precedence; default fallback is the legacy LocalLlamaCppProvider so
-// SDK code that doesn't pass `embedProvider` keeps the prior behavior.
-export { createEmbeddingProvider, resolveProviderKind, LocalLlamaCppProvider, OpenAIEmbeddingsProvider, CircuitBreaker, CircuitOpenError, HttpError, ModelMismatchError, assertModelCompatible, DEFAULT_BATCH_SIZE as DEFAULT_PROVIDER_BATCH_SIZE, DEFAULT_TIMEOUT_MS as DEFAULT_PROVIDER_TIMEOUT_MS, RETRY_BACKOFFS_MS as PROVIDER_RETRY_BACKOFFS_MS, } from "./embedding/index.js";
+// `createEmbeddingProvider` is commercial-only. The historical local symbol
+// remains exported for source compatibility but its constructor returns HOLD.
+export { createEmbeddingProvider, resolveProviderKind, assertCommercialEndpoint, LocalLlamaCppProvider, OpenAIEmbeddingsProvider, CircuitBreaker, CircuitOpenError, HttpError, ModelMismatchError, assertModelCompatible, DEFAULT_BATCH_SIZE as DEFAULT_PROVIDER_BATCH_SIZE, DEFAULT_TIMEOUT_MS as DEFAULT_PROVIDER_TIMEOUT_MS, RETRY_BACKOFFS_MS as PROVIDER_RETRY_BACKOFFS_MS, } from "./embedding/index.js";
+export { CommercialApiHoldError, COMMERCIAL_API_HOLD_CODE, commercialApiHold, } from "./model-policy.js";
 export { getDistinctEmbeddingModels } from "./store.js";
 /**
  * Create a QMD store for programmatic access to search and indexing.

+ 40 - 328
dist/llm.d.ts

@@ -1,125 +1,64 @@
-/**
- * llm.ts - LLM abstraction layer for QMD using node-llama-cpp
- *
- * Provides embeddings, text generation, and reranking using local GGUF models.
- */
-import { type Token as LlamaToken } from "node-llama-cpp";
-/**
- * `QMD_DISABLE_LOCAL_LLM=1` opt-out: when set, `LlamaCpp.ensureLlama()`
- * throws on first invocation. Use for remote-only deployments where any
- * `getLlama()` call indicates an unintended fallback (e.g. cron host
- * without libvulkan-dev/glslc — issue i-c28wngnd).
- */
-export declare function isLocalLlmDisabled(env?: NodeJS.ProcessEnv): boolean;
-/**
- * Resolve the GPU mode for `getLlama()`:
- *   1. Explicit `QMD_LLAMA_GPU=off|none|0|...`     → "cpu"
- *   2. Explicit `QMD_LLAMA_GPU=auto`               → "auto"
- *   3. Auto-detect: `QMD_EMBED_ENDPOINT` set        → "cpu"
- *      (remote embed provider — embed never touches local LLM. Rerank/expand
- *       still use prebuilt CPU binary; no Vulkan probe / cmake build.)
- *   4. Otherwise (legacy local-only setup)          → "auto"
- */
-export declare function resolveLlamaGpuMode(env?: NodeJS.ProcessEnv): "cpu" | "auto";
-/**
- * Detect if a model URI uses the Qwen3-Embedding format.
- * Qwen3-Embedding uses a different prompting style than nomic/embeddinggemma.
- */
+export declare function isLocalLlmDisabled(_env?: NodeJS.ProcessEnv): boolean;
+export declare function resolveLlamaGpuMode(_env?: NodeJS.ProcessEnv): "cpu" | "auto";
 export declare function isQwen3EmbeddingModel(modelUri: string): boolean;
-/**
- * Format a query for embedding.
- * Uses nomic-style task prefix format for embeddinggemma (default).
- * Uses Qwen3-Embedding instruct format when a Qwen embedding model is active.
- */
 export declare function formatQueryForEmbedding(query: string, modelUri?: string): string;
-/**
- * Format a document for embedding.
- * Uses nomic-style format with title and text fields (default).
- * Qwen3-Embedding encodes documents as raw text without special prefixes.
- */
 export declare function formatDocForEmbedding(text: string, title?: string, modelUri?: string): string;
-/**
- * Token with log probability
- */
 export type TokenLogProb = {
     token: string;
     logprob: number;
 };
-/**
- * Embedding result
- */
 export type EmbeddingResult = {
     embedding: number[];
     model: string;
 };
-/**
- * Generation result with optional logprobs
- */
 export type GenerateResult = {
     text: string;
     model: string;
     logprobs?: TokenLogProb[];
     done: boolean;
 };
-/**
- * Rerank result for a single document
- */
 export type RerankDocumentResult = {
     file: string;
     score: number;
     index: number;
 };
-/**
- * Batch rerank result
- */
 export type RerankResult = {
     results: RerankDocumentResult[];
     model: string;
 };
-/**
- * Model info
- */
 export type ModelInfo = {
     name: string;
     exists: boolean;
     path?: string;
 };
-/**
- * Options for embedding
- */
 export type EmbedOptions = {
     model?: string;
     isQuery?: boolean;
     title?: string;
 };
-/**
- * Options for text generation
- */
 export type GenerateOptions = {
     model?: string;
     maxTokens?: number;
     temperature?: number;
 };
-/**
- * Options for reranking
- */
 export type RerankOptions = {
     model?: string;
 };
-/**
- * Options for LLM sessions
- */
 export type LLMSessionOptions = {
-    /** Max session duration in ms (default: 10 minutes) */
     maxDuration?: number;
-    /** External abort signal */
     signal?: AbortSignal;
-    /** Debug name for logging */
     name?: string;
 };
-/**
- * Session interface for scoped LLM access with lifecycle guarantees
- */
+export type QueryType = "lex" | "vec" | "hyde";
+export type Queryable = {
+    type: QueryType;
+    text: string;
+};
+export type RerankDocument = {
+    file: string;
+    text: string;
+    title?: string;
+};
 export interface ILLMSession {
     embed(text: string, options?: EmbedOptions): Promise<EmbeddingResult | null>;
     embedBatch(texts: string[], options?: EmbedOptions): Promise<(EmbeddingResult | null)[]>;
@@ -128,78 +67,36 @@ export interface ILLMSession {
         includeLexical?: boolean;
     }): Promise<Queryable[]>;
     rerank(query: string, documents: RerankDocument[], options?: RerankOptions): Promise<RerankResult>;
-    /** Whether this session is still valid (not released or aborted) */
     readonly isValid: boolean;
-    /** Abort signal for this session (aborts on release or maxDuration) */
     readonly signal: AbortSignal;
 }
-/**
- * Supported query types for different search backends
- */
-export type QueryType = 'lex' | 'vec' | 'hyde';
-/**
- * A single query and its target backend type
- */
-export type Queryable = {
-    type: QueryType;
-    text: string;
-};
-/**
- * Document to rerank
- */
-export type RerankDocument = {
-    file: string;
-    text: string;
-    title?: string;
-};
-export declare const LFM2_GENERATE_MODEL = "hf:LiquidAI/LFM2-1.2B-GGUF/LFM2-1.2B-Q4_K_M.gguf";
-export declare const LFM2_INSTRUCT_MODEL = "hf:LiquidAI/LFM2.5-1.2B-Instruct-GGUF/LFM2.5-1.2B-Instruct-Q4_K_M.gguf";
-export declare const DEFAULT_EMBED_MODEL_URI = "hf:ggml-org/embeddinggemma-300M-GGUF/embeddinggemma-300M-Q8_0.gguf";
-export declare const DEFAULT_RERANK_MODEL_URI = "hf:ggml-org/Qwen3-Reranker-0.6B-Q8_0-GGUF/qwen3-reranker-0.6b-q8_0.gguf";
-export declare const DEFAULT_GENERATE_MODEL_URI = "hf:tobil/qmd-query-expansion-1.7B-gguf/qmd-query-expansion-1.7B-q4_k_m.gguf";
-export declare const DEFAULT_MODEL_CACHE_DIR: string;
+export declare const LFM2_GENERATE_MODEL = "commercial-api-required";
+export declare const LFM2_INSTRUCT_MODEL = "commercial-api-required";
+export declare const DEFAULT_EMBED_MODEL_URI = "commercial-api:embedding-unconfigured";
+export declare const DEFAULT_RERANK_MODEL_URI = "commercial-api:rerank-unconfigured";
+export declare const DEFAULT_GENERATE_MODEL_URI = "commercial-api:generation-unconfigured";
+export declare const DEFAULT_MODEL_CACHE_DIR = "commercial-api:no-local-cache";
 export type PullResult = {
     model: string;
     path: string;
     sizeBytes: number;
     refreshed: boolean;
 };
-export declare function pullModels(models: string[], options?: {
+export declare function pullModels(_models: string[], _options?: {
     refresh?: boolean;
     cacheDir?: string;
 }): Promise<PullResult[]>;
-/**
- * Abstract LLM interface - implement this for different backends
- */
 export interface LLM {
-    /**
-     * Get embeddings for text
-     */
     embed(text: string, options?: EmbedOptions): Promise<EmbeddingResult | null>;
-    /**
-     * Generate text completion
-     */
+    embedBatch(texts: string[], options?: EmbedOptions): Promise<(EmbeddingResult | null)[]>;
     generate(prompt: string, options?: GenerateOptions): Promise<GenerateResult | null>;
-    /**
-     * Check if a model exists/is available
-     */
-    modelExists(model: string): Promise<ModelInfo>;
-    /**
-     * Expand a search query into multiple variations for different backends.
-     * Returns a list of Queryable objects.
-     */
+    modelExists(modelUri: string): Promise<ModelInfo>;
     expandQuery(query: string, options?: {
         context?: string;
         includeLexical?: boolean;
+        intent?: string;
     }): Promise<Queryable[]>;
-    /**
-     * Rerank documents by relevance to a query
-     * Returns list of documents with relevance scores (higher = more relevant)
-     */
     rerank(query: string, documents: RerankDocument[], options?: RerankOptions): Promise<RerankResult>;
-    /**
-     * Dispose of resources
-     */
     dispose(): Promise<void>;
 }
 export type LlamaCppConfig = {
@@ -207,182 +104,31 @@ export type LlamaCppConfig = {
     generateModel?: string;
     rerankModel?: string;
     modelCacheDir?: string;
-    /**
-     * Context size used for query expansion generation contexts.
-     * Default: 2048. Can also be set via QMD_EXPAND_CONTEXT_SIZE.
-     */
     expandContextSize?: number;
-    /**
-     * Inactivity timeout in ms before unloading contexts (default: 2 minutes, 0 to disable).
-     *
-     * Per node-llama-cpp lifecycle guidance, we prefer keeping models loaded and only disposing
-     * contexts when idle, since contexts (and their sequences) are the heavy per-session objects.
-     * @see https://node-llama-cpp.withcat.ai/guide/objects-lifecycle
-     */
     inactivityTimeoutMs?: number;
-    /**
-     * Whether to dispose models on inactivity (default: false).
-     *
-     * Keeping models loaded avoids repeated VRAM thrash; set to true only if you need aggressive
-     * memory reclaim.
-     */
     disposeModelsOnInactivity?: boolean;
 };
+/** Historical SDK name retained as a fail-closed compatibility adapter. */
 export declare class LlamaCpp implements LLM {
-    private readonly _ciMode;
-    private llama;
-    private embedModel;
-    private embedContexts;
-    private generateModel;
-    private rerankModel;
-    private rerankContexts;
-    private embedModelUri;
-    private generateModelUri;
-    private rerankModelUri;
-    private modelCacheDir;
-    private expandContextSize;
-    private embedModelLoadPromise;
-    private generateModelLoadPromise;
-    private rerankModelLoadPromise;
-    private inactivityTimer;
-    private inactivityTimeoutMs;
-    private disposeModelsOnInactivity;
-    private disposed;
+    static readonly EMBED_CONTEXT_SIZE = 2048;
+    static readonly RERANK_CONTEXT_SIZE = 2048;
+    static readonly RERANK_TARGET_DOCS_PER_CONTEXT = 32;
+    static readonly RERANK_TEMPLATE_OVERHEAD = 32;
+    readonly embedModelName: string;
     constructor(config?: LlamaCppConfig);
-    get embedModelName(): string;
-    /**
-     * Reset the inactivity timer. Called after each model operation.
-     * When timer fires, models are unloaded to free memory (if no active sessions).
-     */
-    private touchActivity;
-    /**
-     * Check if any contexts are currently loaded (and therefore worth unloading on inactivity).
-     */
-    private hasLoadedContexts;
-    /**
-     * Unload idle resources but keep the instance alive for future use.
-     *
-     * By default, this disposes contexts (and their dependent sequences), while keeping models loaded.
-     * This matches the intended lifecycle: model → context → sequence, where contexts are per-session.
-     */
-    unloadIdleResources(): Promise<void>;
-    /**
-     * Ensure model cache directory exists
-     */
-    private ensureModelCacheDir;
-    /**
-     * Initialize the llama instance (lazy)
-     *
-     * Env-var controls (i-c28wngnd):
-     *   - QMD_DISABLE_LOCAL_LLM=1    : hard-disable; throws on first ensureLlama()
-     *                                  call. Use when the deployment must NEVER
-     *                                  load node-llama-cpp (e.g. headless cron
-     *                                  on a host without libvulkan-dev/glslc).
-     *   - QMD_LLAMA_GPU=off|none|... : force CPU-only (skip Vulkan probe).
-     *   - QMD_LLAMA_GPU=auto         : explicit opt-in to GPU probe even when
-     *                                  QMD_EMBED_ENDPOINT is set (rare; useful
-     *                                  for hybrid local-rerank + remote-embed).
-     *
-     * Auto-detect: when QMD_EMBED_ENDPOINT is set (HTTP embed provider, e.g.
-     * cron on `code` → ai.mm.mk → models:8082), we default to CPU-only because
-     * the embed path runs over HTTP and the only remaining local LLM consumers
-     * are rerank/query-expansion, which work fine on the prebuilt CPU binary
-     * and never need to invoke cmake-js-llama. This silences ~30s/run of
-     * Vulkan probe + cmake noise on headless LXCs.
-     */
-    private ensureLlama;
-    /**
-     * Resolve a model URI to a local path, downloading if needed
-     */
-    private resolveModel;
-    /**
-     * Load embedding model (lazy)
-     */
-    private ensureEmbedModel;
-    /**
-     * Compute how many parallel contexts to create.
-     *
-     * GPU: constrained by VRAM (25% of free, capped at 8).
-     * CPU: constrained by cores. Splitting threads across contexts enables
-     *      true parallelism (each context runs on its own cores). Use at most
-     *      half the math cores, with at least 4 threads per context.
-     */
-    private computeParallelism;
-    /**
-     * Get the number of threads each context should use, given N parallel contexts.
-     * Splits available math cores evenly across contexts.
-     */
-    private threadsPerContext;
-    /**
-     * Load embedding contexts (lazy). Creates multiple for parallel embedding.
-     * Uses promise guard to prevent concurrent context creation race condition.
-     */
-    private embedContextsCreatePromise;
-    private ensureEmbedContexts;
-    /**
-     * Get a single embed context (for single-embed calls). Uses first from pool.
-     */
-    private ensureEmbedContext;
-    /**
-     * Load generation model (lazy) - context is created fresh per call
-     */
-    private ensureGenerateModel;
-    /**
-     * Load rerank model (lazy)
-     */
-    private ensureRerankModel;
-    /**
-     * Load rerank contexts (lazy). Creates multiple contexts for parallel ranking.
-     * Each context has its own sequence, so they can evaluate independently.
-     *
-     * Tuning choices:
-     * - contextSize 1024: reranking chunks are ~800 tokens max, 1024 is plenty
-     * - flashAttention: ~20% less VRAM per context (568 vs 711 MB)
-     * - Combined: drops from 11.6 GB (auto, no flash) to 568 MB per context (20×)
-     */
-    private static readonly RERANK_CONTEXT_SIZE;
-    private static readonly EMBED_CONTEXT_SIZE;
-    private ensureRerankContexts;
-    /**
-     * Tokenize text using the embedding model's tokenizer
-     * Returns tokenizer tokens (opaque type from node-llama-cpp)
-     */
-    tokenize(text: string): Promise<readonly LlamaToken[]>;
-    /**
-     * Count tokens in text using the embedding model's tokenizer
-     */
-    countTokens(text: string): Promise<number>;
-    /**
-     * Detokenize token IDs back to text
-     */
-    detokenize(tokens: readonly LlamaToken[]): Promise<string>;
-    /**
-     * Truncate text to fit within the embedding model's context window.
-     * Uses the model's own tokenizer for accurate token counting, then
-     * detokenizes back to text if truncation is needed.
-     * Returns the (possibly truncated) text and whether truncation occurred.
-     */
-    private truncateToContextSize;
-    embed(text: string, options?: EmbedOptions): Promise<EmbeddingResult | null>;
-    /**
-     * Batch embed multiple texts efficiently
-     * Uses Promise.all for parallel embedding - node-llama-cpp handles batching internally
-     */
-    embedBatch(texts: string[], options?: EmbedOptions): Promise<(EmbeddingResult | null)[]>;
-    generate(prompt: string, options?: GenerateOptions): Promise<GenerateResult | null>;
+    tokenize(_text: string): Promise<readonly number[]>;
+    countTokens(_text: string): Promise<number>;
+    detokenize(_tokens: readonly number[]): Promise<string>;
+    embed(_text: string, _options?: EmbedOptions): Promise<EmbeddingResult | null>;
+    embedBatch(_texts: string[], _options?: EmbedOptions): Promise<(EmbeddingResult | null)[]>;
+    generate(_prompt: string, _options?: GenerateOptions): Promise<GenerateResult | null>;
     modelExists(modelUri: string): Promise<ModelInfo>;
-    expandQuery(query: string, options?: {
+    expandQuery(_query: string, _options?: {
         context?: string;
         includeLexical?: boolean;
         intent?: string;
     }): Promise<Queryable[]>;
-    private static readonly RERANK_TEMPLATE_OVERHEAD;
-    private static readonly RERANK_TARGET_DOCS_PER_CONTEXT;
-    rerank(query: string, documents: RerankDocument[], options?: RerankOptions): Promise<RerankResult>;
-    /**
-     * Get device/GPU info for status display.
-     * Initializes llama if not already done.
-     */
+    rerank(_query: string, _documents: RerankDocument[], _options?: RerankOptions): Promise<RerankResult>;
     getDeviceInfo(): Promise<{
         gpu: string | false;
         gpuOffloading: boolean;
@@ -394,49 +140,15 @@ export declare class LlamaCpp implements LLM {
         };
         cpuCores: number;
     }>;
+    unloadIdleResources(): Promise<void>;
     dispose(): Promise<void>;
 }
-/**
- * Error thrown when an operation is attempted on a released or aborted session.
- */
 export declare class SessionReleasedError extends Error {
     constructor(message?: string);
 }
-/**
- * Execute a function with a scoped LLM session.
- * The session provides lifecycle guarantees - resources won't be disposed mid-operation.
- *
- * @example
- * ```typescript
- * await withLLMSession(async (session) => {
- *   const expanded = await session.expandQuery(query);
- *   const embeddings = await session.embedBatch(texts);
- *   const reranked = await session.rerank(query, docs);
- *   return reranked;
- * }, { maxDuration: 10 * 60 * 1000, name: 'querySearch' });
- * ```
- */
-export declare function withLLMSession<T>(fn: (session: ILLMSession) => Promise<T>, options?: LLMSessionOptions): Promise<T>;
-/**
- * Execute a function with a scoped LLM session using a specific LlamaCpp instance.
- * Unlike withLLMSession, this does not use the global singleton.
- */
-export declare function withLLMSessionForLlm<T>(llm: LlamaCpp, fn: (session: ILLMSession) => Promise<T>, options?: LLMSessionOptions): Promise<T>;
-/**
- * Check if idle unload is safe (no active sessions or operations).
- * Used internally by LlamaCpp idle timer.
- */
-export declare function canUnloadLLM(): boolean;
-/**
- * Get the default LlamaCpp instance (creates one if needed)
- */
 export declare function getDefaultLlamaCpp(): LlamaCpp;
-/**
- * Set a custom default LlamaCpp instance (useful for testing)
- */
 export declare function setDefaultLlamaCpp(llm: LlamaCpp | null): void;
-/**
- * Dispose the default LlamaCpp instance if it exists.
- * Call this before process exit to prevent NAPI crashes.
- */
 export declare function disposeDefaultLlamaCpp(): Promise<void>;
+export declare function withLLMSession<T>(fn: (session: ILLMSession) => Promise<T>, options?: LLMSessionOptions): Promise<T>;
+export declare function withLLMSessionForLlm<T>(llm: LlamaCpp, fn: (session: ILLMSession) => Promise<T>, options?: LLMSessionOptions): Promise<T>;
+export declare function canUnloadLLM(): boolean;

+ 83 - 1229
dist/llm.js

@@ -1,1299 +1,153 @@
-/**
- * llm.ts - LLM abstraction layer for QMD using node-llama-cpp
- *
- * Provides embeddings, text generation, and reranking using local GGUF models.
- */
-import { getLlama, resolveModelFile, LlamaChatSession, LlamaLogLevel, } from "node-llama-cpp";
-import { homedir } from "os";
-import { join } from "path";
-import { existsSync, mkdirSync, statSync, unlinkSync, readdirSync, readFileSync, writeFileSync } from "fs";
-// =============================================================================
-// Local-LLM env-var policy (i-c28wngnd)
-// =============================================================================
-/**
- * Truthy values for boolean-style env vars. Mirrors the convention used by
- * `QMD_LLAMA_GPU` (false-style) — kept narrow so unrelated values don't flip
- * the disable.
- */
-const TRUTHY_ENV_VALUES = new Set(["1", "true", "yes", "on"]);
-/**
- * Falsy / off-style values accepted by `QMD_LLAMA_GPU`.
- */
-const QMD_LLAMA_GPU_OFF_VALUES = new Set([
-    "false", "off", "none", "disable", "disabled", "0",
-]);
-/**
- * `QMD_DISABLE_LOCAL_LLM=1` opt-out: when set, `LlamaCpp.ensureLlama()`
- * throws on first invocation. Use for remote-only deployments where any
- * `getLlama()` call indicates an unintended fallback (e.g. cron host
- * without libvulkan-dev/glslc — issue i-c28wngnd).
- */
-export function isLocalLlmDisabled(env = process.env) {
-    const raw = env.QMD_DISABLE_LOCAL_LLM?.trim().toLowerCase();
-    return raw !== undefined && TRUTHY_ENV_VALUES.has(raw);
+import { cpus } from "node:os";
+import { commercialApiHold } from "./model-policy.js";
+export function isLocalLlmDisabled(_env = process.env) {
+    return true;
 }
-/**
- * Resolve the GPU mode for `getLlama()`:
- *   1. Explicit `QMD_LLAMA_GPU=off|none|0|...`     → "cpu"
- *   2. Explicit `QMD_LLAMA_GPU=auto`               → "auto"
- *   3. Auto-detect: `QMD_EMBED_ENDPOINT` set        → "cpu"
- *      (remote embed provider — embed never touches local LLM. Rerank/expand
- *       still use prebuilt CPU binary; no Vulkan probe / cmake build.)
- *   4. Otherwise (legacy local-only setup)          → "auto"
- */
-export function resolveLlamaGpuMode(env = process.env) {
-    const explicit = env.QMD_LLAMA_GPU?.trim().toLowerCase();
-    if (explicit !== undefined && explicit !== "") {
-        if (QMD_LLAMA_GPU_OFF_VALUES.has(explicit))
-            return "cpu";
-        if (explicit === "auto" || explicit === "true" || explicit === "on") {
-            return "auto";
-        }
-        // Unknown value — preserve legacy behavior (probe).
-        return "auto";
-    }
-    // Auto-detect remote-only deployment. When QMD_EMBED_ENDPOINT is set the
-    // embed path runs over HTTP (factory.ts resolveProviderKind), so any
-    // local LLM access is for rerank/expand only — the prebuilt CPU binary
-    // is sufficient and skipping the Vulkan probe avoids the ~30s cmake
-    // attempt on hosts without libvulkan-dev/glslc.
-    const remoteEmbed = env.QMD_EMBED_ENDPOINT?.trim();
-    if (remoteEmbed && remoteEmbed !== "")
-        return "cpu";
-    return "auto";
+export function resolveLlamaGpuMode(_env = process.env) {
+    return "cpu";
 }
-// =============================================================================
-// Embedding Formatting Functions
-// =============================================================================
-/**
- * Detect if a model URI uses the Qwen3-Embedding format.
- * Qwen3-Embedding uses a different prompting style than nomic/embeddinggemma.
- */
 export function isQwen3EmbeddingModel(modelUri) {
     return /qwen.*embed/i.test(modelUri) || /embed.*qwen/i.test(modelUri);
 }
-/**
- * Format a query for embedding.
- * Uses nomic-style task prefix format for embeddinggemma (default).
- * Uses Qwen3-Embedding instruct format when a Qwen embedding model is active.
- */
 export function formatQueryForEmbedding(query, modelUri) {
-    const uri = modelUri ?? process.env.QMD_EMBED_MODEL ?? DEFAULT_EMBED_MODEL;
-    if (isQwen3EmbeddingModel(uri)) {
+    if (modelUri && isQwen3EmbeddingModel(modelUri)) {
         return `Instruct: Retrieve relevant documents for the given query\nQuery: ${query}`;
     }
     return `task: search result | query: ${query}`;
 }
-/**
- * Format a document for embedding.
- * Uses nomic-style format with title and text fields (default).
- * Qwen3-Embedding encodes documents as raw text without special prefixes.
- */
 export function formatDocForEmbedding(text, title, modelUri) {
-    const uri = modelUri ?? process.env.QMD_EMBED_MODEL ?? DEFAULT_EMBED_MODEL;
-    if (isQwen3EmbeddingModel(uri)) {
-        // Qwen3-Embedding: documents are raw text, no task prefix
+    if (modelUri && isQwen3EmbeddingModel(modelUri)) {
         return title ? `${title}\n${text}` : text;
     }
     return `title: ${title || "none"} | text: ${text}`;
 }
-// =============================================================================
-// Model Configuration
-// =============================================================================
-// HuggingFace model URIs for node-llama-cpp
-// Format: hf:<user>/<repo>/<file>
-// Override via QMD_EMBED_MODEL env var (e.g. hf:Qwen/Qwen3-Embedding-0.6B-GGUF/Qwen3-Embedding-0.6B-Q8_0.gguf)
-const DEFAULT_EMBED_MODEL = "hf:ggml-org/embeddinggemma-300M-GGUF/embeddinggemma-300M-Q8_0.gguf";
-const DEFAULT_RERANK_MODEL = "hf:ggml-org/Qwen3-Reranker-0.6B-Q8_0-GGUF/qwen3-reranker-0.6b-q8_0.gguf";
-// const DEFAULT_GENERATE_MODEL = "hf:ggml-org/Qwen3-0.6B-GGUF/Qwen3-0.6B-Q8_0.gguf";
-const DEFAULT_GENERATE_MODEL = "hf:tobil/qmd-query-expansion-1.7B-gguf/qmd-query-expansion-1.7B-q4_k_m.gguf";
-// Alternative generation models for query expansion:
-// LiquidAI LFM2 - hybrid architecture optimized for edge/on-device inference
-// Use these as base for fine-tuning with configs/sft_lfm2.yaml
-export const LFM2_GENERATE_MODEL = "hf:LiquidAI/LFM2-1.2B-GGUF/LFM2-1.2B-Q4_K_M.gguf";
-export const LFM2_INSTRUCT_MODEL = "hf:LiquidAI/LFM2.5-1.2B-Instruct-GGUF/LFM2.5-1.2B-Instruct-Q4_K_M.gguf";
-export const DEFAULT_EMBED_MODEL_URI = DEFAULT_EMBED_MODEL;
-export const DEFAULT_RERANK_MODEL_URI = DEFAULT_RERANK_MODEL;
-export const DEFAULT_GENERATE_MODEL_URI = DEFAULT_GENERATE_MODEL;
-// Local model cache directory
-const MODEL_CACHE_DIR = process.env.XDG_CACHE_HOME
-    ? join(process.env.XDG_CACHE_HOME, "qmd", "models")
-    : join(homedir(), ".cache", "qmd", "models");
-export const DEFAULT_MODEL_CACHE_DIR = MODEL_CACHE_DIR;
-function parseHfUri(model) {
-    if (!model.startsWith("hf:"))
-        return null;
-    const without = model.slice(3);
-    const parts = without.split("/");
-    if (parts.length < 3)
-        return null;
-    const repo = parts.slice(0, 2).join("/");
-    const file = parts.slice(2).join("/");
-    return { repo, file };
-}
-async function getRemoteEtag(ref) {
-    const url = `https://huggingface.co/${ref.repo}/resolve/main/${ref.file}`;
-    try {
-        const resp = await fetch(url, { method: "HEAD" });
-        if (!resp.ok)
-            return null;
-        const etag = resp.headers.get("etag");
-        return etag || null;
-    }
-    catch {
-        return null;
-    }
+export const LFM2_GENERATE_MODEL = "commercial-api-required";
+export const LFM2_INSTRUCT_MODEL = "commercial-api-required";
+export const DEFAULT_EMBED_MODEL_URI = "commercial-api:embedding-unconfigured";
+export const DEFAULT_RERANK_MODEL_URI = "commercial-api:rerank-unconfigured";
+export const DEFAULT_GENERATE_MODEL_URI = "commercial-api:generation-unconfigured";
+export const DEFAULT_MODEL_CACHE_DIR = "commercial-api:no-local-cache";
+export async function pullModels(_models, _options = {}) {
+    throw commercialApiHold("local model downloads are disabled");
 }
-export async function pullModels(models, options = {}) {
-    const cacheDir = options.cacheDir || MODEL_CACHE_DIR;
-    if (!existsSync(cacheDir)) {
-        mkdirSync(cacheDir, { recursive: true });
-    }
-    const results = [];
-    for (const model of models) {
-        let refreshed = false;
-        const hfRef = parseHfUri(model);
-        const filename = model.split("/").pop();
-        const entries = readdirSync(cacheDir, { withFileTypes: true });
-        const cached = filename
-            ? entries
-                .filter((entry) => entry.isFile() && entry.name.includes(filename))
-                .map((entry) => join(cacheDir, entry.name))
-            : [];
-        if (hfRef && filename) {
-            const etagPath = join(cacheDir, `${filename}.etag`);
-            const remoteEtag = await getRemoteEtag(hfRef);
-            const localEtag = existsSync(etagPath)
-                ? readFileSync(etagPath, "utf-8").trim()
-                : null;
-            const shouldRefresh = options.refresh || !remoteEtag || remoteEtag !== localEtag || cached.length === 0;
-            if (shouldRefresh) {
-                for (const candidate of cached) {
-                    if (existsSync(candidate))
-                        unlinkSync(candidate);
-                }
-                if (existsSync(etagPath))
-                    unlinkSync(etagPath);
-                refreshed = cached.length > 0;
-            }
-        }
-        else if (options.refresh && filename) {
-            for (const candidate of cached) {
-                if (existsSync(candidate))
-                    unlinkSync(candidate);
-                refreshed = true;
-            }
-        }
-        const path = await resolveModelFile(model, cacheDir);
-        const sizeBytes = existsSync(path) ? statSync(path).size : 0;
-        if (hfRef && filename) {
-            const remoteEtag = await getRemoteEtag(hfRef);
-            if (remoteEtag) {
-                const etagPath = join(cacheDir, `${filename}.etag`);
-                writeFileSync(etagPath, remoteEtag + "\n", "utf-8");
-            }
-        }
-        results.push({ model, path, sizeBytes, refreshed });
-    }
-    return results;
-}
-/**
- * LLM implementation using node-llama-cpp
- */
-// Default inactivity timeout: 5 minutes (keep models warm during typical search sessions)
-const DEFAULT_INACTIVITY_TIMEOUT_MS = 5 * 60 * 1000;
-const DEFAULT_EXPAND_CONTEXT_SIZE = 2048;
-function resolveExpandContextSize(configValue) {
-    if (configValue !== undefined) {
-        if (!Number.isInteger(configValue) || configValue <= 0) {
-            throw new Error(`Invalid expandContextSize: ${configValue}. Must be a positive integer.`);
-        }
-        return configValue;
-    }
-    const envValue = process.env.QMD_EXPAND_CONTEXT_SIZE?.trim();
-    if (!envValue)
-        return DEFAULT_EXPAND_CONTEXT_SIZE;
-    const parsed = Number.parseInt(envValue, 10);
-    if (!Number.isInteger(parsed) || parsed <= 0) {
-        process.stderr.write(`QMD Warning: invalid QMD_EXPAND_CONTEXT_SIZE="${envValue}", using default ${DEFAULT_EXPAND_CONTEXT_SIZE}.\n`);
-        return DEFAULT_EXPAND_CONTEXT_SIZE;
-    }
-    return parsed;
+function learnedModelHold(operation) {
+    throw commercialApiHold(`${operation} requires an approved commercial API adapter`);
 }
+/** Historical SDK name retained as a fail-closed compatibility adapter. */
 export class LlamaCpp {
-    _ciMode = !!process.env.CI;
-    llama = null;
-    embedModel = null;
-    embedContexts = [];
-    generateModel = null;
-    rerankModel = null;
-    rerankContexts = [];
-    embedModelUri;
-    generateModelUri;
-    rerankModelUri;
-    modelCacheDir;
-    expandContextSize;
-    // Ensure we don't load the same model/context concurrently (which can allocate duplicate VRAM).
-    embedModelLoadPromise = null;
-    generateModelLoadPromise = null;
-    rerankModelLoadPromise = null;
-    // Inactivity timer for auto-unloading models
-    inactivityTimer = null;
-    inactivityTimeoutMs;
-    disposeModelsOnInactivity;
-    // Track disposal state to prevent double-dispose
-    disposed = false;
+    static EMBED_CONTEXT_SIZE = 2048;
+    static RERANK_CONTEXT_SIZE = 2048;
+    static RERANK_TARGET_DOCS_PER_CONTEXT = 32;
+    static RERANK_TEMPLATE_OVERHEAD = 32;
+    embedModelName;
     constructor(config = {}) {
-        this.embedModelUri = config.embedModel || process.env.QMD_EMBED_MODEL || DEFAULT_EMBED_MODEL;
-        this.generateModelUri = config.generateModel || process.env.QMD_GENERATE_MODEL || DEFAULT_GENERATE_MODEL;
-        this.rerankModelUri = config.rerankModel || process.env.QMD_RERANK_MODEL || DEFAULT_RERANK_MODEL;
-        this.modelCacheDir = config.modelCacheDir || MODEL_CACHE_DIR;
-        this.expandContextSize = resolveExpandContextSize(config.expandContextSize);
-        this.inactivityTimeoutMs = config.inactivityTimeoutMs ?? DEFAULT_INACTIVITY_TIMEOUT_MS;
-        this.disposeModelsOnInactivity = config.disposeModelsOnInactivity ?? false;
-    }
-    get embedModelName() {
-        return this.embedModelUri;
-    }
-    /**
-     * Reset the inactivity timer. Called after each model operation.
-     * When timer fires, models are unloaded to free memory (if no active sessions).
-     */
-    touchActivity() {
-        // Clear existing timer
-        if (this.inactivityTimer) {
-            clearTimeout(this.inactivityTimer);
-            this.inactivityTimer = null;
-        }
-        // Only set timer if we have disposable contexts and timeout is enabled
-        if (this.inactivityTimeoutMs > 0 && this.hasLoadedContexts()) {
-            this.inactivityTimer = setTimeout(() => {
-                // Check if session manager allows unloading
-                // canUnloadLLM is defined later in this file - it checks the session manager
-                // We use dynamic import pattern to avoid circular dependency issues
-                if (typeof canUnloadLLM === 'function' && !canUnloadLLM()) {
-                    // Active sessions/operations - reschedule timer
-                    this.touchActivity();
-                    return;
-                }
-                this.unloadIdleResources().catch(err => {
-                    console.error("Error unloading idle resources:", err);
-                });
-            }, this.inactivityTimeoutMs);
-            // Don't keep process alive just for this timer
-            this.inactivityTimer.unref();
-        }
-    }
-    /**
-     * Check if any contexts are currently loaded (and therefore worth unloading on inactivity).
-     */
-    hasLoadedContexts() {
-        return !!(this.embedContexts.length > 0 || this.rerankContexts.length > 0);
-    }
-    /**
-     * Unload idle resources but keep the instance alive for future use.
-     *
-     * By default, this disposes contexts (and their dependent sequences), while keeping models loaded.
-     * This matches the intended lifecycle: model → context → sequence, where contexts are per-session.
-     */
-    async unloadIdleResources() {
-        // Don't unload if already disposed
-        if (this.disposed) {
-            return;
-        }
-        // Clear timer
-        if (this.inactivityTimer) {
-            clearTimeout(this.inactivityTimer);
-            this.inactivityTimer = null;
-        }
-        // Dispose contexts first
-        for (const ctx of this.embedContexts) {
-            await ctx.dispose();
-        }
-        this.embedContexts = [];
-        for (const ctx of this.rerankContexts) {
-            await ctx.dispose();
-        }
-        this.rerankContexts = [];
-        // Optionally dispose models too (opt-in)
-        if (this.disposeModelsOnInactivity) {
-            if (this.embedModel) {
-                await this.embedModel.dispose();
-                this.embedModel = null;
-            }
-            if (this.generateModel) {
-                await this.generateModel.dispose();
-                this.generateModel = null;
-            }
-            if (this.rerankModel) {
-                await this.rerankModel.dispose();
-                this.rerankModel = null;
-            }
-            // Reset load promises so models can be reloaded later
-            this.embedModelLoadPromise = null;
-            this.generateModelLoadPromise = null;
-            this.rerankModelLoadPromise = null;
-        }
-        // Note: We keep llama instance alive - it's lightweight
-    }
-    /**
-     * Ensure model cache directory exists
-     */
-    ensureModelCacheDir() {
-        if (!existsSync(this.modelCacheDir)) {
-            mkdirSync(this.modelCacheDir, { recursive: true });
-        }
-    }
-    /**
-     * Initialize the llama instance (lazy)
-     *
-     * Env-var controls (i-c28wngnd):
-     *   - QMD_DISABLE_LOCAL_LLM=1    : hard-disable; throws on first ensureLlama()
-     *                                  call. Use when the deployment must NEVER
-     *                                  load node-llama-cpp (e.g. headless cron
-     *                                  on a host without libvulkan-dev/glslc).
-     *   - QMD_LLAMA_GPU=off|none|... : force CPU-only (skip Vulkan probe).
-     *   - QMD_LLAMA_GPU=auto         : explicit opt-in to GPU probe even when
-     *                                  QMD_EMBED_ENDPOINT is set (rare; useful
-     *                                  for hybrid local-rerank + remote-embed).
-     *
-     * Auto-detect: when QMD_EMBED_ENDPOINT is set (HTTP embed provider, e.g.
-     * cron on `code` → ai.mm.mk → models:8082), we default to CPU-only because
-     * the embed path runs over HTTP and the only remaining local LLM consumers
-     * are rerank/query-expansion, which work fine on the prebuilt CPU binary
-     * and never need to invoke cmake-js-llama. This silences ~30s/run of
-     * Vulkan probe + cmake noise on headless LXCs.
-     */
-    async ensureLlama() {
-        if (!this.llama) {
-            // Hard-disable opt-out — fails fast so the caller knows. Throw early
-            // so any path that ignores the documented `EmbeddingProvider` route
-            // and reaches for the local LLM gets a loud, actionable error rather
-            // than a silent 30s Vulkan compile attempt.
-            if (isLocalLlmDisabled(process.env)) {
-                throw new Error("QMD_DISABLE_LOCAL_LLM=1 — local node-llama-cpp is disabled. " +
-                    "This deployment is configured for remote embeddings only; the " +
-                    "code path that reached `ensureLlama()` should route through an " +
-                    "EmbeddingProvider (set QMD_EMBED_ENDPOINT) instead. Unset " +
-                    "QMD_DISABLE_LOCAL_LLM to re-enable local rerank/expand.");
-            }
-            // Resolve GPU mode: explicit QMD_LLAMA_GPU wins, else auto-detect
-            // remote-only deployment (CPU when QMD_EMBED_ENDPOINT is set), else
-            // probe GPU normally for legacy local-only setups.
-            const gpuMode = resolveLlamaGpuMode(process.env);
-            const loadLlama = async (gpu) => await getLlama({
-                // `never` = load a prebuilt binary only; never invoke cmake at query
-                // time. When the GPU auto-probe picks a backend whose prebuilt binary
-                // is incompatible with the host (e.g. the `code` LXC has libvulkan.so.1
-                // but no GPU device and no glslc), `autoAttempt` would compile
-                // llama.cpp from source per-GPU — a 30-60s+ blocking stall that then
-                // fails for lack of glslc and leaves a half-built localBuilds/ dir,
-                // hanging interactive `qmd query`. `never` instead falls straight
-                // through the prebuilt candidate list (Vulkan -> CUDA -> CPU) and lands
-                // on the prebuilt CPU binary. node-llama-cpp ships prebuilts for every
-                // platform we deploy on, so the source-build fallback is dead weight.
-                // Completes i-c28wngnd (which only covered the QMD_EMBED_ENDPOINT=cpu
-                // path) for the interactive / gpu:"auto" path. (i-tgac7ig3)
-                build: "never",
-                logLevel: LlamaLogLevel.error,
-                gpu,
-            });
-            let llama;
-            if (gpuMode === "cpu") {
-                llama = await loadLlama(false);
-            }
-            else {
-                try {
-                    llama = await loadLlama("auto");
-                }
-                catch (err) {
-                    // GPU backend (e.g. Vulkan on headless/driverless machines) can throw at init.
-                    // Fall back to CPU so qmd still works.
-                    process.stderr.write(`QMD Warning: GPU init failed (${err instanceof Error ? err.message : String(err)}), falling back to CPU.\n`);
-                    llama = await loadLlama(false);
-                }
-            }
-            // Suppress the "running on CPU (slow)" warning when CPU was requested
-            // explicitly or auto-selected for a remote-only deployment — there's
-            // nothing the operator can do about it and the hint isn't relevant
-            // (embed runs via HTTP; only rerank/expand use the local CPU path).
-            if (llama.gpu === false && gpuMode === "auto") {
-                process.stderr.write("QMD Warning: no GPU acceleration, running on CPU (slow). Run 'qmd status' for details.\n");
-            }
-            this.llama = llama;
-        }
-        return this.llama;
-    }
-    /**
-     * Resolve a model URI to a local path, downloading if needed
-     */
-    async resolveModel(modelUri) {
-        this.ensureModelCacheDir();
-        // resolveModelFile handles HF URIs and downloads to the cache dir
-        return await resolveModelFile(modelUri, this.modelCacheDir);
-    }
-    /**
-     * Load embedding model (lazy)
-     */
-    async ensureEmbedModel() {
-        if (this.embedModel) {
-            return this.embedModel;
-        }
-        if (this.embedModelLoadPromise) {
-            return await this.embedModelLoadPromise;
-        }
-        this.embedModelLoadPromise = (async () => {
-            const llama = await this.ensureLlama();
-            const modelPath = await this.resolveModel(this.embedModelUri);
-            const model = await llama.loadModel({ modelPath });
-            this.embedModel = model;
-            // Model loading counts as activity - ping to keep alive
-            this.touchActivity();
-            return model;
-        })();
-        try {
-            return await this.embedModelLoadPromise;
-        }
-        finally {
-            // Keep the resolved model cached; clear only the in-flight promise.
-            this.embedModelLoadPromise = null;
-        }
+        this.embedModelName = config.embedModel ?? process.env.QMD_EMBED_MODEL_ID ?? DEFAULT_EMBED_MODEL_URI;
     }
-    /**
-     * Compute how many parallel contexts to create.
-     *
-     * GPU: constrained by VRAM (25% of free, capped at 8).
-     * CPU: constrained by cores. Splitting threads across contexts enables
-     *      true parallelism (each context runs on its own cores). Use at most
-     *      half the math cores, with at least 4 threads per context.
-     */
-    async computeParallelism(perContextMB) {
-        const llama = await this.ensureLlama();
-        if (llama.gpu) {
-            try {
-                const vram = await llama.getVramState();
-                const freeMB = vram.free / (1024 * 1024);
-                const maxByVram = Math.floor((freeMB * 0.25) / perContextMB);
-                return Math.max(1, Math.min(8, maxByVram));
-            }
-            catch {
-                return 2;
-            }
-        }
-        // CPU: split cores across contexts. At least 4 threads per context.
-        const cores = llama.cpuMathCores || 4;
-        const maxContexts = Math.floor(cores / 4);
-        return Math.max(1, Math.min(4, maxContexts));
+    async tokenize(_text) {
+        return learnedModelHold("tokenization");
     }
-    /**
-     * Get the number of threads each context should use, given N parallel contexts.
-     * Splits available math cores evenly across contexts.
-     */
-    async threadsPerContext(parallelism) {
-        const llama = await this.ensureLlama();
-        if (llama.gpu)
-            return 0; // GPU: let the library decide
-        const cores = llama.cpuMathCores || 4;
-        return Math.max(1, Math.floor(cores / parallelism));
+    async countTokens(_text) {
+        return learnedModelHold("token counting");
     }
-    /**
-     * Load embedding contexts (lazy). Creates multiple for parallel embedding.
-     * Uses promise guard to prevent concurrent context creation race condition.
-     */
-    embedContextsCreatePromise = null;
-    async ensureEmbedContexts() {
-        if (this.embedContexts.length > 0) {
-            this.touchActivity();
-            return this.embedContexts;
-        }
-        if (this.embedContextsCreatePromise) {
-            return await this.embedContextsCreatePromise;
-        }
-        this.embedContextsCreatePromise = (async () => {
-            const model = await this.ensureEmbedModel();
-            // Embed contexts are ~143 MB each (nomic-embed 2048 ctx)
-            const n = await this.computeParallelism(150);
-            const threads = await this.threadsPerContext(n);
-            for (let i = 0; i < n; i++) {
-                try {
-                    this.embedContexts.push(await model.createEmbeddingContext({
-                        contextSize: LlamaCpp.EMBED_CONTEXT_SIZE,
-                        ...(threads > 0 ? { threads } : {}),
-                    }));
-                }
-                catch {
-                    if (this.embedContexts.length === 0)
-                        throw new Error("Failed to create any embedding context");
-                    break;
-                }
-            }
-            this.touchActivity();
-            return this.embedContexts;
-        })();
-        try {
-            return await this.embedContextsCreatePromise;
-        }
-        finally {
-            this.embedContextsCreatePromise = null;
-        }
+    async detokenize(_tokens) {
+        return learnedModelHold("detokenization");
     }
-    /**
-     * Get a single embed context (for single-embed calls). Uses first from pool.
-     */
-    async ensureEmbedContext() {
-        const contexts = await this.ensureEmbedContexts();
-        return contexts[0];
+    async embed(_text, _options = {}) {
+        return learnedModelHold("embedding");
     }
-    /**
-     * Load generation model (lazy) - context is created fresh per call
-     */
-    async ensureGenerateModel() {
-        if (!this.generateModel) {
-            if (this.generateModelLoadPromise) {
-                return await this.generateModelLoadPromise;
-            }
-            this.generateModelLoadPromise = (async () => {
-                const llama = await this.ensureLlama();
-                const modelPath = await this.resolveModel(this.generateModelUri);
-                const model = await llama.loadModel({ modelPath });
-                this.generateModel = model;
-                return model;
-            })();
-            try {
-                await this.generateModelLoadPromise;
-            }
-            finally {
-                this.generateModelLoadPromise = null;
-            }
-        }
-        this.touchActivity();
-        if (!this.generateModel) {
-            throw new Error("Generate model not loaded");
-        }
-        return this.generateModel;
+    async embedBatch(_texts, _options = {}) {
+        return learnedModelHold("batch embedding");
     }
-    /**
-     * Load rerank model (lazy)
-     */
-    async ensureRerankModel() {
-        if (this.rerankModel) {
-            return this.rerankModel;
-        }
-        if (this.rerankModelLoadPromise) {
-            return await this.rerankModelLoadPromise;
-        }
-        this.rerankModelLoadPromise = (async () => {
-            const llama = await this.ensureLlama();
-            const modelPath = await this.resolveModel(this.rerankModelUri);
-            const model = await llama.loadModel({ modelPath });
-            this.rerankModel = model;
-            // Model loading counts as activity - ping to keep alive
-            this.touchActivity();
-            return model;
-        })();
-        try {
-            return await this.rerankModelLoadPromise;
-        }
-        finally {
-            this.rerankModelLoadPromise = null;
-        }
-    }
-    /**
-     * Load rerank contexts (lazy). Creates multiple contexts for parallel ranking.
-     * Each context has its own sequence, so they can evaluate independently.
-     *
-     * Tuning choices:
-     * - contextSize 1024: reranking chunks are ~800 tokens max, 1024 is plenty
-     * - flashAttention: ~20% less VRAM per context (568 vs 711 MB)
-     * - Combined: drops from 11.6 GB (auto, no flash) to 568 MB per context (20×)
-     */
-    // Qwen3 reranker template adds ~200 tokens overhead (system prompt, tags, etc.)
-    // Default 2048 was too small for longer documents (e.g. session transcripts,
-    // CJK text, or large markdown files) — callers hit "input lengths exceed
-    // context size" errors even after truncation because the overhead estimate
-    // was insufficient.  4096 comfortably fits the largest real-world chunks
-    // while staying well below the 40 960-token auto size.
-    // Override with QMD_RERANK_CONTEXT_SIZE env var if you need more headroom.
-    static RERANK_CONTEXT_SIZE = (() => {
-        const v = parseInt(process.env.QMD_RERANK_CONTEXT_SIZE ?? "", 10);
-        return Number.isFinite(v) && v > 0 ? v : 4096;
-    })();
-    static EMBED_CONTEXT_SIZE = (() => {
-        const v = parseInt(process.env.QMD_EMBED_CONTEXT_SIZE ?? "", 10);
-        return Number.isFinite(v) && v > 0 ? v : 2048;
-    })();
-    async ensureRerankContexts() {
-        if (this.rerankContexts.length === 0) {
-            const model = await this.ensureRerankModel();
-            // ~960 MB per context with flash attention at contextSize 2048
-            const n = Math.min(await this.computeParallelism(1000), 4);
-            const threads = await this.threadsPerContext(n);
-            for (let i = 0; i < n; i++) {
-                try {
-                    this.rerankContexts.push(await model.createRankingContext({
-                        contextSize: LlamaCpp.RERANK_CONTEXT_SIZE,
-                        flashAttention: true,
-                        ...(threads > 0 ? { threads } : {}),
-                    }));
-                }
-                catch {
-                    if (this.rerankContexts.length === 0) {
-                        // Flash attention might not be supported — retry without it
-                        try {
-                            this.rerankContexts.push(await model.createRankingContext({
-                                contextSize: LlamaCpp.RERANK_CONTEXT_SIZE,
-                                ...(threads > 0 ? { threads } : {}),
-                            }));
-                        }
-                        catch {
-                            throw new Error("Failed to create any rerank context");
-                        }
-                    }
-                    break;
-                }
-            }
-        }
-        this.touchActivity();
-        return this.rerankContexts;
-    }
-    // ==========================================================================
-    // Tokenization
-    // ==========================================================================
-    /**
-     * Tokenize text using the embedding model's tokenizer
-     * Returns tokenizer tokens (opaque type from node-llama-cpp)
-     */
-    async tokenize(text) {
-        await this.ensureEmbedContext(); // Ensure model is loaded
-        if (!this.embedModel) {
-            throw new Error("Embed model not loaded");
-        }
-        return this.embedModel.tokenize(text);
-    }
-    /**
-     * Count tokens in text using the embedding model's tokenizer
-     */
-    async countTokens(text) {
-        const tokens = await this.tokenize(text);
-        return tokens.length;
-    }
-    /**
-     * Detokenize token IDs back to text
-     */
-    async detokenize(tokens) {
-        await this.ensureEmbedContext();
-        if (!this.embedModel) {
-            throw new Error("Embed model not loaded");
-        }
-        return this.embedModel.detokenize(tokens);
-    }
-    // ==========================================================================
-    // Core API methods
-    // ==========================================================================
-    /**
-     * Truncate text to fit within the embedding model's context window.
-     * Uses the model's own tokenizer for accurate token counting, then
-     * detokenizes back to text if truncation is needed.
-     * Returns the (possibly truncated) text and whether truncation occurred.
-     */
-    async truncateToContextSize(text) {
-        if (!this.embedModel)
-            return { text, truncated: false };
-        const maxTokens = this.embedModel.trainContextSize;
-        if (maxTokens <= 0)
-            return { text, truncated: false };
-        const tokens = this.embedModel.tokenize(text);
-        if (tokens.length <= maxTokens)
-            return { text, truncated: false };
-        // Leave a small margin (4 tokens) for BOS/EOS overhead
-        const safeLimit = Math.max(1, maxTokens - 4);
-        const truncatedTokens = tokens.slice(0, safeLimit);
-        const truncatedText = this.embedModel.detokenize(truncatedTokens);
-        return { text: truncatedText, truncated: true };
-    }
-    async embed(text, options = {}) {
-        // Ping activity at start to keep models alive during this operation
-        this.touchActivity();
-        try {
-            const context = await this.ensureEmbedContext();
-            // Guard: truncate text that exceeds model context window to prevent GGML crash
-            const { text: safeText, truncated } = await this.truncateToContextSize(text);
-            if (truncated) {
-                console.warn(`⚠ Text truncated to fit embedding context (${this.embedModel?.trainContextSize} tokens)`);
-            }
-            const embedding = await context.getEmbeddingFor(safeText);
-            return {
-                embedding: Array.from(embedding.vector),
-                model: options.model ?? this.embedModelUri,
-            };
-        }
-        catch (error) {
-            console.error("Embedding error:", error);
-            return null;
-        }
-    }
-    /**
-     * Batch embed multiple texts efficiently
-     * Uses Promise.all for parallel embedding - node-llama-cpp handles batching internally
-     */
-    async embedBatch(texts, options = {}) {
-        if (this._ciMode)
-            throw new Error("LLM operations are disabled in CI (set CI=true)");
-        // Ping activity at start to keep models alive during this operation
-        this.touchActivity();
-        if (texts.length === 0)
-            return [];
-        try {
-            const contexts = await this.ensureEmbedContexts();
-            const n = contexts.length;
-            if (n === 1) {
-                // Single context: sequential (no point splitting)
-                const context = contexts[0];
-                const embeddings = [];
-                for (const text of texts) {
-                    try {
-                        const { text: safeText, truncated } = await this.truncateToContextSize(text);
-                        if (truncated) {
-                            console.warn(`⚠ Batch text truncated to fit embedding context (${this.embedModel?.trainContextSize} tokens)`);
-                        }
-                        const embedding = await context.getEmbeddingFor(safeText);
-                        this.touchActivity();
-                        embeddings.push({ embedding: Array.from(embedding.vector), model: options.model ?? this.embedModelUri });
-                    }
-                    catch (err) {
-                        console.error("Embedding error for text:", err);
-                        embeddings.push(null);
-                    }
-                }
-                return embeddings;
-            }
-            // Multiple contexts: split texts across contexts for parallel evaluation
-            const chunkSize = Math.ceil(texts.length / n);
-            const chunks = Array.from({ length: n }, (_, i) => texts.slice(i * chunkSize, (i + 1) * chunkSize));
-            const chunkResults = await Promise.all(chunks.map(async (chunk, i) => {
-                const ctx = contexts[i];
-                const results = [];
-                for (const text of chunk) {
-                    try {
-                        const { text: safeText, truncated } = await this.truncateToContextSize(text);
-                        if (truncated) {
-                            console.warn(`⚠ Batch text truncated to fit embedding context (${this.embedModel?.trainContextSize} tokens)`);
-                        }
-                        const embedding = await ctx.getEmbeddingFor(safeText);
-                        this.touchActivity();
-                        results.push({ embedding: Array.from(embedding.vector), model: options.model ?? this.embedModelUri });
-                    }
-                    catch (err) {
-                        console.error("Embedding error for text:", err);
-                        results.push(null);
-                    }
-                }
-                return results;
-            }));
-            return chunkResults.flat();
-        }
-        catch (error) {
-            console.error("Batch embedding error:", error);
-            return texts.map(() => null);
-        }
-    }
-    async generate(prompt, options = {}) {
-        if (this._ciMode)
-            throw new Error("LLM operations are disabled in CI (set CI=true)");
-        // Ping activity at start to keep models alive during this operation
-        this.touchActivity();
-        // Ensure model is loaded
-        await this.ensureGenerateModel();
-        // Create fresh context -> sequence -> session for each call
-        const context = await this.generateModel.createContext();
-        const sequence = context.getSequence();
-        const session = new LlamaChatSession({ contextSequence: sequence });
-        const maxTokens = options.maxTokens ?? 150;
-        // Qwen3 recommends temp=0.7, topP=0.8, topK=20 for non-thinking mode
-        // DO NOT use greedy decoding (temp=0) - causes repetition loops
-        const temperature = options.temperature ?? 0.7;
-        let result = "";
-        try {
-            await session.prompt(prompt, {
-                maxTokens,
-                temperature,
-                topK: 20,
-                topP: 0.8,
-                onTextChunk: (text) => {
-                    result += text;
-                },
-            });
-            return {
-                text: result,
-                model: this.generateModelUri,
-                done: true,
-            };
-        }
-        finally {
-            // Dispose context (which disposes dependent sequences/sessions per lifecycle rules)
-            await context.dispose();
-        }
+    async generate(_prompt, _options = {}) {
+        return learnedModelHold("generation");
     }
     async modelExists(modelUri) {
-        // For HuggingFace URIs, we assume they exist
-        // For local paths, check if file exists
-        if (modelUri.startsWith("hf:")) {
-            return { name: modelUri, exists: true };
-        }
-        const exists = existsSync(modelUri);
-        return {
-            name: modelUri,
-            exists,
-            path: exists ? modelUri : undefined,
-        };
+        return { name: modelUri, exists: false };
     }
-    // ==========================================================================
-    // High-level abstractions
-    // ==========================================================================
-    async expandQuery(query, options = {}) {
-        if (this._ciMode)
-            throw new Error("LLM operations are disabled in CI (set CI=true)");
-        // Ping activity at start to keep models alive during this operation
-        this.touchActivity();
-        const llama = await this.ensureLlama();
-        await this.ensureGenerateModel();
-        const includeLexical = options.includeLexical ?? true;
-        const context = options.context;
-        const grammar = await llama.createGrammar({
-            grammar: `
-        root ::= line+
-        line ::= type ": " content "\\n"
-        type ::= "lex" | "vec" | "hyde"
-        content ::= [^\\n]+
-      `
-        });
-        const intent = options.intent;
-        const prompt = intent
-            ? `/no_think Expand this search query: ${query}\nQuery intent: ${intent}`
-            : `/no_think Expand this search query: ${query}`;
-        // Create a bounded context for expansion to prevent large default VRAM allocations.
-        const genContext = await this.generateModel.createContext({
-            contextSize: this.expandContextSize,
-        });
-        const sequence = genContext.getSequence();
-        const session = new LlamaChatSession({ contextSequence: sequence });
-        try {
-            // Qwen3 recommended settings for non-thinking mode:
-            // temp=0.7, topP=0.8, topK=20, presence_penalty for repetition
-            // DO NOT use greedy decoding (temp=0) - causes infinite loops
-            const result = await session.prompt(prompt, {
-                grammar,
-                maxTokens: 600,
-                temperature: 0.7,
-                topK: 20,
-                topP: 0.8,
-                repeatPenalty: {
-                    lastTokens: 64,
-                    presencePenalty: 0.5,
-                },
-            });
-            const lines = result.trim().split("\n");
-            const queryLower = query.toLowerCase();
-            const queryTerms = queryLower.replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter(Boolean);
-            const hasQueryTerm = (text) => {
-                const lower = text.toLowerCase();
-                if (queryTerms.length === 0)
-                    return true;
-                return queryTerms.some(term => lower.includes(term));
-            };
-            const queryables = lines.map(line => {
-                const colonIdx = line.indexOf(":");
-                if (colonIdx === -1)
-                    return null;
-                const type = line.slice(0, colonIdx).trim();
-                if (type !== 'lex' && type !== 'vec' && type !== 'hyde')
-                    return null;
-                const text = line.slice(colonIdx + 1).trim();
-                if (!hasQueryTerm(text))
-                    return null;
-                return { type: type, text };
-            }).filter((q) => q !== null);
-            // Filter out lex entries if not requested
-            const filtered = includeLexical ? queryables : queryables.filter(q => q.type !== 'lex');
-            if (filtered.length > 0)
-                return filtered;
-            const fallback = [
-                { type: 'hyde', text: `Information about ${query}` },
-                { type: 'lex', text: query },
-                { type: 'vec', text: query },
-            ];
-            return includeLexical ? fallback : fallback.filter(q => q.type !== 'lex');
-        }
-        catch (error) {
-            console.error("Structured query expansion failed:", error);
-            // Fallback to original query
-            const fallback = [{ type: 'vec', text: query }];
-            if (includeLexical)
-                fallback.unshift({ type: 'lex', text: query });
-            return fallback;
-        }
-        finally {
-            await genContext.dispose();
-        }
+    async expandQuery(_query, _options = {}) {
+        return learnedModelHold("query expansion");
     }
-    // Qwen3 reranker chat template overhead (system prompt, tags, separators).
-    // Measured at ~350 tokens on real queries; use 512 as a safe upper bound so
-    // the truncation budget never lets a document slip past the context limit.
-    static RERANK_TEMPLATE_OVERHEAD = 512;
-    static RERANK_TARGET_DOCS_PER_CONTEXT = 10;
-    async rerank(query, documents, options = {}) {
-        if (this._ciMode)
-            throw new Error("LLM operations are disabled in CI (set CI=true)");
-        // Ping activity at start to keep models alive during this operation
-        this.touchActivity();
-        const contexts = await this.ensureRerankContexts();
-        const model = await this.ensureRerankModel();
-        // Truncate documents that would exceed the rerank context size.
-        // Budget = contextSize - template overhead - query tokens
-        const queryTokens = model.tokenize(query).length;
-        const maxDocTokens = LlamaCpp.RERANK_CONTEXT_SIZE - LlamaCpp.RERANK_TEMPLATE_OVERHEAD - queryTokens;
-        const truncationCache = new Map();
-        const truncatedDocs = documents.map((doc) => {
-            const cached = truncationCache.get(doc.text);
-            if (cached !== undefined) {
-                return cached === doc.text ? doc : { ...doc, text: cached };
-            }
-            const tokens = model.tokenize(doc.text);
-            const truncatedText = tokens.length <= maxDocTokens
-                ? doc.text
-                : model.detokenize(tokens.slice(0, maxDocTokens));
-            truncationCache.set(doc.text, truncatedText);
-            if (truncatedText === doc.text)
-                return doc;
-            return { ...doc, text: truncatedText };
-        });
-        // Deduplicate identical effective texts before scoring.
-        // This avoids redundant work for repeated chunks and fixes collisions where
-        // multiple docs map to the same chunk text.
-        const textToDocs = new Map();
-        truncatedDocs.forEach((doc, index) => {
-            const existing = textToDocs.get(doc.text);
-            if (existing) {
-                existing.push({ file: doc.file, index });
-            }
-            else {
-                textToDocs.set(doc.text, [{ file: doc.file, index }]);
-            }
-        });
-        // Extract just the text for ranking
-        const texts = Array.from(textToDocs.keys());
-        // Split documents across contexts for parallel evaluation.
-        // Each context has its own sequence with a lock, so parallelism comes
-        // from multiple contexts evaluating different chunks simultaneously.
-        const activeContextCount = Math.max(1, Math.min(contexts.length, Math.ceil(texts.length / LlamaCpp.RERANK_TARGET_DOCS_PER_CONTEXT)));
-        const activeContexts = contexts.slice(0, activeContextCount);
-        const chunkSize = Math.ceil(texts.length / activeContexts.length);
-        const chunks = Array.from({ length: activeContexts.length }, (_, i) => texts.slice(i * chunkSize, (i + 1) * chunkSize)).filter(chunk => chunk.length > 0);
-        const allScores = await Promise.all(chunks.map((chunk, i) => activeContexts[i].rankAll(query, chunk)));
-        // Reassemble scores in original order and sort
-        const flatScores = allScores.flat();
-        const ranked = texts
-            .map((text, i) => ({ document: text, score: flatScores[i] }))
-            .sort((a, b) => b.score - a.score);
-        // Map back to our result format.
-        const results = [];
-        for (const item of ranked) {
-            const docInfos = textToDocs.get(item.document) ?? [];
-            for (const docInfo of docInfos) {
-                results.push({
-                    file: docInfo.file,
-                    score: item.score,
-                    index: docInfo.index,
-                });
-            }
-        }
-        return {
-            results,
-            model: this.rerankModelUri,
-        };
+    async rerank(_query, _documents, _options = {}) {
+        return learnedModelHold("reranking");
     }
-    /**
-     * Get device/GPU info for status display.
-     * Initializes llama if not already done.
-     */
     async getDeviceInfo() {
-        const llama = await this.ensureLlama();
-        const gpuDevices = await llama.getGpuDeviceNames();
-        let vram;
-        if (llama.gpu) {
-            try {
-                const state = await llama.getVramState();
-                vram = { total: state.total, used: state.used, free: state.free };
-            }
-            catch { /* no vram info */ }
-        }
-        return {
-            gpu: llama.gpu,
-            gpuOffloading: llama.supportsGpuOffloading,
-            gpuDevices,
-            vram,
-            cpuCores: llama.cpuMathCores,
-        };
-    }
-    async dispose() {
-        // Prevent double-dispose
-        if (this.disposed) {
-            return;
-        }
-        this.disposed = true;
-        // Clear inactivity timer
-        if (this.inactivityTimer) {
-            clearTimeout(this.inactivityTimer);
-            this.inactivityTimer = null;
-        }
-        // Disposing llama cascades to models and contexts automatically
-        // See: https://node-llama-cpp.withcat.ai/guide/objects-lifecycle
-        // Note: llama.dispose() can hang indefinitely, so we use a timeout
-        if (this.llama) {
-            const disposePromise = this.llama.dispose();
-            const timeoutPromise = new Promise((resolve) => setTimeout(resolve, 1000));
-            await Promise.race([disposePromise, timeoutPromise]);
-        }
-        // Clear references
-        this.embedContexts = [];
-        this.rerankContexts = [];
-        this.embedModel = null;
-        this.generateModel = null;
-        this.rerankModel = null;
-        this.llama = null;
-        // Clear any in-flight load/create promises
-        this.embedModelLoadPromise = null;
-        this.embedContextsCreatePromise = null;
-        this.generateModelLoadPromise = null;
-        this.rerankModelLoadPromise = null;
-    }
-}
-// =============================================================================
-// Session Management Layer
-// =============================================================================
-/**
- * Manages LLM session lifecycle with reference counting.
- * Coordinates with LlamaCpp idle timeout to prevent disposal during active sessions.
- */
-class LLMSessionManager {
-    llm;
-    _activeSessionCount = 0;
-    _inFlightOperations = 0;
-    constructor(llm) {
-        this.llm = llm;
-    }
-    get activeSessionCount() {
-        return this._activeSessionCount;
-    }
-    get inFlightOperations() {
-        return this._inFlightOperations;
-    }
-    /**
-     * Returns true only when both session count and in-flight operations are 0.
-     * Used by LlamaCpp to determine if idle unload is safe.
-     */
-    canUnload() {
-        return this._activeSessionCount === 0 && this._inFlightOperations === 0;
-    }
-    acquire() {
-        this._activeSessionCount++;
-    }
-    release() {
-        this._activeSessionCount = Math.max(0, this._activeSessionCount - 1);
-    }
-    operationStart() {
-        this._inFlightOperations++;
-    }
-    operationEnd() {
-        this._inFlightOperations = Math.max(0, this._inFlightOperations - 1);
-    }
-    getLlamaCpp() {
-        return this.llm;
+        return { gpu: false, gpuOffloading: false, gpuDevices: [], cpuCores: cpus().length };
     }
+    async unloadIdleResources() { }
+    async dispose() { }
 }
-/**
- * Error thrown when an operation is attempted on a released or aborted session.
- */
 export class SessionReleasedError extends Error {
     constructor(message = "LLM session has been released or aborted") {
         super(message);
         this.name = "SessionReleasedError";
     }
 }
-/**
- * Scoped LLM session with automatic lifecycle management.
- * Wraps LlamaCpp methods with operation tracking and abort handling.
- */
-class LLMSession {
-    manager;
+class CommercialHoldSession {
+    llm;
     released = false;
-    abortController;
-    maxDurationTimer = null;
-    name;
-    constructor(manager, options = {}) {
-        this.manager = manager;
-        this.name = options.name || "unnamed";
-        this.abortController = new AbortController();
-        // Link external abort signal if provided
-        if (options.signal) {
-            if (options.signal.aborted) {
-                this.abortController.abort(options.signal.reason);
-            }
-            else {
-                options.signal.addEventListener("abort", () => {
-                    this.abortController.abort(options.signal.reason);
-                }, { once: true });
-            }
-        }
-        // Set up max duration timer
-        const maxDuration = options.maxDuration ?? 10 * 60 * 1000; // Default 10 minutes
-        if (maxDuration > 0) {
-            this.maxDurationTimer = setTimeout(() => {
-                this.abortController.abort(new Error(`Session "${this.name}" exceeded max duration of ${maxDuration}ms`));
-            }, maxDuration);
-            this.maxDurationTimer.unref(); // Don't keep process alive
-        }
-        // Acquire session lease
-        this.manager.acquire();
+    controller = new AbortController();
+    constructor(llm, options = {}) {
+        this.llm = llm;
+        if (options.signal?.aborted)
+            this.controller.abort(options.signal.reason);
+        options.signal?.addEventListener("abort", () => this.controller.abort(options.signal?.reason), { once: true });
     }
     get isValid() {
-        return !this.released && !this.abortController.signal.aborted;
+        return !this.released && !this.controller.signal.aborted;
     }
     get signal() {
-        return this.abortController.signal;
+        return this.controller.signal;
     }
-    /**
-     * Release the session and decrement ref count.
-     * Called automatically by withLLMSession when the callback completes.
-     */
     release() {
-        if (this.released)
-            return;
         this.released = true;
-        if (this.maxDurationTimer) {
-            clearTimeout(this.maxDurationTimer);
-            this.maxDurationTimer = null;
-        }
-        this.abortController.abort(new Error("Session released"));
-        this.manager.release();
+        this.controller.abort(new SessionReleasedError());
     }
-    /**
-     * Wrap an operation with tracking and abort checking.
-     */
-    async withOperation(fn) {
-        if (!this.isValid) {
+    assertValid() {
+        if (!this.isValid)
             throw new SessionReleasedError();
-        }
-        this.manager.operationStart();
-        try {
-            // Check abort before starting
-            if (this.abortController.signal.aborted) {
-                throw new SessionReleasedError(this.abortController.signal.reason?.message || "Session aborted");
-            }
-            return await fn();
-        }
-        finally {
-            this.manager.operationEnd();
-        }
     }
     async embed(text, options) {
-        return this.withOperation(() => this.manager.getLlamaCpp().embed(text, options));
+        this.assertValid();
+        return this.llm.embed(text, options);
     }
     async embedBatch(texts, options) {
-        return this.withOperation(() => this.manager.getLlamaCpp().embedBatch(texts, options));
+        this.assertValid();
+        return this.llm.embedBatch(texts, options);
     }
     async expandQuery(query, options) {
-        return this.withOperation(() => this.manager.getLlamaCpp().expandQuery(query, options));
+        this.assertValid();
+        return this.llm.expandQuery(query, options);
     }
     async rerank(query, documents, options) {
-        return this.withOperation(() => this.manager.getLlamaCpp().rerank(query, documents, options));
-    }
-}
-// Session manager for the default LlamaCpp instance
-let defaultSessionManager = null;
-/**
- * Get the session manager for the default LlamaCpp instance.
- */
-function getSessionManager() {
-    const llm = getDefaultLlamaCpp();
-    if (!defaultSessionManager || defaultSessionManager.getLlamaCpp() !== llm) {
-        defaultSessionManager = new LLMSessionManager(llm);
+        this.assertValid();
+        return this.llm.rerank(query, documents, options);
     }
-    return defaultSessionManager;
 }
-/**
- * Execute a function with a scoped LLM session.
- * The session provides lifecycle guarantees - resources won't be disposed mid-operation.
- *
- * @example
- * ```typescript
- * await withLLMSession(async (session) => {
- *   const expanded = await session.expandQuery(query);
- *   const embeddings = await session.embedBatch(texts);
- *   const reranked = await session.rerank(query, docs);
- *   return reranked;
- * }, { maxDuration: 10 * 60 * 1000, name: 'querySearch' });
- * ```
- */
-export async function withLLMSession(fn, options) {
-    const manager = getSessionManager();
-    const session = new LLMSession(manager, options);
-    try {
-        return await fn(session);
-    }
-    finally {
-        session.release();
-    }
-}
-/**
- * Execute a function with a scoped LLM session using a specific LlamaCpp instance.
- * Unlike withLLMSession, this does not use the global singleton.
- */
-export async function withLLMSessionForLlm(llm, fn, options) {
-    const manager = new LLMSessionManager(llm);
-    const session = new LLMSession(manager, options);
-    try {
-        return await fn(session);
-    }
-    finally {
-        session.release();
-    }
-}
-/**
- * Check if idle unload is safe (no active sessions or operations).
- * Used internally by LlamaCpp idle timer.
- */
-export function canUnloadLLM() {
-    if (!defaultSessionManager)
-        return true;
-    return defaultSessionManager.canUnload();
-}
-// =============================================================================
-// Singleton for default LlamaCpp instance
-// =============================================================================
 let defaultLlamaCpp = null;
-/**
- * Get the default LlamaCpp instance (creates one if needed)
- */
 export function getDefaultLlamaCpp() {
-    if (!defaultLlamaCpp) {
-        defaultLlamaCpp = new LlamaCpp();
-    }
+    defaultLlamaCpp ??= new LlamaCpp();
     return defaultLlamaCpp;
 }
-/**
- * Set a custom default LlamaCpp instance (useful for testing)
- */
 export function setDefaultLlamaCpp(llm) {
     defaultLlamaCpp = llm;
 }
-/**
- * Dispose the default LlamaCpp instance if it exists.
- * Call this before process exit to prevent NAPI crashes.
- */
 export async function disposeDefaultLlamaCpp() {
-    if (defaultLlamaCpp) {
+    if (defaultLlamaCpp)
         await defaultLlamaCpp.dispose();
-        defaultLlamaCpp = null;
+    defaultLlamaCpp = null;
+}
+export async function withLLMSession(fn, options = {}) {
+    return withLLMSessionForLlm(getDefaultLlamaCpp(), fn, options);
+}
+export async function withLLMSessionForLlm(llm, fn, options = {}) {
+    const session = new CommercialHoldSession(llm, options);
+    try {
+        return await fn(session);
+    }
+    finally {
+        session.release();
     }
 }
+export function canUnloadLLM() {
+    return true;
+}

+ 33 - 21
dist/mcp/server.js

@@ -17,32 +17,44 @@ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/
 import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
 import { z } from "zod";
 import { existsSync } from "fs";
-import { createStore, extractSnippet, addLineNumbers, getDefaultDbPath, DEFAULT_MULTI_GET_MAX_BYTES, createEmbeddingProvider, resolveProviderKind, } from "../index.js";
+import { createStore, extractSnippet, addLineNumbers, getDefaultDbPath, DEFAULT_MULTI_GET_MAX_BYTES, createEmbeddingProvider, } from "../index.js";
 import { getConfigPath } from "../collections.js";
 /**
- * Build a query-side embedding provider (i-loazq6ze) for MCP server start.
- * Mirrors `buildQueryEmbedProvider` in the CLI: returns `undefined` when
- * the user has not opted into a remote provider, preserving pre-patch
- * behavior (local llama-cpp). Construction errors are logged and the
- * server falls back to the legacy path.
+ * Resolve the commercial provider only when a semantic operation first uses
+ * it. MCP startup and deterministic BM25/document tools remain available when
+ * commercial credentials are absent; semantic operations fail with typed HOLD.
  */
-function buildMcpEmbedProvider() {
-    const env = process.env;
-    const envOptIn = !!(env.QMD_EMBED_PROVIDER ||
-        env.QMD_EMBED_ENDPOINT ||
-        env.QMD_EMBED_AUTO_FALLBACK);
-    // Probe resolved kind via the factory's standard precedence (env + config).
-    const resolved = resolveProviderKind({});
-    if (!envOptIn && resolved === "local")
-        return undefined;
-    try {
-        return createEmbeddingProvider({});
+class LazyMcpEmbeddingProvider {
+    kind = "openai";
+    provider;
+    resolve() {
+        this.provider ??= createEmbeddingProvider({});
+        return this.provider;
+    }
+    getModelId() {
+        return this.resolve().getModelId();
+    }
+    getDimensions() {
+        return this.provider?.getDimensions();
+    }
+    healthcheck(signal) {
+        return this.resolve().healthcheck(signal);
     }
-    catch (err) {
-        // Log + fall through to undefined so legacy local path is used.
-        process.stderr.write(`[qmd mcp] WARN failed to build embedding provider — using local fallback: ${err instanceof Error ? err.message : String(err)}\n`);
-        return undefined;
+    embed(text, options) {
+        return this.resolve().embed(text, options);
     }
+    embedBatch(texts, options) {
+        return this.resolve().embedBatch(texts, options);
+    }
+    getLastError() {
+        return this.provider?.getLastError?.();
+    }
+    async dispose() {
+        await this.provider?.dispose();
+    }
+}
+function buildMcpEmbedProvider() {
+    return new LazyMcpEmbeddingProvider();
 }
 // =============================================================================
 // Helper functions

+ 8 - 0
dist/model-policy.d.ts

@@ -0,0 +1,8 @@
+export declare const COMMERCIAL_API_HOLD_CODE: "QMD_COMMERCIAL_API_HOLD";
+/** Fail-closed result for learned-model work without an approved commercial API. */
+export declare class CommercialApiHoldError extends Error {
+    readonly code: "QMD_COMMERCIAL_API_HOLD";
+    readonly disposition: "HOLD";
+    constructor(reason: string);
+}
+export declare function commercialApiHold(reason: string): CommercialApiHoldError;

+ 13 - 0
dist/model-policy.js

@@ -0,0 +1,13 @@
+export const COMMERCIAL_API_HOLD_CODE = "QMD_COMMERCIAL_API_HOLD";
+/** Fail-closed result for learned-model work without an approved commercial API. */
+export class CommercialApiHoldError extends Error {
+    code = COMMERCIAL_API_HOLD_CODE;
+    disposition = "HOLD";
+    constructor(reason) {
+        super(`[${COMMERCIAL_API_HOLD_CODE}] HOLD: ${reason}`);
+        this.name = "CommercialApiHoldError";
+    }
+}
+export function commercialApiHold(reason) {
+    return new CommercialApiHoldError(reason);
+}

+ 12 - 23
dist/store.d.ts

@@ -343,13 +343,12 @@ export type EmbedOptions = {
     chunkStrategy?: ChunkStrategy;
     onProgress?: (info: EmbedProgress) => void;
     /**
-     * Optional embedding provider. When supplied, embeddings are routed through
-     * this provider (HTTP, GPU worker, etc.) instead of the local llama.cpp
-     * session path. The provider's `getModelId()` is verified against existing
+     * Required provider for embedding work. Embeddings are routed through the
+     * approved commercial HTTPS API. The provider's `getModelId()` is verified against existing
      * `content_vectors.model` rows; mismatch throws unless `force` is set.
      *
-     * When omitted, behavior is identical to pre-patch: embeddings come from
-     * the store's `LlamaCpp` (or the global singleton).
+     * When omitted, learned work reaches the fail-closed compatibility adapter
+     * and returns typed HOLD.
      */
     embedProvider?: EmbeddingProvider;
     /**
@@ -579,14 +578,10 @@ export declare function chunkDocumentAsync(content: string, maxChars?: number, o
  * Counts the tokens in `text`. Used by `chunkDocumentByTokens` for the
  * safety re-split that splits chunks exceeding `maxTokens`.
  *
- * When `chunkDocumentByTokens` is called WITHOUT a tokenizer (default),
- * it lazily resolves `getDefaultLlamaCpp()` and uses `llm.tokenize` —
- * accurate but expensive (loads the local GGUF embed model + initialises
- * llama.cpp, ~22s on cold cache).
+ * When `chunkDocumentByTokens` is called without a tokenizer, the disabled
+ * compatibility adapter returns typed HOLD.
  *
- * Provider-mode callers (HTTP embed providers like the GPU worker on
- * `models` LXC) MUST pass a JS-only approximator to avoid loading the
- * local model entirely. A char-based estimate like
+ * Commercial-provider callers pass a deterministic JS-only approximator. A char-based estimate like
  * `Math.ceil(text.length / 3)` is a reasonable default — it matches the
  * `avgCharsPerToken=3` heuristic used for the initial char-space chunk
  * step, so the safety re-split stays a near no-op while populating the
@@ -594,14 +589,9 @@ export declare function chunkDocumentAsync(content: string, maxChars?: number, o
  */
 export type TokenCounter = (text: string) => number | Promise<number>;
 /**
- * Chunk a document by actual token count using the LLM tokenizer.
- * More accurate than character-based chunking but requires async.
+ * Chunk a document with an injected token counter.
  *
- * When `tokenizer` is supplied, it is used in place of the local
- * `llm.tokenize(...)` call — neither `getDefaultLlamaCpp()` nor
- * `llm.tokenize(...)` is invoked. This lets remote-only deployments
- * (`QMD_EMBED_ENDPOINT=...`) chunk documents without warming up
- * node-llama-cpp (DoD #1 of i-1rqixh6m / i-qkarfffa).
+ * When `tokenizer` is supplied, no compatibility learned adapter is invoked.
  *
  * When `filepath` and `chunkStrategy` are provided, uses AST-aware break
  * points for supported code files.
@@ -948,7 +938,7 @@ export interface VectorSearchResult {
  *
  * Pipeline:
  * 1. expandQuery() → typed variants, filter to vec/hyde only (lex irrelevant here)
- * 2. searchVec() for original + vec/hyde variants (sequential — node-llama-cpp embed limitation)
+ * 2. searchVec() for original + vec/hyde variants through the commercial provider
  * 3. Dedup by filepath (keep max score)
  * 4. Sort by score descending, filter by minScore, slice to limit
  */
@@ -990,8 +980,7 @@ export interface StructuredSearchOptions {
  * 5. Position-aware score blending
  * 6. Dedup, filter, slice
  *
- * This is the recommended endpoint for capable LLMs — they can generate
- * better query variations than our small local model, especially for
- * domain-specific or nuanced queries.
+ * This is the recommended endpoint when the caller supplies domain-specific
+ * query variants and a commercial provider contract is active.
  */
 export declare function structuredSearch(store: Store, searches: ExpandedQuery[], options?: StructuredSearchOptions): Promise<HybridQueryResult[]>;

+ 15 - 30
dist/store.js

@@ -1101,13 +1101,10 @@ function getEmbeddingDocsForBatch(db, batch) {
 /**
  * Run `body` with a session-shaped argument that supplies an AbortSignal +
  * isValid flag. When `provider` is supplied, the session is a lightweight
- * AbortController-backed stub — `getLlm(store)` is never called and
- * `withLLMSessionForLlm` is bypassed entirely, so node-llama-cpp is not
- * warmed up on remote-only deployments (i-08ovbvtb, follow-up to i-qkarfffa).
+ * AbortController-backed stub; `getLlm(store)` and the fail-closed legacy
+ * session wrapper are bypassed entirely.
  *
- * When `provider` is undefined, behavior is unchanged: a real `LLMSession`
- * is created via `withLLMSessionForLlm(getLlm(store), ...)` so that the
- * body can use `session.embed`/`session.embedBatch` for the local path.
+ * When `provider` is undefined, the compatibility session returns typed HOLD.
  *
  * The fake session's LLM-only methods (embed/embedBatch/expandQuery/rerank)
  * throw if called — they MUST NOT be reached when `provider` is set, since
@@ -1190,21 +1187,19 @@ export async function generateEmbeddings(store, options) {
         // callers that never touch ~/.config/qmd working.
     }
     // Provider routing — when an EmbeddingProvider is supplied, embed calls go
-    // through it (HTTP, GPU worker, etc.). Otherwise, use the LLM session path.
+    // through it. Otherwise, use the fail-closed compatibility session path.
     // The outer session is still created for its abort signal (chunking uses
     // `session.signal` for cooperative cancellation).
     const provider = options?.embedProvider;
     const providerModel = provider?.getModelId() ?? model;
     // Resolve `embedModelUri` (used for formatting prefixes etc.) lazily —
     // when `provider` is set, take it from the provider; otherwise fall back
-    // to the local LlamaCpp's embed model name. Accessing `getLlm(store)` is
-    // deferred to the non-provider branch so remote-only deployments do not
-    // construct a `LlamaCpp` instance just to read its embedModelName.
+    // to the disabled compatibility adapter's model name. Accessing `getLlm(store)`
+    // is deferred to the non-provider branch.
     const embedModelUri = provider
         ? provider.getModelId()
         : getLlm(store).embedModelName;
-    // Run the embedding loop inside a session-scoped wrapper. When `provider`
-    // is set, this short-circuits the local LLM warm-up entirely (i-08ovbvtb).
+    // Run the embedding loop inside a session-scoped wrapper.
     const result = await withEmbedSession(store, provider, async (session) => {
         let chunksEmbedded = 0;
         let errors = 0;
@@ -1241,8 +1236,7 @@ export async function generateEmbeddings(store, options) {
         // avgCharsPerToken=3 — matches the heuristic the chunker already
         // uses for its initial char-space pass, so the safety re-split is a
         // near no-op while populating the `tokens` field with a stable
-        // estimate. CRITICAL: avoids loading node-llama-cpp on remote-only
-        // deployments (`QMD_EMBED_ENDPOINT=...`). i-1rqixh6m DoD #1.
+        // estimate without invoking any learned tokenizer.
         const chunkTokenizer = provider
             ? (text) => Math.ceil(text.length / 3)
             : undefined;
@@ -1938,23 +1932,16 @@ function chunkByFunctionRanges(content, ranges, regexPoints, codeFences, maxChar
     return out;
 }
 /**
- * Chunk a document by actual token count using the LLM tokenizer.
- * More accurate than character-based chunking but requires async.
+ * Chunk a document with an injected token counter.
  *
- * When `tokenizer` is supplied, it is used in place of the local
- * `llm.tokenize(...)` call — neither `getDefaultLlamaCpp()` nor
- * `llm.tokenize(...)` is invoked. This lets remote-only deployments
- * (`QMD_EMBED_ENDPOINT=...`) chunk documents without warming up
- * node-llama-cpp (DoD #1 of i-1rqixh6m / i-qkarfffa).
+ * When `tokenizer` is supplied, no compatibility learned adapter is invoked.
  *
  * When `filepath` and `chunkStrategy` are provided, uses AST-aware break
  * points for supported code files.
  */
 export async function chunkDocumentByTokens(content, maxTokens = CHUNK_SIZE_TOKENS, overlapTokens = CHUNK_OVERLAP_TOKENS, windowTokens = CHUNK_WINDOW_TOKENS, filepath, chunkStrategy = "regex", signal, tokenizer) {
     // Resolve token counter lazily so callers that supply `tokenizer` never
-    // touch the local LlamaCpp instance — `getDefaultLlamaCpp()` is only
-    // invoked from inside the default closure when it is actually called
-    // (i.e. when no tokenizer is supplied).
+    // touch the disabled compatibility adapter unless no tokenizer was supplied.
     let llm;
     const countTokens = tokenizer ?? (async (text) => {
         if (!llm)
@@ -2747,8 +2734,7 @@ export async function searchVec(db, query, model, limit = 20, collectionName, se
 // =============================================================================
 async function getEmbedding(text, model, isQuery, session, llmOverride, embedProvider) {
     // When an EmbeddingProvider is supplied, route the encoding through it
-    // (HTTP / GPU worker / fallback chain) instead of touching local
-    // node-llama-cpp at all. The provider sees the raw text + the desired
+    // through the approved commercial API. The provider sees the raw text + the desired
     // model id; query-formatting prefixes are still applied via
     // formatQueryForEmbedding so embedding parity with the index is preserved.
     if (embedProvider) {
@@ -3691,7 +3677,7 @@ export async function hybridQuery(store, query, options) {
  *
  * Pipeline:
  * 1. expandQuery() → typed variants, filter to vec/hyde only (lex irrelevant here)
- * 2. searchVec() for original + vec/hyde variants (sequential — node-llama-cpp embed limitation)
+ * 2. searchVec() for original + vec/hyde variants through the commercial provider
  * 3. Dedup by filepath (keep max score)
  * 4. Sort by score descending, filter by minScore, slice to limit
  */
@@ -3751,9 +3737,8 @@ export async function vectorSearchQuery(store, query, options) {
  * 5. Position-aware score blending
  * 6. Dedup, filter, slice
  *
- * This is the recommended endpoint for capable LLMs — they can generate
- * better query variations than our small local model, especially for
- * domain-specific or nuanced queries.
+ * This is the recommended endpoint when the caller supplies domain-specific
+ * query variants and a commercial provider contract is active.
  */
 export async function structuredSearch(store, searches, options) {
     const limit = options?.limit ?? 10;

+ 3 - 4
package.json

@@ -1,7 +1,7 @@
 {
   "name": "@oivo/qmd",
   "version": "2.2.0-oivo.0",
-  "description": "Query Markup Documents - On-device hybrid search for markdown files with BM25, vector search, and LLM reranking",
+  "description": "Query Markup Documents - BM25 search with commercial-API semantic retrieval for markdown files",
   "type": "module",
   "main": "dist/index.js",
   "types": "dist/index.d.ts",
@@ -50,7 +50,6 @@
     "@modelcontextprotocol/sdk": "1.29.0",
     "better-sqlite3": "12.8.0",
     "fast-glob": "3.3.3",
-    "node-llama-cpp": "3.18.1",
     "picomatch": "4.0.4",
     "sqlite-vec": "0.1.9",
     "web-tree-sitter": "0.26.7",
@@ -72,6 +71,7 @@
   },
   "devDependencies": {
     "@types/better-sqlite3": "7.6.13",
+    "@types/node": "25.6.0",
     "tsx": "4.21.0",
     "vitest": "3.2.4"
   },
@@ -80,7 +80,6 @@
       "@tree-sitter-grammars/tree-sitter-kotlin",
       "better-sqlite3",
       "esbuild",
-      "node-llama-cpp",
       "tree-sitter-go",
       "tree-sitter-java",
       "tree-sitter-javascript",
@@ -109,7 +108,7 @@
     "mcp",
     "reranking",
     "knowledge-base",
-    "local-ai",
+    "commercial-ai-api",
     "llm"
   ],
   "author": "Tobi Lutke <tobi@lutke.com>",

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 43 - 591
pnpm-lock.yaml


+ 4 - 316
src/bench-rerank.ts

@@ -1,318 +1,6 @@
 #!/usr/bin/env bun
-/**
- * QMD Reranker Benchmark
- *
- * Measures reranking performance across different configurations.
- * Reports device, parallelism, memory, VRAM, and throughput.
- *
- * Usage:
- *   bun src/bench-rerank.ts              # full benchmark
- *   bun src/bench-rerank.ts --quick      # quick smoke test (10 docs, 1 iteration)
- *   bun src/bench-rerank.ts --docs 100   # custom doc count
- */
+import { commercialApiHold } from "./model-policy.js";
 
-import {
-  getLlama,
-  resolveModelFile,
-  LlamaLogLevel,
-  type Llama,
-  type LlamaModel,
-} from "node-llama-cpp";
-import { homedir } from "os";
-import { join } from "path";
-import { cpus } from "os";
-
-// ============================================================================
-// Config
-// ============================================================================
-
-const RERANK_MODEL = "hf:ggml-org/Qwen3-Reranker-0.6B-Q8_0-GGUF/qwen3-reranker-0.6b-q8_0.gguf";
-const MODEL_CACHE = join(homedir(), ".cache", "qmd", "models");
-const CONTEXT_SIZE = 2048;
-
-const args = process.argv.slice(2);
-const quick = args.includes("--quick");
-const docsIdx = args.indexOf("--docs");
-const DOC_COUNT = docsIdx >= 0 ? parseInt(args[docsIdx + 1]!) : (quick ? 10 : 40);
-const ITERATIONS = quick ? 1 : 3;
-const PARALLEL_CONFIGS = quick ? [1, 4] : [1, 2, 4, 8];
-
-// ============================================================================
-// Test data — realistic-ish chunks of varying length
-// ============================================================================
-
-const QUERY = "How do AI agents work and what are their limitations?";
-
-function generateDocs(n: number): string[] {
-  const templates = [
-    "Artificial intelligence agents are software systems that perceive their environment and take actions to achieve goals. They use techniques like reinforcement learning, planning, and natural language processing to operate autonomously.",
-    "The transformer architecture, introduced in 2017, revolutionized natural language processing. Self-attention mechanisms allow models to weigh the importance of different parts of input sequences when generating outputs.",
-    "Machine learning models require careful evaluation to avoid overfitting. Cross-validation, holdout sets, and metrics like precision, recall, and F1 score help assess generalization performance.",
-    "Retrieval-augmented generation combines information retrieval with language models. Documents are embedded into vector spaces, retrieved based on query similarity, and used as context for generation.",
-    "Neural network training involves forward propagation, loss computation, and backpropagation. Optimizers like Adam and SGD adjust weights to minimize the loss function over training iterations.",
-    "Large language models exhibit emergent capabilities at scale, including few-shot learning, chain-of-thought reasoning, and instruction following. These properties were not explicitly trained for.",
-    "Embedding models convert text into dense vector representations that capture semantic meaning. Similar texts produce similar vectors, enabling efficient similarity search and clustering.",
-    "Autonomous agents face challenges including hallucination, lack of grounding, limited planning horizons, and difficulty with multi-step reasoning. Safety and alignment remain open research problems.",
-    "The attention mechanism computes query-key-value interactions to determine which parts of the input are most relevant. Multi-head attention allows the model to attend to different representation subspaces.",
-    "Fine-tuning adapts a pre-trained model to specific tasks using domain-specific data. Techniques like LoRA reduce the number of trainable parameters while maintaining performance.",
-  ];
-  return Array.from({ length: n }, (_, i) => templates[i % templates.length]!);
-}
-
-// ============================================================================
-// Helpers
-// ============================================================================
-
-function formatBytes(bytes: number): string {
-  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
-  if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
-  return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
-}
-
-function getMemUsage(): { rss: number; heapUsed: number } {
-  const m = process.memoryUsage();
-  return { rss: m.rss, heapUsed: m.heapUsed };
-}
-
-function median(arr: number[]): number {
-  const sorted = [...arr].sort((a, b) => a - b);
-  const mid = Math.floor(sorted.length / 2);
-  return sorted.length % 2 !== 0 ? sorted[mid]! : (sorted[mid - 1]! + sorted[mid]!) / 2;
-}
-
-// ============================================================================
-// Benchmark runner
-// ============================================================================
-
-interface BenchResult {
-  parallelism: number;
-  contextSize: number;
-  flashAttention: boolean;
-  times: number[];       // ms per run
-  medianMs: number;
-  docsPerSec: number;
-  vramPerContext: number; // bytes
-  totalVram: number;      // bytes
-  peakRss: number;        // bytes
-}
-
-async function benchmarkConfig(
-  model: LlamaModel,
-  llama: Llama,
-  docs: string[],
-  parallelism: number,
-  flash: boolean,
-): Promise<BenchResult> {
-  // Measure VRAM before
-  const vramBefore = llama.gpu ? await llama.getVramState() : null;
-  const rssBefore = getMemUsage().rss;
-
-  // Create contexts. On CPU, split threads evenly across contexts.
-  const cpuThreads = !llama.gpu ? Math.floor(llama.cpuMathCores / parallelism) : 0;
-  const contexts = [];
-  for (let i = 0; i < parallelism; i++) {
-    try {
-      contexts.push(await model.createRankingContext({
-        contextSize: CONTEXT_SIZE,
-        flashAttention: flash,
-        ...(cpuThreads > 0 ? { threads: cpuThreads } : {}),
-      }));
-    } catch {
-      if (contexts.length === 0) {
-        // Try without flash
-        contexts.push(await model.createRankingContext({
-          contextSize: CONTEXT_SIZE,
-          ...(cpuThreads > 0 ? { threads: cpuThreads } : {}),
-        }));
-      }
-      break;
-    }
-  }
-  const actualParallelism = contexts.length;
-
-  // Measure VRAM after context creation
-  const vramAfter = llama.gpu ? await llama.getVramState() : null;
-  const vramUsed = vramBefore && vramAfter ? vramAfter.used - vramBefore.used : 0;
-  const vramPerCtx = actualParallelism > 0 ? vramUsed / actualParallelism : 0;
-
-  // Warm up
-  await contexts[0]!.rankAll(QUERY, docs.slice(0, 2));
-
-  // Benchmark iterations
-  const times: number[] = [];
-  let peakRss = getMemUsage().rss;
-
-  for (let iter = 0; iter < ITERATIONS; iter++) {
-    const chunkSize = Math.ceil(docs.length / actualParallelism);
-
-    const t0 = performance.now();
-    const allScores = await Promise.all(
-      Array.from({ length: actualParallelism }, (_, i) => {
-        const chunk = docs.slice(i * chunkSize, (i + 1) * chunkSize);
-        return chunk.length > 0 ? contexts[i]!.rankAll(QUERY, chunk) : Promise.resolve([]);
-      })
-    );
-    const elapsed = performance.now() - t0;
-    times.push(elapsed);
-
-    // Verify scores are valid
-    const flat = allScores.flat();
-    if (flat.some(s => s < 0 || s > 1 || isNaN(s))) {
-      throw new Error("Invalid scores detected");
-    }
-
-    const currentRss = getMemUsage().rss;
-    if (currentRss > peakRss) peakRss = currentRss;
-  }
-
-  // Cleanup
-  for (const ctx of contexts) await ctx.dispose();
-
-  const med = median(times);
-  return {
-    parallelism: actualParallelism,
-    contextSize: CONTEXT_SIZE,
-    flashAttention: flash,
-    times,
-    medianMs: med,
-    docsPerSec: (docs.length / med) * 1000,
-    vramPerContext: vramPerCtx,
-    totalVram: vramUsed,
-    peakRss,
-  };
-}
-
-// ============================================================================
-// Main
-// ============================================================================
-
-async function main() {
-  console.log("═══════════════════════════════════════════════════════════════");
-  console.log("  QMD Reranker Benchmark");
-  console.log("═══════════════════════════════════════════════════════════════\n");
-
-  const llama = await getLlama({
-    // Load prebuilt binaries only — never compile llama.cpp at runtime. See the
-    // detailed rationale in src/llm.ts: `autoAttempt` blocks on a doomed cmake
-    // build when the auto-probed GPU's prebuilt is host-incompatible. (i-tgac7ig3)
-    build: "never",
-    logLevel: LlamaLogLevel.error
-  });
-  let gpuLabel: string = llama.gpu === false
-    ? "cpu"
-    : llama.gpu;
-
-  // System info
-  const cpuInfo = cpus();
-  const cpuModel = cpuInfo[0]?.model || "unknown";
-  const cpuCount = cpuInfo.length;
-
-  console.log("System");
-  console.log(`  CPU:       ${cpuModel}`);
-  console.log(`  Cores:     ${cpuCount} (${llama.cpuMathCores} math)`);
-  console.log(`  Device:    ${gpuLabel}`);
-
-  if (llama.gpu) {
-    const gpuNames = await llama.getGpuDeviceNames();
-    const counts = new Map<string, number>();
-    for (const name of gpuNames) counts.set(name, (counts.get(name) || 0) + 1);
-    const devStr = Array.from(counts.entries())
-      .map(([name, n]) => n > 1 ? `${n}× ${name}` : name).join(", ");
-    console.log(`  GPU:       ${devStr}`);
-    const vram = await llama.getVramState();
-    console.log(`  VRAM:      ${formatBytes(vram.total)} total, ${formatBytes(vram.free)} free`);
-  }
-
-  console.log(`  RAM:       ${formatBytes(getMemUsage().rss)} RSS at start`);
-
-  // Load model
-  console.log(`\nModel`);
-  console.log(`  URI:       ${RERANK_MODEL}`);
-  const modelPath = await resolveModelFile(RERANK_MODEL, MODEL_CACHE);
-  const vramPreModel = llama.gpu ? await llama.getVramState() : null;
-  const model = await llama.loadModel({ modelPath });
-  const vramPostModel = llama.gpu ? await llama.getVramState() : null;
-  const modelVram = vramPreModel && vramPostModel ? vramPostModel.used - vramPreModel.used : 0;
-  console.log(`  Params:    ${model.trainContextSize} train ctx`);
-  if (modelVram > 0) console.log(`  VRAM:      ${formatBytes(modelVram)} (model weights)`);
-
-  // Generate test docs
-  const docs = generateDocs(DOC_COUNT);
-  console.log(`\nBenchmark`);
-  console.log(`  Documents: ${DOC_COUNT}`);
-  console.log(`  Ctx size:  ${CONTEXT_SIZE}`);
-  console.log(`  Iterations:${ITERATIONS}`);
-  console.log(`  Query:     "${QUERY.slice(0, 50)}..."`);
-
-  // Run benchmarks
-  const results: BenchResult[] = [];
-
-  for (const p of PARALLEL_CONFIGS) {
-    if (!llama.gpu && p > 1) {
-      // CPU: only test if we have enough cores (at least 4 per context)
-      if (llama.cpuMathCores < p * 4) {
-        console.log(`\n  [${p} ctx] skipped (need ${p * 4} cores, have ${llama.cpuMathCores})`);
-        continue;
-      }
-    }
-
-    // Test with flash attention
-    process.stdout.write(`\n  [${p} ctx, flash] running...`);
-    try {
-      const r = await benchmarkConfig(model, llama, docs, p, true);
-      results.push(r);
-      process.stdout.write(` ${r.medianMs.toFixed(0)}ms (${r.docsPerSec.toFixed(1)} docs/s)\n`);
-    } catch (e: any) {
-      process.stdout.write(` failed: ${e.message}\n`);
-      // Try without flash
-      process.stdout.write(`  [${p} ctx, no flash] running...`);
-      try {
-        const r = await benchmarkConfig(model, llama, docs, p, false);
-        results.push(r);
-        process.stdout.write(` ${r.medianMs.toFixed(0)}ms (${r.docsPerSec.toFixed(1)} docs/s)\n`);
-      } catch (e2: any) {
-        process.stdout.write(` failed: ${e2.message}\n`);
-      }
-    }
-  }
-
-  // Summary table
-  console.log("\n═══════════════════════════════════════════════════════════════");
-  console.log("  Results");
-  console.log("═══════════════════════════════════════════════════════════════\n");
-
-  const header = "  Ctx  Flash  Median    Docs/s   VRAM/ctx   Total VRAM  Peak RSS";
-  const sep    = "  ───  ─────  ──────    ──────   ────────   ──────────  ────────";
-  console.log(header);
-  console.log(sep);
-
-  const baseline = results[0]?.medianMs ?? 1;
-  for (const r of results) {
-    const speedup = baseline / r.medianMs;
-    const speedupStr = r === results[0] ? "      " : `(${speedup.toFixed(1)}×)`;
-    console.log(
-      `  ${String(r.parallelism).padStart(3)}  ` +
-      `${r.flashAttention ? " yes " : "  no "}  ` +
-      `${r.medianMs.toFixed(0).padStart(5)}ms  ` +
-      `${r.docsPerSec.toFixed(1).padStart(6)}  ` +
-      `${formatBytes(r.vramPerContext).padStart(8)}  ` +
-      `${formatBytes(r.totalVram).padStart(10)}  ` +
-      `${formatBytes(r.peakRss).padStart(8)}  ` +
-      speedupStr
-    );
-  }
-
-  // Best config
-  if (results.length > 0) {
-    const best = results.reduce((a, b) => a.docsPerSec > b.docsPerSec ? a : b);
-    console.log(`\n  Best: ${best.parallelism} contexts, flash=${best.flashAttention}`);
-    console.log(`        ${best.medianMs.toFixed(0)}ms for ${DOC_COUNT} docs (${best.docsPerSec.toFixed(1)} docs/s)`);
-    if (best.totalVram > 0) console.log(`        ${formatBytes(best.totalVram)} VRAM`);
-  }
-
-  console.log("");
-  await model.dispose();
-  await llama.dispose();
-}
-
-main().catch(console.error);
+throw commercialApiHold(
+  "the local reranker benchmark is disabled; benchmark an approved commercial API adapter instead",
+);

+ 52 - 141
src/cli/qmd.ts

@@ -77,7 +77,7 @@ import {
   type ReindexResult,
   type ChunkStrategy,
 } from "../store.js";
-import { disposeDefaultLlamaCpp, getDefaultLlamaCpp, setDefaultLlamaCpp, LlamaCpp, withLLMSession, pullModels, DEFAULT_EMBED_MODEL_URI, DEFAULT_GENERATE_MODEL_URI, DEFAULT_RERANK_MODEL_URI, DEFAULT_MODEL_CACHE_DIR } from "../llm.js";
+import { disposeDefaultLlamaCpp, getDefaultLlamaCpp, setDefaultLlamaCpp, LlamaCpp, withLLMSession, DEFAULT_EMBED_MODEL_URI, DEFAULT_GENERATE_MODEL_URI, DEFAULT_RERANK_MODEL_URI } from "../llm.js";
 import {
   formatSearchResults,
   formatDocuments,
@@ -101,12 +101,12 @@ import {
 import { getEmbeddedQmdSkillContent, getEmbeddedQmdSkillFiles } from "../embedded-skills.js";
 import {
   createEmbeddingProvider,
-  resolveProviderKind,
   type EmbeddingProvider,
   type ProviderKind,
   type CreateEmbeddingProviderOptions,
   ModelMismatchError,
 } from "../embedding/index.js";
+import { commercialApiHold } from "../model-policy.js";
 
 // Enable production mode - allows using default database path
 // Tests must set INDEX_PATH or use createStore() with explicit path
@@ -465,48 +465,10 @@ async function showStatus(): Promise<void> {
     console.log(`\n${c.dim}No collections. Run 'qmd collection add .' to index markdown files.${c.reset}`);
   }
 
-  // Models
-  {
-    // hf:org/repo/file.gguf → https://huggingface.co/org/repo
-    const hfLink = (uri: string) => {
-      const match = uri.match(/^hf:([^/]+\/[^/]+)\//);
-      return match ? `https://huggingface.co/${match[1]}` : uri;
-    };
-    console.log(`\n${c.bold}Models${c.reset}`);
-    console.log(`  Embedding:   ${hfLink(DEFAULT_EMBED_MODEL_URI)}`);
-    console.log(`  Reranking:   ${hfLink(DEFAULT_RERANK_MODEL_URI)}`);
-    console.log(`  Generation:  ${hfLink(DEFAULT_GENERATE_MODEL_URI)}`);
-  }
-
-  // Device / GPU info
-  try {
-    const llm = getDefaultLlamaCpp();
-    const device = await llm.getDeviceInfo();
-    console.log(`\n${c.bold}Device${c.reset}`);
-    if (device.gpu) {
-      console.log(`  GPU:      ${c.green}${device.gpu}${c.reset} (offloading: ${device.gpuOffloading ? 'yes' : 'no'})`);
-      if (device.gpuDevices.length > 0) {
-        // Deduplicate and count GPUs
-        const counts = new Map<string, number>();
-        for (const name of device.gpuDevices) {
-          counts.set(name, (counts.get(name) || 0) + 1);
-        }
-        const deviceStr = Array.from(counts.entries())
-          .map(([name, count]) => count > 1 ? `${count}× ${name}` : name)
-          .join(', ');
-        console.log(`  Devices:  ${deviceStr}`);
-      }
-      if (device.vram) {
-        console.log(`  VRAM:     ${formatBytes(device.vram.free)} free / ${formatBytes(device.vram.total)} total`);
-      }
-    } else {
-      console.log(`  GPU:      ${c.yellow}none${c.reset} (running on CPU — models will be slow)`);
-      console.log(`  ${c.dim}Tip: Install CUDA, Vulkan, or Metal support for GPU acceleration.${c.reset}`);
-    }
-    console.log(`  CPU:      ${device.cpuCores} math cores`);
-  } catch {
-    // Don't fail status if LLM init fails
-  }
+  console.log(`\n${c.bold}Learned model policy${c.reset}`);
+  console.log("  Runtime:     commercial API only");
+  console.log(`  Embeddings:  ${process.env.QMD_EMBED_ENDPOINT ? "configured" : "HOLD (QMD_EMBED_ENDPOINT missing)"}`);
+  console.log("  Local model: disabled");
 
   // Tips section
   const tips: string[] = [];
@@ -551,7 +513,7 @@ async function updateCollections(collectionFilter?: string): Promise<void> {
   const storeInstance = getStore();
   // Collections are defined in YAML; no duplicate cleanup needed.
 
-  // Clear Ollama cache on update
+  // Clear legacy learned-response cache on update.
   clearCache(db);
 
   const allCollections = listCollections(db);
@@ -1538,7 +1500,7 @@ async function indexFiles(pwd?: string, globPattern: string = DEFAULT_GLOB, coll
   const now = new Date().toISOString();
   const excludeDirs = ["node_modules", ".git", ".cache", "vendor", "dist", "build"];
 
-  // Clear Ollama cache on index
+  // Clear legacy learned-response cache on index.
   clearCache(db);
 
   // Collection name must be provided (from YAML)
@@ -1696,8 +1658,11 @@ function parseChunkStrategy(value: unknown): ChunkStrategy | undefined {
 function parseProviderKind(value: unknown): ProviderKind | undefined {
   if (value === undefined) return undefined;
   const s = String(value).toLowerCase();
-  if (s === "local" || s === "openai") return s;
-  throw new Error(`--provider must be "local" or "openai" (got "${s}")`);
+  if (s === "local") {
+    throw commercialApiHold('--provider local is forbidden; use an approved commercial API');
+  }
+  if (s === "openai") return s;
+  throw commercialApiHold(`unsupported commercial provider kind "${s}"`);
 }
 
 function parseOptionalPositiveInt(name: string, value: unknown): number | undefined {
@@ -1710,50 +1675,13 @@ function parseOptionalPositiveInt(name: string, value: unknown): number | undefi
 }
 
 /**
- * Build an `EmbeddingProvider` for the QUERY-side path (vsearch / query)
- * if and only if the user has opted into a non-local provider via flags or
- * env vars. Returns `undefined` for the zero-config case so the legacy
- * `getDefaultLlamaCpp().embed(...)` path is used unchanged — preserving
- * pre-patch behavior for callers that have not configured remote embedding
- * (i-loazq6ze DoD #5: backward compat).
- *
- * Resolution mirrors `qmd embed` (factory.resolveProviderKind):
- *   1. Explicit `--provider` flag → build provider
- *   2. Any `--embed-*` flag / `QMD_EMBED_*` env / `embedProvider.endpoint`
- *      in `~/.config/qmd/config.json` → build provider
- *   3. Otherwise → return `undefined` (legacy path)
- *
- * Returns `null` on construction failure (e.g. malformed flags) so the
- * caller can warn + fall back to the legacy path.
+ * Build the commercial query-side provider. Missing or malformed config is a
+ * typed HOLD; it must never select the legacy local path.
  */
 function buildQueryEmbedProvider(values: Record<string, unknown>): EmbeddingProvider | undefined {
   const providerCliKind = parseProviderKind(values["provider"]);
   const opts = buildProviderOpts(values, providerCliKind);
-
-  // Determine whether the user opted into a provider. The factory's resolve
-  // step returns "local" by default; without explicit opt-in (flag/env/
-  // config), we keep the legacy path with no construction overhead.
-  const resolved = resolveProviderKind(opts);
-  const hasProviderFlag = providerCliKind !== undefined;
-  const hasOpenAiOverride = !!opts.openai && Object.keys(opts.openai).length > 0;
-  const envOptIn = !!(
-    process.env.QMD_EMBED_PROVIDER ||
-    process.env.QMD_EMBED_ENDPOINT ||
-    process.env.QMD_EMBED_AUTO_FALLBACK
-  );
-
-  if (!hasProviderFlag && !hasOpenAiOverride && !envOptIn && resolved === "local") {
-    return undefined;
-  }
-
-  try {
-    return createEmbeddingProvider(opts);
-  } catch (err) {
-    process.stderr.write(
-      `${c.yellow}Warning: failed to build query embedding provider — using local fallback (${err instanceof Error ? err.message : String(err)})${c.reset}\n`,
-    );
-    return undefined;
-  }
+  return createEmbeddingProvider(opts);
 }
 
 /**
@@ -1784,7 +1712,7 @@ function buildProviderOpts(
         }
       : undefined;
 
-  // CLI flag for auto-fallback wrapping (only meaningful when kind === openai)
+  // Historical flag is passed through so the factory can reject it as typed HOLD.
   const autoFallback = values["embed-auto-fallback"] === true ? true : undefined;
 
   return {
@@ -1800,6 +1728,29 @@ function optionalString(v: unknown): string | undefined {
   return s === "" ? undefined : s;
 }
 
+function validateEmbedCollectionSelection(collection: string | undefined, force: boolean): void {
+  if (collection === undefined) return;
+
+  const allCollections = listCollections(getDb());
+  const match = allCollections.find(col => col.name === collection);
+  if (!match) {
+    const known = allCollections.map(col => col.name).sort().join(", ");
+    console.error(`${c.red}Collection not found: "${collection}"${c.reset}`);
+    console.error(`${c.dim}Available collections: ${known || "(none)"}${c.reset}`);
+    console.error(`${c.dim}Run 'qmd embed --all' (or 'qmd embed' with no args) to embed every collection.${c.reset}`);
+    closeDb();
+    process.exit(1);
+  }
+
+  if (force) {
+    console.error(`${c.red}--force cannot be combined with a positional collection name.${c.reset}`);
+    console.error(`${c.dim}--force clears ALL vectors fleet-wide before re-embedding; restricting it to one collection would corrupt the others.${c.reset}`);
+    console.error(`${c.dim}Use 'qmd embed --all -f' to force-re-embed every collection, OR drop -f and run 'qmd embed ${collection}' to embed only this collection's pending hashes.${c.reset}`);
+    closeDb();
+    process.exit(1);
+  }
+}
+
 async function vectorIndex(
   model: string = DEFAULT_EMBED_MODEL_URI,
   force: boolean = false,
@@ -1815,31 +1766,7 @@ async function vectorIndex(
   const storeInstance = getStore();
   const db = storeInstance.db;
 
-  // i-ofojj7dy — validate the collection filter against the known list before
-  // doing any work. Mirrors `qmd update <name>` ergonomics.
-  if (batchOptions?.collection !== undefined) {
-    const allCollections = listCollections(db);
-    const match = allCollections.find(col => col.name === batchOptions.collection);
-    if (!match) {
-      const known = allCollections.map(c => c.name).sort().join(", ");
-      console.error(`${c.red}Collection not found: "${batchOptions.collection}"${c.reset}`);
-      console.error(`${c.dim}Available collections: ${known || "(none)"}${c.reset}`);
-      console.error(`${c.dim}Run 'qmd embed --all' (or 'qmd embed' with no args) to embed every collection.${c.reset}`);
-      closeDb();
-      process.exit(1);
-    }
-    // i-ofojj7dy — `--force` is fleet-wide (nukes all content_vectors).
-    // Combining it with a single-collection filter would silently break
-    // every OTHER collection's embeddings. Per-collection force-clear is a
-    // distinct feature (out of scope here). Refuse and steer the user.
-    if (force) {
-      console.error(`${c.red}--force cannot be combined with a positional collection name.${c.reset}`);
-      console.error(`${c.dim}--force clears ALL vectors fleet-wide before re-embedding; restricting it to one collection would corrupt the others.${c.reset}`);
-      console.error(`${c.dim}Use 'qmd embed --all -f' to force-re-embed every collection, OR drop -f and run 'qmd embed ${batchOptions.collection}' to embed only this collection's pending hashes.${c.reset}`);
-      closeDb();
-      process.exit(1);
-    }
-  }
+  validateEmbedCollectionSelection(batchOptions?.collection, force);
 
   if (force) {
     console.log(`${c.yellow}Force re-indexing: clearing all vectors...${c.reset}`);
@@ -2452,10 +2379,7 @@ async function vectorSearch(query: string, opts: OutputOptions, _model: string =
   checkIndexHealth(store.db);
 
   // Build embedding provider for query encoding (i-loazq6ze).
-  // Same precedence as `qmd embed`: explicit `--provider` flag → env vars →
-  // `~/.config/qmd/config.json` → default LocalLlamaCppProvider. The local
-  // default keeps zero-config callers on the legacy llama-cpp path with no
-  // observable change.
+  // Commercial provider only; missing configuration remains typed HOLD.
   const embedProvider = opts.embedProvider;
 
   await withLLMSession(async () => {
@@ -2911,14 +2835,14 @@ function showHelp(): void {
   console.log("                                  -f clears + re-embeds ALL vectors fleet-wide, incompatible with <collection>)");
   console.log("    --max-docs-per-batch <n>    - Cap docs loaded into memory per embedding batch");
   console.log("    --max-batch-mb <n>          - Cap UTF-8 MB loaded into memory per embedding batch");
-  console.log("    --provider {local,openai}   - Embedding backend (default: local llama.cpp)");
+  console.log("    --provider openai           - Commercial OpenAI-compatible API backend");
   console.log("    --embed-endpoint <url>      - OpenAI-compatible endpoint (or QMD_EMBED_ENDPOINT)");
   console.log("    --embed-api-key <key>       - Bearer token (or QMD_EMBED_API_KEY)");
   console.log("    --embed-model-id <id>       - Stable model id stored in DB (default: embeddinggemma)");
   console.log("    --embed-upstream-model <m>  - Model name sent in HTTP body (default: same as model-id)");
   console.log("    --embed-batch-size <n>      - Batch size for HTTP provider (default: 64)");
   console.log("    --embed-timeout-ms <n>      - Per-request timeout in ms (default: 30000)");
-  console.log("    --embed-auto-fallback       - Wrap openai provider in local fallback (or QMD_EMBED_AUTO_FALLBACK)");
+  console.log("    --embed-auto-fallback       - Forbidden legacy option; returns typed HOLD");
   console.log("  qmd cleanup [--no-vacuum]     - Clear caches and orphaned data; VACUUM unless --no-vacuum");
   console.log("");
   console.log("Query syntax (qmd query):");
@@ -3330,10 +3254,11 @@ if (isMain) {
         }
         const embedCollectionFilter = embedAllFlag ? undefined : embedCollectionArg;
 
-        // Build embedding provider from CLI flags + env + config file.
-        // Backward compat: with no flags / env vars, the factory returns
-        // a LocalLlamaCppProvider that delegates to the default LlamaCpp
-        // singleton — identical to pre-patch behavior.
+        // Validate deterministic CLI arguments before resolving credentials.
+        // Invalid collection intent must not be masked by a provider HOLD.
+        validateEmbedCollectionSelection(embedCollectionFilter, !!cli.values.force);
+
+        // Build the commercial embedding provider. No endpoint means typed HOLD.
         const providerCliKind = parseProviderKind(cli.values["provider"]);
         const providerOpts = buildProviderOpts(cli.values, providerCliKind);
         const embedProvider = createEmbeddingProvider(providerOpts);
@@ -3359,22 +3284,8 @@ if (isMain) {
 
     case "pull": {
       const refresh = cli.values.refresh === undefined ? false : Boolean(cli.values.refresh);
-      const models = [
-        DEFAULT_EMBED_MODEL_URI,
-        DEFAULT_GENERATE_MODEL_URI,
-        DEFAULT_RERANK_MODEL_URI,
-      ];
-      console.log(`${c.bold}Pulling models${c.reset}`);
-      const results = await pullModels(models, {
-        refresh,
-        cacheDir: DEFAULT_MODEL_CACHE_DIR,
-      });
-      for (const result of results) {
-        const size = formatBytes(result.sizeBytes);
-        const note = result.refreshed ? "refreshed" : "cached/checked";
-        console.log(`- ${result.model} -> ${result.path} (${size}, ${note})`);
-      }
-      break;
+      void refresh;
+      throw commercialApiHold("qmd pull is disabled because GGUF downloads are forbidden");
     }
 
     case "search":
@@ -3396,7 +3307,7 @@ if (isMain) {
         cli.opts.minScore = 0.3;
       }
       // Build query-side embedding provider (i-loazq6ze).
-      // Returns undefined for zero-config callers (legacy local path).
+      // Missing commercial configuration fails closed as typed HOLD.
       cli.opts.embedProvider = buildQueryEmbedProvider(cli.values);
       await vectorSearch(cli.query, cli.opts);
       break;

+ 49 - 47
src/embedding/factory.ts

@@ -4,28 +4,20 @@
  * Resolution order (first match wins):
  *   1. Explicit `kind` argument or `--provider` CLI flag → forces a kind
  *   2. `QMD_EMBED_ENDPOINT` env var present and non-empty → "openai"
- *   3. Config file (`~/.config/qmd/config.json`) `embedProvider.kind` → that kind
- *   4. Otherwise → "local" (legacy / backward-compat)
- *
- * Backward compat invariant: when neither `QMD_EMBED_ENDPOINT` nor
- * `~/.config/qmd/config.json` mentions a provider, callers get the same
- * `LocalLlamaCppProvider` they had before this change.
+ *   3. Config file (`~/.config/qmd/config.json`) commercial endpoint
+ *   4. Otherwise → typed HOLD (there is no local or self-hosted fallback)
  */
 
 import { existsSync, readFileSync } from "node:fs";
 import { homedir } from "node:os";
 import { join } from "node:path";
 
-import { LocalLlamaCppProvider, type LocalLlamaCppProviderConfig } from "./local.js";
 import {
   OpenAIEmbeddingsProvider,
   type OpenAIProviderConfig,
 } from "./openai.js";
-import {
-  AutoFallbackEmbeddingProvider,
-  type AutoFallbackProviderConfig,
-} from "./autofallback.js";
 import type { EmbeddingProvider, ProviderKind } from "./provider.js";
+import { commercialApiHold } from "../model-policy.js";
 
 // ─────────────────────────── Config file ─────────────────────────────────────
 
@@ -44,7 +36,7 @@ export type EmbedProviderConfigFile = {
      */
     concurrency?: number;
     timeoutMs?: number;
-    /** When true, wrap the openai provider in AutoFallback (local fallback). */
+    /** Historical only. `true` is rejected because local fallback is forbidden. */
     autoFallback?: boolean;
   };
 };
@@ -57,7 +49,7 @@ export function defaultConfigPath(): string {
 
 /**
  * Load `~/.config/qmd/config.json` if present. Returns an empty object on
- * any read/parse error so we silently fall back to env/local.
+ * any read/parse error; provider construction then fails closed without an endpoint.
  */
 export function loadConfigFile(path: string = defaultConfigPath()): EmbedProviderConfigFile {
   if (!existsSync(path)) return {};
@@ -78,29 +70,13 @@ export type CreateEmbeddingProviderOptions = {
   kind?: ProviderKind;
   /** Override config file path (mostly for tests) */
   configPath?: string;
-  /** Local-provider overrides */
-  local?: LocalLlamaCppProviderConfig;
   /** OpenAI-provider overrides — merged on top of env/config */
   openai?: Partial<OpenAIProviderConfig>;
   /**
-   * Wrap the chosen provider in `AutoFallbackEmbeddingProvider` so that a
-   * remote outage transparently falls back to local llama.cpp. Default:
-   * `false` — opt-in, since the wrapper requires both backends to be
-   * available and the local one will warm node-llama-cpp on first call.
-   *
-   * Resolution: explicit `autoFallback` wins → env `QMD_EMBED_AUTO_FALLBACK`
-   * (`1`/`true`) → config-file `embedProvider.autoFallback` → false.
-   *
-   * Only applies when the resolved kind is `openai` (no fallback wrap when
-   * the primary IS local already).
+   * Historical compatibility input. Any truthy value produces typed HOLD;
+   * commercial provider failures must never fall back to a local model.
    */
   autoFallback?: boolean;
-  /**
-   * Override config for `AutoFallbackEmbeddingProvider` (failureStreak,
-   * cooldownMs, etc.). Only used when `autoFallback` resolves true.
-   * Primary + fallback are constructed automatically.
-   */
-  autoFallbackOverrides?: Omit<AutoFallbackProviderConfig, "primary" | "fallback">;
   /**
    * Custom env source (mostly for tests). Defaults to `process.env`.
    * Read keys: QMD_EMBED_PROVIDER, QMD_EMBED_ENDPOINT, QMD_EMBED_API_KEY,
@@ -119,11 +95,17 @@ export function resolveProviderKind(opts: CreateEmbeddingProviderOptions = {}):
   const cfg = loadConfigFile(opts.configPath);
 
   // 1. Explicit kind argument
-  if (opts.kind) return opts.kind;
+  if (opts.kind === "local") {
+    throw commercialApiHold('provider kind "local" is disabled; configure an approved commercial API');
+  }
+  if (opts.kind === "openai") return opts.kind;
 
   // 2a. Explicit env override
   const envKind = env.QMD_EMBED_PROVIDER?.trim().toLowerCase();
-  if (envKind === "local" || envKind === "openai") return envKind;
+  if (envKind === "local") {
+    throw commercialApiHold("QMD_EMBED_PROVIDER=local is forbidden");
+  }
+  if (envKind === "openai") return envKind;
 
   // 2b. Endpoint env present → openai
   if (env.QMD_EMBED_ENDPOINT && env.QMD_EMBED_ENDPOINT.trim() !== "") {
@@ -131,15 +113,18 @@ export function resolveProviderKind(opts: CreateEmbeddingProviderOptions = {}):
   }
 
   // 3. Config file
-  if (cfg.embedProvider?.kind === "local" || cfg.embedProvider?.kind === "openai") {
-    return cfg.embedProvider.kind;
+  if (cfg.embedProvider?.kind === "local") {
+    throw commercialApiHold("embedProvider.kind=local is forbidden");
+  }
+  if (cfg.embedProvider?.kind === "openai") {
+    return "openai";
   }
   if (cfg.embedProvider?.endpoint && cfg.embedProvider.endpoint.trim() !== "") {
     return "openai";
   }
 
-  // 4. Default
-  return "local";
+  // Commercial-only default. Missing endpoint is handled as typed HOLD by the factory.
+  return "openai";
 }
 
 /**
@@ -154,7 +139,7 @@ export function createEmbeddingProvider(
   const kind = resolveProviderKind(opts);
 
   if (kind === "local") {
-    return new LocalLlamaCppProvider(opts.local ?? {});
+    throw commercialApiHold('provider kind "local" is disabled');
   }
 
   // OpenAI
@@ -163,12 +148,13 @@ export function createEmbeddingProvider(
     env.QMD_EMBED_ENDPOINT ??
     cfg.embedProvider?.endpoint;
   if (!endpoint || endpoint.trim() === "") {
-    throw new Error(
-      'createEmbeddingProvider: kind="openai" requires an endpoint. ' +
+    throw commercialApiHold(
+      'commercial provider requires an endpoint. ' +
       "Set QMD_EMBED_ENDPOINT env var, or `embedProvider.endpoint` in " +
       "~/.config/qmd/config.json, or pass `openai.endpoint`.",
     );
   }
+  assertCommercialEndpoint(endpoint);
 
   const apiKey =
     opts.openai?.apiKey ??
@@ -215,15 +201,31 @@ export function createEmbeddingProvider(
     now: opts.openai?.now,
   });
 
-  // Should we wrap with AutoFallback? Resolution: arg → env → config → false.
+  // Historical fallback inputs are rejected instead of silently weakening policy.
   const autoFallback = resolveAutoFallback(opts, env, cfg);
-  if (!autoFallback) return openaiProvider;
+  if (autoFallback) {
+    throw commercialApiHold("local auto-fallback is forbidden; commercial API failures must remain HOLD");
+  }
+  return openaiProvider;
+}
 
-  return new AutoFallbackEmbeddingProvider({
-    primary: openaiProvider,
-    fallback: new LocalLlamaCppProvider(opts.local ?? { modelId }),
-    ...(opts.autoFallbackOverrides ?? {}),
-  });
+export function assertCommercialEndpoint(endpoint: string): void {
+  let parsed: URL;
+  try {
+    parsed = new URL(endpoint);
+  } catch {
+    throw commercialApiHold("commercial provider endpoint is malformed");
+  }
+
+  const host = parsed.hostname.toLowerCase();
+  const privateIpv4 = /^(?:10\.|127\.|169\.254\.|192\.168\.|172\.(?:1[6-9]|2\d|3[01])\.)/;
+  const localHost = host === "localhost" || host === "models" || host.endsWith(".local");
+  const localIpv6 = host === "::1" || host.startsWith("fe80:") || host.startsWith("fc") || host.startsWith("fd");
+  if (parsed.protocol !== "https:" || localHost || privateIpv4.test(host) || localIpv6) {
+    throw commercialApiHold(
+      `endpoint ${parsed.protocol}//${host} is local, private, or non-TLS; use an approved commercial HTTPS API`,
+    );
+  }
 }
 
 function resolveAutoFallback(

+ 1 - 0
src/embedding/index.ts

@@ -34,6 +34,7 @@ export {
 export {
   createEmbeddingProvider,
   resolveProviderKind,
+  assertCommercialEndpoint,
   loadConfigFile,
   defaultConfigPath,
   type CreateEmbeddingProviderOptions,

+ 15 - 144
src/embedding/local.ts

@@ -1,15 +1,5 @@
-/**
- * local.ts - Local llama.cpp adapter implementing EmbeddingProvider.
- *
- * Wraps an existing `LlamaCpp` instance so the legacy GGUF path looks like
- * any other EmbeddingProvider to upstream callers. Used as the default and
- * as the fallback target when `OpenAIEmbeddingsProvider` trips its breaker.
- */
-
-import {
-  type LlamaCpp,
-  getDefaultLlamaCpp,
-} from "../llm.js";
+import { commercialApiHold } from "../model-policy.js";
+import type { LlamaCpp } from "../llm.js";
 import type {
   EmbeddingProvider,
   ProviderEmbedOptions,
@@ -18,146 +8,27 @@ import type {
   ProviderKind,
 } from "./provider.js";
 
-export type LocalLlamaCppProviderConfig = {
-  /** Pre-built LlamaCpp instance (optional — falls back to global singleton). */
-  llm?: LlamaCpp;
-  /**
-   * Stable model id reported via `getModelId()`. Defaults to "embeddinggemma"
-   * to match the value in `content_vectors.model` for existing qmd installs.
-   */
-  modelId?: string;
-};
+export type LocalLlamaCppProviderConfig = { llm?: LlamaCpp; modelId?: string };
 
+/** Historical SDK symbol. Construction is blocked by the commercial-only policy. */
 export class LocalLlamaCppProvider implements EmbeddingProvider {
   readonly kind: ProviderKind = "local";
 
-  private readonly llm: LlamaCpp;
-  private readonly modelId: string;
-  private dimensions: number | undefined = undefined;
-  private lastError: string | undefined = undefined;
-
-  constructor(config: LocalLlamaCppProviderConfig = {}) {
-    this.llm = config.llm ?? getDefaultLlamaCpp();
-    this.modelId = config.modelId ?? "embeddinggemma";
-  }
-
-  getModelId(): string {
-    return this.modelId;
-  }
-
-  getDimensions(): number | undefined {
-    return this.dimensions;
-  }
-
-  /**
-   * Most recent thrown error from `llm.embed` / `llm.embedBatch`. Returns
-   * `undefined` after a successful call or before the first call. See
-   * `EmbeddingProvider.getLastError`.
-   */
-  getLastError(): string | undefined {
-    return this.lastError;
+  constructor(_config: LocalLlamaCppProviderConfig = {}) {
+    throw commercialApiHold("LocalLlamaCppProvider is disabled; configure an approved commercial API");
   }
 
+  getModelId(): string { return "disabled-local-provider"; }
+  getDimensions(): number | undefined { return undefined; }
+  getLastError(): string | undefined { return "QMD_COMMERCIAL_API_HOLD"; }
   async healthcheck(_signal?: AbortSignal): Promise<ProviderHealth> {
-    // For the local provider, "healthy" means the embed model loads.
-    // We probe with a single embed call.
-    try {
-      const result = await this.llm.embed("healthcheck", { model: this.modelId });
-      if (!result) {
-        return {
-          ok: false,
-          model: this.modelId,
-          detail: "embed probe returned null",
-        };
-      }
-      this.dimensions = result.embedding.length;
-      return {
-        ok: true,
-        model: this.modelId,
-        dimensions: this.dimensions,
-        detail: `local llama.cpp ready, ${this.dimensions}-d`,
-      };
-    } catch (err) {
-      return {
-        ok: false,
-        model: this.modelId,
-        detail: err instanceof Error ? err.message : String(err),
-      };
-    }
+    throw commercialApiHold("local provider healthcheck is disabled");
   }
-
-  async embed(
-    text: string,
-    options: ProviderEmbedOptions = {},
-  ): Promise<ProviderEmbedding | null> {
-    if (options.signal?.aborted) {
-      this.lastError = `aborted by caller${options.signal.reason ? `: ${String(options.signal.reason)}` : ""}`;
-      return null;
-    }
-    let result;
-    try {
-      result = await this.llm.embed(text, { model: options.model ?? this.modelId });
-    } catch (err) {
-      this.lastError = `provider=local error="${err instanceof Error ? err.message : String(err)}"`;
-      return null;
-    }
-    if (!result) {
-      this.lastError = `provider=local error="llm.embed returned null/undefined"`;
-      return null;
-    }
-    if (this.dimensions === undefined) {
-      this.dimensions = result.embedding.length;
-    }
-    this.lastError = undefined;
-    return {
-      embedding: result.embedding,
-      model: this.modelId,
-    };
-  }
-
-  async embedBatch(
-    texts: string[],
-    options: ProviderEmbedOptions = {},
-  ): Promise<(ProviderEmbedding | null)[]> {
-    if (texts.length === 0) return [];
-    if (options.signal?.aborted) {
-      this.lastError = `aborted by caller${options.signal.reason ? `: ${String(options.signal.reason)}` : ""}`;
-      return texts.map(() => null);
-    }
-
-    let raw;
-    try {
-      raw = await this.llm.embedBatch(texts, {
-        model: options.model ?? this.modelId,
-      });
-    } catch (err) {
-      this.lastError = `provider=local error="${err instanceof Error ? err.message : String(err)}"`;
-      return texts.map(() => null);
-    }
-
-    const out = raw.map((r) => {
-      if (!r) return null;
-      if (this.dimensions === undefined && r.embedding.length > 0) {
-        this.dimensions = r.embedding.length;
-      }
-      return {
-        embedding: r.embedding,
-        model: this.modelId,
-      };
-    });
-
-    if (out.every((r) => r !== null)) {
-      this.lastError = undefined;
-    } else if (out.some((r) => r === null)) {
-      this.lastError = `provider=local error="llm.embedBatch returned null entries (${out.filter((r) => r === null).length}/${out.length})"`;
-    }
-
-    return out;
+  async embed(_text: string, _options: ProviderEmbedOptions = {}): Promise<ProviderEmbedding | null> {
+    throw commercialApiHold("local embedding is disabled");
   }
-
-  async dispose(): Promise<void> {
-    // We do NOT dispose the underlying LlamaCpp here because the singleton
-    // is shared with rerank/generate/expansion paths. Disposal is handled
-    // by the existing `disposeDefaultLlamaCpp()` global hook.
+  async embedBatch(_texts: string[], _options: ProviderEmbedOptions = {}): Promise<(ProviderEmbedding | null)[]> {
+    throw commercialApiHold("local batch embedding is disabled");
   }
+  async dispose(): Promise<void> {}
 }

+ 2 - 4
src/embedding/openai.ts

@@ -5,16 +5,14 @@
  * shape: request `{model, input: string|string[]}`, response
  * `{data: [{embedding: number[], index: number}, ...]}`.
  *
- * Used by qmd to delegate embeddings to a GPU worker (e.g. ai.mm.mk →
- * qmd-embed-worker on `models` LXC, RTX 4090) instead of running
- * node-llama-cpp locally.
+ * Used by qmd to delegate embeddings to an approved commercial HTTPS API.
  *
  * Features:
  *   - Batches input in groups of ≤64 (configurable via QMD_EMBED_BATCH_SIZE)
  *   - Retries 429 / 503 with exponential backoff (1s, 4s, 16s)
  *   - 4xx (non-429) → no retry, count as failure
  *   - Circuit breaker: >50% failures in a 60s window → OPEN for 5 min,
- *     callers can use this to fall back to a local provider
+ *     callers receive failures; local fallback is forbidden
  *   - Per-call timeout via AbortSignal (default QMD_EMBED_TIMEOUT_MS=30000)
  *   - Healthcheck via `GET /health` if available, else a probe embed call
  */

+ 3 - 5
src/embedding/provider.ts

@@ -1,9 +1,8 @@
 /**
  * provider.ts - Embedding provider abstraction
  *
- * Defines the EmbeddingProvider interface that allows qmd to use either:
- *   - LocalLlamaCppProvider (legacy, GGUF via node-llama-cpp)
- *   - OpenAIEmbeddingsProvider (HTTP, OpenAI-compatible endpoint like ai.mm.mk)
+ * Production embeddings use a commercial OpenAI-compatible API. The `local`
+ * kind remains readable only for historical config and is rejected by the factory.
  *
  * The factory in `./factory.ts` selects an implementation based on env vars,
  * a CLI flag, or `~/.config/qmd/config.json`.
@@ -47,7 +46,7 @@ export type ProviderEmbedOptions = {
 };
 
 /**
- * Provider interface — both LocalLlamaCppProvider and OpenAIEmbeddingsProvider implement this.
+ * Provider interface for commercial embedding adapters and historical readers.
  *
  * Implementations MUST:
  *   - Return `null` (not throw) for individual texts that fail to embed;
@@ -78,7 +77,6 @@ export interface EmbeddingProvider {
    * Should NOT throw — return `{ ok: false, detail: ... }` on failure.
    *
    * For HTTP providers: ping `/health` endpoint.
-   * For local provider: ensure model loads.
    */
   healthcheck(signal?: AbortSignal): Promise<ProviderHealth>;
 

+ 9 - 3
src/index.ts

@@ -128,12 +128,12 @@ export { Maintenance } from "./maintenance.js";
 import type { EmbeddingProvider } from "./embedding/index.js";
 
 // Re-export embedding provider abstraction for SDK consumers (i-qkarfffa).
-// `createEmbeddingProvider` honors QMD_EMBED_ENDPOINT / config-file / kind
-// arg precedence; default fallback is the legacy LocalLlamaCppProvider so
-// SDK code that doesn't pass `embedProvider` keeps the prior behavior.
+// `createEmbeddingProvider` is commercial-only. The historical local symbol
+// remains exported for source compatibility but its constructor returns HOLD.
 export {
   createEmbeddingProvider,
   resolveProviderKind,
+  assertCommercialEndpoint,
   LocalLlamaCppProvider,
   OpenAIEmbeddingsProvider,
   CircuitBreaker,
@@ -155,6 +155,12 @@ export {
   RETRY_BACKOFFS_MS as PROVIDER_RETRY_BACKOFFS_MS,
 } from "./embedding/index.js";
 
+export {
+  CommercialApiHoldError,
+  COMMERCIAL_API_HOLD_CODE,
+  commercialApiHold,
+} from "./model-policy.js";
+
 export { getDistinctEmbeddingModels } from "./store.js";
 
 /**

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 63 - 1260
src/llm.ts


+ 42 - 24
src/mcp/server.ts

@@ -26,7 +26,6 @@ import {
   getDefaultDbPath,
   DEFAULT_MULTI_GET_MAX_BYTES,
   createEmbeddingProvider,
-  resolveProviderKind,
   type QMDStore,
   type ExpandedQuery,
   type IndexStatus,
@@ -35,31 +34,50 @@ import {
 import { getConfigPath } from "../collections.js";
 
 /**
- * Build a query-side embedding provider (i-loazq6ze) for MCP server start.
- * Mirrors `buildQueryEmbedProvider` in the CLI: returns `undefined` when
- * the user has not opted into a remote provider, preserving pre-patch
- * behavior (local llama-cpp). Construction errors are logged and the
- * server falls back to the legacy path.
+ * Resolve the commercial provider only when a semantic operation first uses
+ * it. MCP startup and deterministic BM25/document tools remain available when
+ * commercial credentials are absent; semantic operations fail with typed HOLD.
  */
-function buildMcpEmbedProvider(): EmbeddingProvider | undefined {
-  const env = process.env;
-  const envOptIn = !!(
-    env.QMD_EMBED_PROVIDER ||
-    env.QMD_EMBED_ENDPOINT ||
-    env.QMD_EMBED_AUTO_FALLBACK
-  );
-  // Probe resolved kind via the factory's standard precedence (env + config).
-  const resolved = resolveProviderKind({});
-  if (!envOptIn && resolved === "local") return undefined;
-  try {
-    return createEmbeddingProvider({});
-  } catch (err) {
-    // Log + fall through to undefined so legacy local path is used.
-    process.stderr.write(
-      `[qmd mcp] WARN failed to build embedding provider — using local fallback: ${err instanceof Error ? err.message : String(err)}\n`,
-    );
-    return undefined;
+class LazyMcpEmbeddingProvider implements EmbeddingProvider {
+  readonly kind = "openai" as const;
+  private provider: EmbeddingProvider | undefined;
+
+  private resolve(): EmbeddingProvider {
+    this.provider ??= createEmbeddingProvider({});
+    return this.provider;
+  }
+
+  getModelId(): string {
+    return this.resolve().getModelId();
+  }
+
+  getDimensions(): number | undefined {
+    return this.provider?.getDimensions();
+  }
+
+  healthcheck(signal?: AbortSignal) {
+    return this.resolve().healthcheck(signal);
   }
+
+  embed(text: string, options?: Parameters<EmbeddingProvider["embed"]>[1]) {
+    return this.resolve().embed(text, options);
+  }
+
+  embedBatch(texts: string[], options?: Parameters<EmbeddingProvider["embedBatch"]>[1]) {
+    return this.resolve().embedBatch(texts, options);
+  }
+
+  getLastError(): string | undefined {
+    return this.provider?.getLastError?.();
+  }
+
+  async dispose(): Promise<void> {
+    await this.provider?.dispose();
+  }
+}
+
+function buildMcpEmbedProvider(): EmbeddingProvider {
+  return new LazyMcpEmbeddingProvider();
 }
 
 // =============================================================================

+ 16 - 0
src/model-policy.ts

@@ -0,0 +1,16 @@
+export const COMMERCIAL_API_HOLD_CODE = "QMD_COMMERCIAL_API_HOLD" as const;
+
+/** Fail-closed result for learned-model work without an approved commercial API. */
+export class CommercialApiHoldError extends Error {
+  readonly code = COMMERCIAL_API_HOLD_CODE;
+  readonly disposition = "HOLD" as const;
+
+  constructor(reason: string) {
+    super(`[${COMMERCIAL_API_HOLD_CODE}] HOLD: ${reason}`);
+    this.name = "CommercialApiHoldError";
+  }
+}
+
+export function commercialApiHold(reason: string): CommercialApiHoldError {
+  return new CommercialApiHoldError(reason);
+}

+ 22 - 42
src/store.ts

@@ -1371,13 +1371,12 @@ export type EmbedOptions = {
   chunkStrategy?: ChunkStrategy;
   onProgress?: (info: EmbedProgress) => void;
   /**
-   * Optional embedding provider. When supplied, embeddings are routed through
-   * this provider (HTTP, GPU worker, etc.) instead of the local llama.cpp
-   * session path. The provider's `getModelId()` is verified against existing
+   * Required provider for embedding work. Embeddings are routed through the
+   * approved commercial HTTPS API. The provider's `getModelId()` is verified against existing
    * `content_vectors.model` rows; mismatch throws unless `force` is set.
    *
-   * When omitted, behavior is identical to pre-patch: embeddings come from
-   * the store's `LlamaCpp` (or the global singleton).
+   * When omitted, learned work reaches the fail-closed compatibility adapter
+   * and returns typed HOLD.
    */
   embedProvider?: EmbeddingProvider;
   /**
@@ -1511,13 +1510,10 @@ function getEmbeddingDocsForBatch(db: Database, batch: PendingEmbeddingDoc[]): E
 /**
  * Run `body` with a session-shaped argument that supplies an AbortSignal +
  * isValid flag. When `provider` is supplied, the session is a lightweight
- * AbortController-backed stub — `getLlm(store)` is never called and
- * `withLLMSessionForLlm` is bypassed entirely, so node-llama-cpp is not
- * warmed up on remote-only deployments (i-08ovbvtb, follow-up to i-qkarfffa).
+ * AbortController-backed stub; `getLlm(store)` and the fail-closed legacy
+ * session wrapper are bypassed entirely.
  *
- * When `provider` is undefined, behavior is unchanged: a real `LLMSession`
- * is created via `withLLMSessionForLlm(getLlm(store), ...)` so that the
- * body can use `session.embed`/`session.embedBatch` for the local path.
+ * When `provider` is undefined, the compatibility session returns typed HOLD.
  *
  * The fake session's LLM-only methods (embed/embedBatch/expandQuery/rerank)
  * throw if called — they MUST NOT be reached when `provider` is set, since
@@ -1612,7 +1608,7 @@ export async function generateEmbeddings(
   }
 
   // Provider routing — when an EmbeddingProvider is supplied, embed calls go
-  // through it (HTTP, GPU worker, etc.). Otherwise, use the LLM session path.
+  // through it. Otherwise, use the fail-closed compatibility session path.
   // The outer session is still created for its abort signal (chunking uses
   // `session.signal` for cooperative cancellation).
   const provider = options?.embedProvider;
@@ -1620,15 +1616,13 @@ export async function generateEmbeddings(
 
   // Resolve `embedModelUri` (used for formatting prefixes etc.) lazily —
   // when `provider` is set, take it from the provider; otherwise fall back
-  // to the local LlamaCpp's embed model name. Accessing `getLlm(store)` is
-  // deferred to the non-provider branch so remote-only deployments do not
-  // construct a `LlamaCpp` instance just to read its embedModelName.
+  // to the disabled compatibility adapter's model name. Accessing `getLlm(store)`
+  // is deferred to the non-provider branch.
   const embedModelUri = provider
     ? provider.getModelId()
     : getLlm(store).embedModelName;
 
-  // Run the embedding loop inside a session-scoped wrapper. When `provider`
-  // is set, this short-circuits the local LLM warm-up entirely (i-08ovbvtb).
+  // Run the embedding loop inside a session-scoped wrapper.
   const result = await withEmbedSession(store, provider, async (session) => {
     let chunksEmbedded = 0;
     let errors = 0;
@@ -1673,8 +1667,7 @@ export async function generateEmbeddings(
     // avgCharsPerToken=3 — matches the heuristic the chunker already
     // uses for its initial char-space pass, so the safety re-split is a
     // near no-op while populating the `tokens` field with a stable
-    // estimate. CRITICAL: avoids loading node-llama-cpp on remote-only
-    // deployments (`QMD_EMBED_ENDPOINT=...`). i-1rqixh6m DoD #1.
+    // estimate without invoking any learned tokenizer.
     const chunkTokenizer: TokenCounter | undefined = provider
       ? (text: string) => Math.ceil(text.length / 3)
       : undefined;
@@ -2630,14 +2623,10 @@ function chunkByFunctionRanges(
  * Counts the tokens in `text`. Used by `chunkDocumentByTokens` for the
  * safety re-split that splits chunks exceeding `maxTokens`.
  *
- * When `chunkDocumentByTokens` is called WITHOUT a tokenizer (default),
- * it lazily resolves `getDefaultLlamaCpp()` and uses `llm.tokenize` —
- * accurate but expensive (loads the local GGUF embed model + initialises
- * llama.cpp, ~22s on cold cache).
+ * When `chunkDocumentByTokens` is called without a tokenizer, the disabled
+ * compatibility adapter returns typed HOLD.
  *
- * Provider-mode callers (HTTP embed providers like the GPU worker on
- * `models` LXC) MUST pass a JS-only approximator to avoid loading the
- * local model entirely. A char-based estimate like
+ * Commercial-provider callers pass a deterministic JS-only approximator. A char-based estimate like
  * `Math.ceil(text.length / 3)` is a reasonable default — it matches the
  * `avgCharsPerToken=3` heuristic used for the initial char-space chunk
  * step, so the safety re-split stays a near no-op while populating the
@@ -2646,14 +2635,9 @@ function chunkByFunctionRanges(
 export type TokenCounter = (text: string) => number | Promise<number>;
 
 /**
- * Chunk a document by actual token count using the LLM tokenizer.
- * More accurate than character-based chunking but requires async.
+ * Chunk a document with an injected token counter.
  *
- * When `tokenizer` is supplied, it is used in place of the local
- * `llm.tokenize(...)` call — neither `getDefaultLlamaCpp()` nor
- * `llm.tokenize(...)` is invoked. This lets remote-only deployments
- * (`QMD_EMBED_ENDPOINT=...`) chunk documents without warming up
- * node-llama-cpp (DoD #1 of i-1rqixh6m / i-qkarfffa).
+ * When `tokenizer` is supplied, no compatibility learned adapter is invoked.
  *
  * When `filepath` and `chunkStrategy` are provided, uses AST-aware break
  * points for supported code files.
@@ -2669,9 +2653,7 @@ export async function chunkDocumentByTokens(
   tokenizer?: TokenCounter,
 ): Promise<{ text: string; pos: number; tokens: number }[]> {
   // Resolve token counter lazily so callers that supply `tokenizer` never
-  // touch the local LlamaCpp instance — `getDefaultLlamaCpp()` is only
-  // invoked from inside the default closure when it is actually called
-  // (i.e. when no tokenizer is supplied).
+  // touch the disabled compatibility adapter unless no tokenizer was supplied.
   let llm: ReturnType<typeof getDefaultLlamaCpp> | undefined;
   const countTokens: TokenCounter = tokenizer ?? (async (text: string) => {
     if (!llm) llm = getDefaultLlamaCpp();
@@ -3559,8 +3541,7 @@ export async function searchVec(db: Database, query: string, model: string, limi
 
 async function getEmbedding(text: string, model: string, isQuery: boolean, session?: ILLMSession, llmOverride?: LlamaCpp, embedProvider?: EmbeddingProvider): Promise<number[] | null> {
   // When an EmbeddingProvider is supplied, route the encoding through it
-  // (HTTP / GPU worker / fallback chain) instead of touching local
-  // node-llama-cpp at all. The provider sees the raw text + the desired
+  // through the approved commercial API. The provider sees the raw text + the desired
   // model id; query-formatting prefixes are still applied via
   // formatQueryForEmbedding so embedding parity with the index is preserved.
   if (embedProvider) {
@@ -4731,7 +4712,7 @@ export interface VectorSearchResult {
  *
  * Pipeline:
  * 1. expandQuery() → typed variants, filter to vec/hyde only (lex irrelevant here)
- * 2. searchVec() for original + vec/hyde variants (sequential — node-llama-cpp embed limitation)
+ * 2. searchVec() for original + vec/hyde variants through the commercial provider
  * 3. Dedup by filepath (keep max score)
  * 4. Sort by score descending, filter by minScore, slice to limit
  */
@@ -4832,9 +4813,8 @@ export interface StructuredSearchOptions {
  * 5. Position-aware score blending
  * 6. Dedup, filter, slice
  *
- * This is the recommended endpoint for capable LLMs — they can generate
- * better query variations than our small local model, especially for
- * domain-specific or nuanced queries.
+ * This is the recommended endpoint when the caller supplies domain-specific
+ * query variants and a commercial provider contract is active.
  */
 export async function structuredSearch(
   store: Store,

+ 1 - 1
src/test-preload.ts

@@ -1,7 +1,7 @@
 /**
  * Test preload file to ensure proper cleanup of native resources.
  *
- * Uses bun:test afterAll to properly dispose of llama.cpp Metal
+ * Uses bun:test afterAll to dispose compatibility resources.
  * resources before the process exits, avoiding GGML_ASSERT failures.
  */
 import { afterAll } from "bun:test";

+ 44 - 25
test/embedding-factory.test.ts

@@ -6,7 +6,7 @@
  *   2. QMD_EMBED_PROVIDER env
  *   3. QMD_EMBED_ENDPOINT env (forces openai)
  *   4. config file `embedProvider.kind` / `embedProvider.endpoint`
- *   5. fallback: local
+ *   5. missing commercial configuration: typed HOLD
  */
 
 import { describe, test, expect, beforeEach, afterEach } from "vitest";
@@ -17,9 +17,10 @@ import {
   resolveProviderKind,
   createEmbeddingProvider,
   loadConfigFile,
+  assertCommercialEndpoint,
 } from "../src/embedding/factory.js";
 import { OpenAIEmbeddingsProvider } from "../src/embedding/openai.js";
-import { LocalLlamaCppProvider } from "../src/embedding/local.js";
+import { CommercialApiHoldError } from "../src/model-policy.js";
 
 let workDir: string;
 let configPath: string;
@@ -45,14 +46,12 @@ const EMPTY_ENV: Record<string, string | undefined> = {};
 // ─────────────────────────── resolveProviderKind ─────────────────────────────
 
 describe("resolveProviderKind", () => {
-  test("explicit kind argument wins", () => {
-    expect(
-      resolveProviderKind({
+  test("explicit local kind is rejected", () => {
+    expect(() => resolveProviderKind({
         kind: "local",
         env: { QMD_EMBED_ENDPOINT: "https://x" },
         configPath,
-      }),
-    ).toBe("local");
+      })).toThrow(CommercialApiHoldError);
     expect(
       resolveProviderKind({
         kind: "openai",
@@ -62,13 +61,11 @@ describe("resolveProviderKind", () => {
     ).toBe("openai");
   });
 
-  test("QMD_EMBED_PROVIDER env wins over QMD_EMBED_ENDPOINT", () => {
-    expect(
-      resolveProviderKind({
+  test("QMD_EMBED_PROVIDER=local is rejected even with an endpoint", () => {
+    expect(() => resolveProviderKind({
         env: { QMD_EMBED_PROVIDER: "local", QMD_EMBED_ENDPOINT: "https://x" },
         configPath,
-      }),
-    ).toBe("local");
+      })).toThrow(CommercialApiHoldError);
   });
 
   test("QMD_EMBED_ENDPOINT presence → openai", () => {
@@ -80,13 +77,13 @@ describe("resolveProviderKind", () => {
     ).toBe("openai");
   });
 
-  test("QMD_EMBED_ENDPOINT empty string ignored", () => {
+  test("QMD_EMBED_ENDPOINT empty string resolves commercial kind then holds at construction", () => {
     expect(
       resolveProviderKind({
         env: { QMD_EMBED_ENDPOINT: "" },
         configPath,
       }),
-    ).toBe("local");
+    ).toBe("openai");
   });
 
   test("config file embedProvider.kind respected", () => {
@@ -99,8 +96,8 @@ describe("resolveProviderKind", () => {
     expect(resolveProviderKind({ env: EMPTY_ENV, configPath })).toBe("openai");
   });
 
-  test("no signal anywhere → local fallback", () => {
-    expect(resolveProviderKind({ env: EMPTY_ENV, configPath })).toBe("local");
+  test("no signal anywhere keeps commercial-only kind", () => {
+    expect(resolveProviderKind({ env: EMPTY_ENV, configPath })).toBe("openai");
   });
 
   test("invalid env QMD_EMBED_PROVIDER is ignored", () => {
@@ -109,7 +106,7 @@ describe("resolveProviderKind", () => {
         env: { QMD_EMBED_PROVIDER: "garbage" },
         configPath,
       }),
-    ).toBe("local");
+    ).toBe("openai");
   });
 
   test("uppercase env QMD_EMBED_PROVIDER normalized", () => {
@@ -226,19 +223,41 @@ describe("createEmbeddingProvider", () => {
     ).toThrow(/endpoint/);
   });
 
-  test("local kind explicitly requested → LocalLlamaCppProvider", () => {
-    const p = createEmbeddingProvider({
+  test("local kind explicitly requested → typed HOLD", () => {
+    expect(() => createEmbeddingProvider({
       kind: "local",
       env: EMPTY_ENV,
       configPath,
-    });
-    expect(p).toBeInstanceOf(LocalLlamaCppProvider);
-    expect(p.kind).toBe("local");
+    })).toThrow(CommercialApiHoldError);
+  });
+
+  test("missing endpoint → typed HOLD", () => {
+    expect(() => createEmbeddingProvider({ env: EMPTY_ENV, configPath }))
+      .toThrow(CommercialApiHoldError);
+  });
+
+  test("legacy auto-fallback request → typed HOLD", () => {
+    expect(() => createEmbeddingProvider({
+      env: { QMD_EMBED_ENDPOINT: "https://commercial.example.com" },
+      autoFallback: true,
+      configPath,
+    })).toThrow(CommercialApiHoldError);
+  });
+
+  test("self-hosted and local endpoints → typed HOLD", () => {
+    for (const endpoint of [
+      "http://models:8082",
+      "http://127.0.0.1:8082",
+      "https://10.0.2.162/v1",
+      "https://localhost/v1",
+    ]) {
+      expect(() => assertCommercialEndpoint(endpoint)).toThrow(CommercialApiHoldError);
+    }
   });
 
-  test("default fallback → LocalLlamaCppProvider", () => {
-    const p = createEmbeddingProvider({ env: EMPTY_ENV, configPath });
-    expect(p).toBeInstanceOf(LocalLlamaCppProvider);
+  test("commercial HTTPS endpoint is accepted", () => {
+    expect(() => assertCommercialEndpoint("https://generativelanguage.googleapis.com/v1beta/openai"))
+      .not.toThrow();
   });
 });
 

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно