Migrate to node-llama-cpp and add structured query expansion

- Replace Ollama HTTP API with node-llama-cpp for local GGUF models
- Add structured query expansion using JSON schema grammar:
  - Generates lexical query (for BM25), vector query, and HyDE
  - Tree-style CLI output showing query types
- Fix vector search: use cosine distance instead of L2
- Format queries with embeddinggemma nomic-style prompts
- Rename ollama_cache table to llm_cache
- Add disposeDefaultLlamaCpp() for clean process exit

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Tobi Lutke 2025-12-20 18:03:41 -04:00
parent a3703c069a
commit d383b5c226
No known key found for this signature in database
10 changed files with 1683 additions and 1715 deletions

View File

@ -20,7 +20,7 @@ qmd get <file> # Get document by path or docid (#abc123)
qmd multi-get <pattern> # Get multiple docs by glob or comma-separated list
qmd status # Show index status and collections
qmd update [--pull] # Re-index all collections (--pull: git pull first)
qmd embed # Generate vector embeddings (requires Ollama)
qmd embed # Generate vector embeddings (uses node-llama-cpp)
qmd search <query> # BM25 full-text search
qmd vsearch <query> # Vector similarity search
qmd query <query> # Hybrid search with reranking (best quality)
@ -124,8 +124,9 @@ bun link # Install globally as 'qmd'
- SQLite FTS5 for full-text search (BM25)
- sqlite-vec for vector similarity search
- Ollama for embeddings (embeddinggemma) and reranking (qwen3-reranker)
- node-llama-cpp for embeddings (embeddinggemma), reranking (qwen3-reranker), and query expansion (Qwen3)
- Reciprocal Rank Fusion (RRF) for combining results
- Token-based chunking: 800 tokens/chunk with 15% overlap
## Important: Do NOT run automatically

View File

@ -2,7 +2,7 @@
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.
QMD combines BM25 full-text search, vector semantic search, and LLM re-ranking—all running locally via Ollama.
QMD combines BM25 full-text search, vector semantic search, and LLM re-ranking—all running locally via node-llama-cpp with GGUF models.
## Quick Start
@ -112,7 +112,7 @@ Although the tool works perfectly fine when you just tell your agent to use it o
▼ ▼
┌────────────────┐ ┌────────────────┐
│ Query Expansion│ │ Original Query│
(qwen3:0.6b) │ │ (×2 weight) │
(Qwen3-0.6B) │ │ (×2 weight) │
└───────┬────────┘ └───────┬────────┘
│ │
│ 2 alternative queries │
@ -204,24 +204,18 @@ The `query` command uses **Reciprocal Rank Fusion (RRF)** with position-aware bl
```sh
brew install sqlite
```
- **Ollama** running locally (default: `http://localhost:11434`)
### Ollama Models
### GGUF Models (via node-llama-cpp)
QMD uses three models (auto-pulled if missing):
QMD uses three local GGUF models (auto-downloaded on first use):
| Model | Purpose | Size |
|-------|---------|------|
| `embeddinggemma` | Vector embeddings | ~1.6GB |
| `ExpedientFalcon/qwen3-reranker:0.6b-q8_0` | Re-ranking (trained) | ~640MB |
| `qwen3:0.6b` | Query expansion | ~400MB |
| `embeddinggemma-300M-Q8_0` | Vector embeddings | ~300MB |
| `qwen3-reranker-0.6b-q8_0` | Re-ranking | ~640MB |
| `Qwen3-0.6B-Q8_0` | Query expansion | ~640MB |
```sh
# Pre-pull models (optional)
ollama pull embeddinggemma
ollama pull ExpedientFalcon/qwen3-reranker:0.6b-q8_0
ollama pull qwen3:0.6b
```
Models are downloaded from HuggingFace and cached in `~/.cache/qmd/models/`.
## Installation
@ -257,7 +251,7 @@ qmd ls notes/subfolder
### Generate Vector Embeddings
```sh
# Embed all indexed documents (chunked into ~6KB pieces)
# Embed all indexed documents (800 tokens/chunk, 15% overlap)
qmd embed
# Force re-embed everything
@ -434,16 +428,15 @@ collections -- Indexed directories with name and glob patterns
path_contexts -- Context descriptions by virtual path (qmd://...)
documents -- Markdown content with metadata and docid (6-char hash)
documents_fts -- FTS5 full-text index
content_vectors -- Embedding chunks (hash, seq, pos)
content_vectors -- Embedding chunks (hash, seq, pos, 800 tokens each)
vectors_vec -- sqlite-vec vector index (hash_seq key)
ollama_cache -- Cached API responses
llm_cache -- Cached LLM responses (query expansion, rerank scores)
```
## Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `OLLAMA_URL` | `http://localhost:11434` | Ollama API endpoint |
| `XDG_CACHE_HOME` | `~/.cache` | Cache directory location |
## How It Works
@ -465,11 +458,11 @@ Collection ──► Glob Pattern ──► Markdown Files ──► Parse Title
### Embedding Flow
Documents are chunked into ~6KB pieces to fit the embedding model's token window:
Documents are chunked into 800-token pieces with 15% overlap:
```
Document ──► Chunk (~6KB each) ──► Format each chunk ──► Ollama API ──► Store Vectors
│ "title | text" /api/embed
Document ──► Chunk (800 tokens) ──► Format each chunk ──► node-llama-cpp ──► Store Vectors
│ "title | text" embedBatch()
└─► Chunks stored with:
- hash: document hash
@ -517,12 +510,12 @@ Query ──► LLM Expansion ──► [Original, Variant 1, Variant 2]
## Model Configuration
Models are configured as constants in `src/qmd.ts`:
Models are configured in `src/llm.ts` as HuggingFace URIs:
```typescript
const DEFAULT_EMBED_MODEL = "embeddinggemma";
const DEFAULT_RERANK_MODEL = "ExpedientFalcon/qwen3-reranker:0.6b-q8_0";
const DEFAULT_QUERY_MODEL = "qwen3:0.6b";
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";
```
### EmbeddingGemma Prompt Format
@ -537,24 +530,11 @@ const DEFAULT_QUERY_MODEL = "qwen3:0.6b";
### Qwen3-Reranker
A dedicated reranker model trained on relevance classification:
```
System: Judge whether the Document meets the requirements based on the Query
and the Instruct provided. Note that the answer can only be "yes" or "no".
User: <Instruct>: Given a search query, determine if the document is relevant...
<Query>: {query}
<Document>: {doc}
```
- Uses `logprobs: true` to extract token probabilities
- Outputs yes/no with confidence score (0.0 - 1.0)
- `num_predict: 1` - Only need the yes/no token
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)
- `num_predict: 150` - For generating query variations
Used for generating query variations via `LlamaChatSession`.
## License

409
bun.lock
View File

@ -6,6 +6,7 @@
"name": "2025-12-07-bm25-q",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.24.3",
"node-llama-cpp": "^3.14.5",
"sqlite-vec": "^0.1.7-alpha.2",
"yaml": "^2.8.2",
"zod": "^4.1.13",
@ -25,8 +26,112 @@
},
},
"packages": {
"@huggingface/jinja": ["@huggingface/jinja@0.5.3", "", {}, "sha512-asqfZ4GQS0hD876Uw4qiUb7Tr/V5Q+JZuo2L+BtdrD4U40QU58nIRq3ZSgAzJgT874VLjhGVacaYfrdpXtEvtA=="],
"@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.24.3", "", { "dependencies": { "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.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-YgSHW29fuzKKAHTGe9zjNoo+yF8KaQPzDC2W9Pv41E7/57IfY+AMGJ/aDFlgTLcVVELoggKE4syABCE75u3NCw=="],
"@node-llama-cpp/linux-arm64": ["@node-llama-cpp/linux-arm64@3.14.5", "", { "os": "linux", "cpu": [ "x64", "arm64", ] }, "sha512-58IcWW7EOqc/66mYWXRsoMCy1MR3pTX/YaC0HYF9Rg5XeAPKhUP7NHrglbqgjO62CkcuFZaSEiX2AtG972GQYQ=="],
"@node-llama-cpp/linux-armv7l": ["@node-llama-cpp/linux-armv7l@3.14.5", "", { "os": "linux", "cpu": [ "arm", "x64", ] }, "sha512-mJWN0qWsn8y+r/34DC3XlSiXjjKs6wX1BTx0wwJ37fWefS/qfzuBJwQGqpfqe5xpfafib/RgQX44fsvE/9yb1w=="],
"@node-llama-cpp/linux-x64": ["@node-llama-cpp/linux-x64@3.14.5", "", { "os": "linux", "cpu": "x64" }, "sha512-f6xCqlSqSxMP9Iwm3CpaTzFybbHrzpLkNzA18v21PwhMN8u4DP44euLoxe+BMbOpyzx4iMxU1AUsPsgcHD1Y4w=="],
"@node-llama-cpp/linux-x64-cuda": ["@node-llama-cpp/linux-x64-cuda@3.14.5", "", { "os": "linux", "cpu": "x64" }, "sha512-yk0EGnAJ+m/paSaItigmxcqC8nNjZlkx9yZgQE51CsTip7tmnqqlj60pW1fWmhrjOJ9XnRlVVTP81fa9B+O1Hg=="],
"@node-llama-cpp/linux-x64-cuda-ext": ["@node-llama-cpp/linux-x64-cuda-ext@3.14.5", "", { "os": "linux", "cpu": "x64" }, "sha512-AACXmXjqvAppoC6Z20UI7yeSZaFb6uP9x/2lzctVwlm42ef76SN6DNXaX1yzH7DTyzK5zYhoH4ycJUe+zOeGzw=="],
"@node-llama-cpp/linux-x64-vulkan": ["@node-llama-cpp/linux-x64-vulkan@3.14.5", "", { "os": "linux", "cpu": "x64" }, "sha512-9wZG90CUyyO8EsqfDEh03/fK0ctbQFbKaAFa6Goh+jFLOtqPL+plLqAsW3jDFdLRF5+oAPTKt9/4Y7vHTajQbQ=="],
"@node-llama-cpp/mac-arm64-metal": ["@node-llama-cpp/mac-arm64-metal@3.14.5", "", { "os": "darwin", "cpu": [ "x64", "arm64", ] }, "sha512-7pclj/nbQyx7gPVbyqkCn+ftlGcnw7YrewxBv1/BWWAMzBrMt2+qkjtUcUhwXH7mT5WN/+eWsszhIMXH3Uf6vQ=="],
"@node-llama-cpp/mac-x64": ["@node-llama-cpp/mac-x64@3.14.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-iZBmLgPkLKiKS0lYAuqq8i85etGeQ9L+AjEJUhG5N6T/vCF4XSOkUTsEFMEX+iJLV3VxvY/C8R1e/UF7InUjUg=="],
"@node-llama-cpp/win-arm64": ["@node-llama-cpp/win-arm64@3.14.5", "", { "os": "win32", "cpu": [ "x64", "arm64", ] }, "sha512-WTZJeb2JZo/qPNHf++xA2YeMXB46G7G4WsKEnHVyCpAhhslHAhe/LPgSQfNfk9rYusbsRiy9QMxeGNSOowZMVQ=="],
"@node-llama-cpp/win-x64": ["@node-llama-cpp/win-x64@3.14.5", "", { "os": "win32", "cpu": "x64" }, "sha512-cEuhb1iLTodM+V8xc1mWKeWRYkX9tlnl0+9jUjwsv2kgnAjEob3WlTYsCXewvEe2ShSyk8AsLsBPZxv7IQaBsw=="],
"@node-llama-cpp/win-x64-cuda": ["@node-llama-cpp/win-x64-cuda@3.14.5", "", { "os": "win32", "cpu": "x64" }, "sha512-gwBMSzUteLD765Gq/hYQ4UC21vggR7oG+DU4zAg0Mt3i34PqKJC+tBop5jsTN5Hq8RaM9+nTNrVbF/x228TLvg=="],
"@node-llama-cpp/win-x64-cuda-ext": ["@node-llama-cpp/win-x64-cuda-ext@3.14.5", "", { "os": "win32", "cpu": "x64" }, "sha512-kBHnUmodr+n8N+sKTh1c6aNNEmvXBWM5AtaLWIEfkCb00bVHNFeqYPmLuPNtMX3dIUtD9PHdA4Jsn0RJmNZJfA=="],
"@node-llama-cpp/win-x64-vulkan": ["@node-llama-cpp/win-x64-vulkan@3.14.5", "", { "os": "win32", "cpu": "x64" }, "sha512-rY+vr5RaGSCWEe22WZMkhUu16o9zpeqTZO/nD5G27Y0bb+xBRDLmXbxYMp2dDQTfpkNWIZ0ia3PGWwl5yhYw7A=="],
"@octokit/app": ["@octokit/app@16.1.2", "", { "dependencies": { "@octokit/auth-app": "^8.1.2", "@octokit/auth-unauthenticated": "^7.0.3", "@octokit/core": "^7.0.6", "@octokit/oauth-app": "^8.0.3", "@octokit/plugin-paginate-rest": "^14.0.0", "@octokit/types": "^16.0.0", "@octokit/webhooks": "^14.0.0" } }, "sha512-8j7sEpUYVj18dxvh0KWj6W/l6uAiVRBl1JBDVRqH1VHKAO/G5eRVl4yEoYACjakWers1DjUkcCHyJNQK47JqyQ=="],
"@octokit/auth-app": ["@octokit/auth-app@8.1.2", "", { "dependencies": { "@octokit/auth-oauth-app": "^9.0.3", "@octokit/auth-oauth-user": "^6.0.2", "@octokit/request": "^10.0.6", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "toad-cache": "^3.7.0", "universal-github-app-jwt": "^2.2.0", "universal-user-agent": "^7.0.0" } }, "sha512-db8VO0PqXxfzI6GdjtgEFHY9tzqUql5xMFXYA12juq8TeTgPAuiiP3zid4h50lwlIP457p5+56PnJOgd2GGBuw=="],
"@octokit/auth-oauth-app": ["@octokit/auth-oauth-app@9.0.3", "", { "dependencies": { "@octokit/auth-oauth-device": "^8.0.3", "@octokit/auth-oauth-user": "^6.0.2", "@octokit/request": "^10.0.6", "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-+yoFQquaF8OxJSxTb7rnytBIC2ZLbLqA/yb71I4ZXT9+Slw4TziV9j/kyGhUFRRTF2+7WlnIWsePZCWHs+OGjg=="],
"@octokit/auth-oauth-device": ["@octokit/auth-oauth-device@8.0.3", "", { "dependencies": { "@octokit/oauth-methods": "^6.0.2", "@octokit/request": "^10.0.6", "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-zh2W0mKKMh/VWZhSqlaCzY7qFyrgd9oTWmTmHaXnHNeQRCZr/CXy2jCgHo4e4dJVTiuxP5dLa0YM5p5QVhJHbw=="],
"@octokit/auth-oauth-user": ["@octokit/auth-oauth-user@6.0.2", "", { "dependencies": { "@octokit/auth-oauth-device": "^8.0.3", "@octokit/oauth-methods": "^6.0.2", "@octokit/request": "^10.0.6", "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-qLoPPc6E6GJoz3XeDG/pnDhJpTkODTGG4kY0/Py154i/I003O9NazkrwJwRuzgCalhzyIeWQ+6MDvkUmKXjg/A=="],
"@octokit/auth-token": ["@octokit/auth-token@6.0.0", "", {}, "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w=="],
"@octokit/auth-unauthenticated": ["@octokit/auth-unauthenticated@7.0.3", "", { "dependencies": { "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0" } }, "sha512-8Jb1mtUdmBHL7lGmop9mU9ArMRUTRhg8vp0T1VtZ4yd9vEm3zcLwmjQkhNEduKawOOORie61xhtYIhTDN+ZQ3g=="],
"@octokit/core": ["@octokit/core@7.0.6", "", { "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.3", "@octokit/request": "^10.0.6", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "before-after-hook": "^4.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q=="],
"@octokit/endpoint": ["@octokit/endpoint@11.0.2", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-4zCpzP1fWc7QlqunZ5bSEjxc6yLAlRTnDwKtgXfcI/FxxGoqedDG8V2+xJ60bV2kODqcGB+nATdtap/XYq2NZQ=="],
"@octokit/graphql": ["@octokit/graphql@9.0.3", "", { "dependencies": { "@octokit/request": "^10.0.6", "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA=="],
"@octokit/oauth-app": ["@octokit/oauth-app@8.0.3", "", { "dependencies": { "@octokit/auth-oauth-app": "^9.0.2", "@octokit/auth-oauth-user": "^6.0.1", "@octokit/auth-unauthenticated": "^7.0.2", "@octokit/core": "^7.0.5", "@octokit/oauth-authorization-url": "^8.0.0", "@octokit/oauth-methods": "^6.0.1", "@types/aws-lambda": "^8.10.83", "universal-user-agent": "^7.0.0" } }, "sha512-jnAjvTsPepyUaMu9e69hYBuozEPgYqP4Z3UnpmvoIzHDpf8EXDGvTY1l1jK0RsZ194oRd+k6Hm13oRU8EoDFwg=="],
"@octokit/oauth-authorization-url": ["@octokit/oauth-authorization-url@8.0.0", "", {}, "sha512-7QoLPRh/ssEA/HuHBHdVdSgF8xNLz/Bc5m9fZkArJE5bb6NmVkDm3anKxXPmN1zh6b5WKZPRr3697xKT/yM3qQ=="],
"@octokit/oauth-methods": ["@octokit/oauth-methods@6.0.2", "", { "dependencies": { "@octokit/oauth-authorization-url": "^8.0.0", "@octokit/request": "^10.0.6", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0" } }, "sha512-HiNOO3MqLxlt5Da5bZbLV8Zarnphi4y9XehrbaFMkcoJ+FL7sMxH/UlUsCVxpddVu4qvNDrBdaTVE2o4ITK8ng=="],
"@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="],
"@octokit/openapi-webhooks-types": ["@octokit/openapi-webhooks-types@12.1.0", "", {}, "sha512-WiuzhOsiOvb7W3Pvmhf8d2C6qaLHXrWiLBP4nJ/4kydu+wpagV5Fkz9RfQwV2afYzv3PB+3xYgp4mAdNGjDprA=="],
"@octokit/plugin-paginate-graphql": ["@octokit/plugin-paginate-graphql@6.0.0", "", { "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-crfpnIoFiBtRkvPqOyLOsw12XsveYuY2ieP6uYDosoUegBJpSVxGwut9sxUgFFcll3VTOTqpUf8yGd8x1OmAkQ=="],
"@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@14.0.0", "", { "dependencies": { "@octokit/types": "^16.0.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw=="],
"@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@17.0.0", "", { "dependencies": { "@octokit/types": "^16.0.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw=="],
"@octokit/plugin-retry": ["@octokit/plugin-retry@8.0.3", "", { "dependencies": { "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "bottleneck": "^2.15.3" }, "peerDependencies": { "@octokit/core": ">=7" } }, "sha512-vKGx1i3MC0za53IzYBSBXcrhmd+daQDzuZfYDd52X5S0M2otf3kVZTVP8bLA3EkU0lTvd1WEC2OlNNa4G+dohA=="],
"@octokit/plugin-throttling": ["@octokit/plugin-throttling@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "bottleneck": "^2.15.3" }, "peerDependencies": { "@octokit/core": "^7.0.0" } }, "sha512-34eE0RkFCKycLl2D2kq7W+LovheM/ex3AwZCYN8udpi6bxsyjZidb2McXs69hZhLmJlDqTSP8cH+jSRpiaijBg=="],
"@octokit/request": ["@octokit/request@10.0.7", "", { "dependencies": { "@octokit/endpoint": "^11.0.2", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-v93h0i1yu4idj8qFPZwjehoJx4j3Ntn+JhXsdJrG9pYaX6j/XRz2RmasMUHtNgQD39nrv/VwTWSqK0RNXR8upA=="],
"@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="],
"@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="],
"@octokit/webhooks": ["@octokit/webhooks@14.2.0", "", { "dependencies": { "@octokit/openapi-webhooks-types": "12.1.0", "@octokit/request-error": "^7.0.0", "@octokit/webhooks-methods": "^6.0.0" } }, "sha512-da6KbdNCV5sr1/txD896V+6W0iamFWrvVl8cHkBSPT+YlvmT3DwXa4jxZnQc+gnuTEqSWbBeoSZYTayXH9wXcw=="],
"@octokit/webhooks-methods": ["@octokit/webhooks-methods@6.0.0", "", {}, "sha512-MFlzzoDJVw/GcbfzVC1RLR36QqkTLUf79vLVO3D+xn7r0QgxnFoLZgtrzxiQErAjFUOdH6fas2KeQJ1yr/qaXQ=="],
"@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=="],
"@tinyhttp/content-disposition": ["@tinyhttp/content-disposition@2.2.2", "", {}, "sha512-crXw1txzrS36huQOyQGYFvhTeLeG0Si1xu+/l6kXUVYpE0TjFjEZRqTbuadQLfKGZ0jaI+jJoRyqaWwxOSHW2g=="],
"@types/aws-lambda": ["@types/aws-lambda@8.10.159", "", {}, "sha512-SAP22WSGNN12OQ8PlCzGzRCZ7QDCwI85dQZbmpz7+mAk+L7j+wI7qnvmdKh+o7A5LaOp6QnOZ2NJphAZQTTHQg=="],
"@types/bun": ["@types/bun@1.3.3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="],
"@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
@ -37,8 +142,28 @@
"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=="],
"aproba": ["aproba@2.1.0", "", {}, "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew=="],
"are-we-there-yet": ["are-we-there-yet@3.0.1", "", { "dependencies": { "delegates": "^1.0.0", "readable-stream": "^3.6.0" } }, "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg=="],
"async-retry": ["async-retry@1.3.3", "", { "dependencies": { "retry": "0.13.1" } }, "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw=="],
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
"axios": ["axios@1.13.2", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA=="],
"before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="],
"body-parser": ["body-parser@2.2.1", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw=="],
"bottleneck": ["bottleneck@2.19.5", "", {}, "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw=="],
"bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="],
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
@ -47,6 +172,34 @@
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
"chmodrp": ["chmodrp@1.0.2", "", {}, "sha512-TdngOlFV1FLTzU0o1w8MB6/BFywhtLC0SzRTGJU7T9lmdjlCWeMRt1iVo0Ki+ldwNk0BqNiKoc8xpLZEQ8mY1w=="],
"chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="],
"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@7.4.0", "", { "dependencies": { "axios": "^1.6.5", "debug": "^4", "fs-extra": "^11.2.0", "memory-stream": "^1.0.0", "node-api-headers": "^1.1.0", "npmlog": "^6.0.2", "rc": "^1.2.7", "semver": "^7.5.4", "tar": "^6.2.0", "url-join": "^4.0.1", "which": "^2.0.2", "yargs": "^17.7.2" }, "bin": { "cmake-js": "bin/cmake-js" } }, "sha512-Lw0JxEHrmk+qNj1n9W9d4IvkDdYTBn7l2BW6XmtLj7WPpIo2shvxUy+YokfjMxAAOELNonQwX3stkPhM5xSC2Q=="],
"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=="],
"color-support": ["color-support@1.1.3", "", { "bin": { "color-support": "bin.js" } }, "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg=="],
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
"commander": ["commander@10.0.1", "", {}, "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug=="],
"console-control-strings": ["console-control-strings@1.1.0", "", {}, "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ=="],
"content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="],
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
@ -61,24 +214,40 @@
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="],
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
"delegates": ["delegates@1.0.0", "", {}, "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ=="],
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"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=="],
"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=="],
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
"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=="],
@ -87,66 +256,144 @@
"express-rate-limit": ["express-rate-limit@7.5.1", "", { "peerDependencies": { "express": ">= 4.11" } }, "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw=="],
"fast-content-type-parse": ["fast-content-type-parse@3.0.0", "", {}, "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
"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=="],
"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=="],
"follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="],
"form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
"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=="],
"fs-minipass": ["fs-minipass@2.1.0", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"gauge": ["gauge@4.0.4", "", { "dependencies": { "aproba": "^1.0.3 || ^2.0.0", "color-support": "^1.1.3", "console-control-strings": "^1.1.0", "has-unicode": "^2.0.1", "signal-exit": "^3.0.7", "string-width": "^4.2.3", "strip-ansi": "^6.0.1", "wide-align": "^1.1.5" } }, "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg=="],
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
"get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="],
"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=="],
"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=="],
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
"has-unicode": ["has-unicode@2.0.1", "", {}, "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ=="],
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
"iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="],
"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=="],
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
"ipull": ["ipull@3.9.3", "", { "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-ZMkxaopfwKHwmEuGDYx7giNBdLxbHbRCWcQVA1D2eqE4crUguupfxej6s7UqbidYEwT69dkyumYkY8DPHIxF9g=="],
"is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="],
"is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="],
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="],
"isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="],
"jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="],
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"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.0.1", "", {}, "sha512-Qt/Jl5dsNIsyCAZsHB6x3mbwHFn0HJbdmvF49sVX/bHgX2cW7+G+U+I67Zw+TPM1Sr21Gb2nfJMd2g6iUcI1EQ=="],
"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=="],
"lowdb": ["lowdb@7.0.1", "", { "dependencies": { "steno": "^4.0.2" } }, "sha512-neJAj8GwF0e8EpycYIDFqEPcx9Qz4GUho20jWFR7YiFeXzF1YMLdxB36PypcTSPMA+4+LvgyMacYhlr18Zlymw=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
"memory-stream": ["memory-stream@1.0.0", "", { "dependencies": { "readable-stream": "^3.4.0" } }, "sha512-Wm13VcsPIMdG96dzILfij09PvuS3APtcKNh7M28FsCA/w6+1mjR7hhPmfFNoilX9xU7wTdhsH5lJAm6XNzdtww=="],
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
"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=="],
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
"minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="],
"minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="],
"mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="],
"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=="],
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
"node-addon-api": ["node-addon-api@8.5.0", "", {}, "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A=="],
"node-api-headers": ["node-api-headers@1.7.0", "", {}, "sha512-uJMGdkhVwu9+I3UsVvI3KW6ICAy/yDfsu5Br9rSnTtY3WpoaComXvKloiV5wtx0Md2rn0B9n29Ys2WMNwWxj9A=="],
"node-llama-cpp": ["node-llama-cpp@3.14.5", "", { "dependencies": { "@huggingface/jinja": "^0.5.3", "async-retry": "^1.3.3", "bytes": "^3.1.2", "chalk": "^5.4.1", "chmodrp": "^1.0.2", "cmake-js": "^7.4.0", "cross-spawn": "^7.0.6", "env-var": "^7.5.0", "filenamify": "^6.0.0", "fs-extra": "^11.3.0", "ignore": "^7.0.4", "ipull": "^3.9.2", "is-unicode-supported": "^2.1.0", "lifecycle-utils": "^3.0.1", "log-symbols": "^7.0.0", "nanoid": "^5.1.5", "node-addon-api": "^8.3.1", "octokit": "^5.0.3", "ora": "^8.2.0", "pretty-ms": "^9.2.0", "proper-lockfile": "^4.1.2", "semver": "^7.7.1", "simple-git": "^3.27.0", "slice-ansi": "^7.1.0", "stdout-update": "^4.0.1", "strip-ansi": "^7.1.0", "validate-npm-package-name": "^6.0.0", "which": "^5.0.0", "yargs": "^17.7.2" }, "optionalDependencies": { "@node-llama-cpp/linux-arm64": "3.14.5", "@node-llama-cpp/linux-armv7l": "3.14.5", "@node-llama-cpp/linux-x64": "3.14.5", "@node-llama-cpp/linux-x64-cuda": "3.14.5", "@node-llama-cpp/linux-x64-cuda-ext": "3.14.5", "@node-llama-cpp/linux-x64-vulkan": "3.14.5", "@node-llama-cpp/mac-arm64-metal": "3.14.5", "@node-llama-cpp/mac-x64": "3.14.5", "@node-llama-cpp/win-arm64": "3.14.5", "@node-llama-cpp/win-x64": "3.14.5", "@node-llama-cpp/win-x64-cuda": "3.14.5", "@node-llama-cpp/win-x64-cuda-ext": "3.14.5", "@node-llama-cpp/win-x64-vulkan": "3.14.5" }, "peerDependencies": { "typescript": ">=5.0.0" }, "optionalPeers": ["typescript"], "bin": { "node-llama-cpp": "dist/cli/cli.js", "nlc": "dist/cli/cli.js" } }, "sha512-Db+RFqFMJOOVWprUINq77LVe44FaiJ6JvNiq14r2+DZRgkgyxckSZa6DcZ5Xe5MC+hGA5aqOdnNxsrudUcs74Q=="],
"npmlog": ["npmlog@6.0.2", "", { "dependencies": { "are-we-there-yet": "^3.0.0", "console-control-strings": "^1.1.0", "gauge": "^4.0.3", "set-blocking": "^2.0.0" } }, "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
"octokit": ["octokit@5.0.5", "", { "dependencies": { "@octokit/app": "^16.1.2", "@octokit/core": "^7.0.6", "@octokit/oauth-app": "^8.0.3", "@octokit/plugin-paginate-graphql": "^6.0.0", "@octokit/plugin-paginate-rest": "^14.0.0", "@octokit/plugin-rest-endpoint-methods": "^17.0.0", "@octokit/plugin-retry": "^8.0.3", "@octokit/plugin-throttling": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "@octokit/webhooks": "^14.0.0" } }, "sha512-4+/OFSqOjoyULo7eN7EA97DE0Xydj/PW5aIckxqQIoFjFwqXKuFCvXUJObyJfBF9Khu4RL/jlDRI9FPaMGfPnw=="],
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
"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@8.2.0", "", { "dependencies": { "chalk": "^5.3.0", "cli-cursor": "^5.0.0", "cli-spinners": "^2.9.2", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.0.0", "log-symbols": "^6.0.0", "stdin-discarder": "^0.2.2", "string-width": "^7.2.0", "strip-ansi": "^7.1.0" } }, "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw=="],
"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=="],
@ -155,24 +402,48 @@
"pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="],
"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=="],
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
"qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="],
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
"raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
"rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="],
"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=="],
"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=="],
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
"send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="],
"serve-static": ["serve-static@2.2.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="],
"set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="],
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
@ -187,6 +458,14 @@
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
"signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
"simple-git": ["simple-git@3.30.0", "", { "dependencies": { "@kwsites/file-exists": "^1.1.1", "@kwsites/promise-deferred": "^1.1.1", "debug": "^4.4.0" } }, "sha512-q6lxyDsCmEal/MEGhP1aVyQ3oxnagGlBDOVSIB4XUVLl1iZh0Pah6ebC9V4xBap/RfgP2WlI8EKs0WS0rMEJHg=="],
"sleep-promise": ["sleep-promise@9.1.0", "", {}, "sha512-UHYzVpz9Xn8b+jikYSD6bqvf754xL2uBUzDFwiU6NcdZeifPr6UfgU43xpkPu67VMS88+TI2PSI7Eohgqf2fKA=="],
"slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="],
"sqlite-vec": ["sqlite-vec@0.1.7-alpha.2", "", { "optionalDependencies": { "sqlite-vec-darwin-arm64": "0.1.7-alpha.2", "sqlite-vec-darwin-x64": "0.1.7-alpha.2", "sqlite-vec-linux-arm64": "0.1.7-alpha.2", "sqlite-vec-linux-x64": "0.1.7-alpha.2", "sqlite-vec-windows-x64": "0.1.7-alpha.2" } }, "sha512-rNgRCv+4V4Ed3yc33Qr+nNmjhtrMnnHzXfLVPeGb28Dx5mmDL3Ngw/Wk8vhCGjj76+oC6gnkmMG8y73BZWGBwQ=="],
"sqlite-vec-darwin-arm64": ["sqlite-vec-darwin-arm64@0.1.7-alpha.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-raIATOqFYkeCHhb/t3r7W7Cf2lVYdf4J3ogJ6GFc8PQEgHCPEsi+bYnm2JT84MzLfTlSTIdxr4/NKv+zF7oLPw=="],
@ -201,6 +480,24 @@
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
"stdin-discarder": ["stdin-discarder@0.2.2", "", {}, "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ=="],
"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@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=="],
"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.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
"strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],
"tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="],
"toad-cache": ["toad-cache@3.7.0", "", {}, "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw=="],
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
"type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
@ -209,18 +506,126 @@
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"universal-github-app-jwt": ["universal-github-app-jwt@2.2.2", "", {}, "sha512-dcmbeSrOdTnsjGjUfAlqNDJrhxXizjAz94ija9Qw8YkZ1uu0d+GoZzyH+Jb9tIIqvGsadUfwg+22k5aDqqwzbw=="],
"universal-user-agent": ["universal-user-agent@7.0.3", "", {}, "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A=="],
"universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
"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@6.0.2", "", {}, "sha512-IUoow1YUtvoBBC06dXs8bR8B9vuA3aJfmQNKMoaPG/OFsPmoQvw8xh+6Ye25Gx9DQhoEom3Pcu9MKHerm/NpUQ=="],
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="],
"wide-align": ["wide-align@1.1.5", "", { "dependencies": { "string-width": "^1.0.2 || 2 || 3 || 4" } }, "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg=="],
"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@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="],
"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.1.13", "", {}, "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig=="],
"zod-to-json-schema": ["zod-to-json-schema@3.25.0", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ=="],
"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/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"gauge/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=="],
"gauge/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"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=="],
"minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"ora/log-symbols": ["log-symbols@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="],
"proper-lockfile/retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="],
"restore-cursor/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"wide-align/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/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=="],
"cmake-js/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
"gauge/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"gauge/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
"gauge/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"ipull/pretty-ms/parse-ms": ["parse-ms@3.0.0", "", {}, "sha512-Tpb8Z7r7XbbtBTrM9UhpkzzaMrqA2VXMT3YChzYltwV3P3pM6t8wl7TvpMnSTosz1aQAdVib7kdoys7vYOPerw=="],
"ora/log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="],
"wide-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"wide-align/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
"wide-align/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"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=="],
"wide-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
}
}

View File

@ -19,6 +19,7 @@
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.24.3",
"node-llama-cpp": "^3.14.5",
"sqlite-vec": "^0.1.7-alpha.2",
"yaml": "^2.8.2",
"zod": "^4.1.13"

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,34 @@
/**
* llm.ts - LLM abstraction layer for QMD
* llm.ts - LLM abstraction layer for QMD using node-llama-cpp
*
* Provides a clean interface for LLM operations with an Ollama implementation.
* All raw fetch calls to LLM APIs should go through this module.
* Provides embeddings, text generation, and reranking using local GGUF models.
*/
import { getLlama, resolveModelFile, type Llama, type LlamaModel, type LlamaEmbeddingContext, type LlamaContext, type LlamaChatSession } from "node-llama-cpp";
import { homedir } from "os";
import { join } from "path";
import { existsSync, mkdirSync } from "fs";
// =============================================================================
// Embedding Formatting Functions
// =============================================================================
/**
* Format a query for embedding.
* Uses nomic-style task prefix format for embeddinggemma.
*/
export function formatQueryForEmbedding(query: string): string {
return `task: search result | query: ${query}`;
}
/**
* Format a document for embedding.
* Uses nomic-style format with title and text fields.
*/
export function formatDocForEmbedding(text: string, title?: string): string {
return `title: ${title || "none"} | text: ${text}`;
}
// =============================================================================
// Types
// =============================================================================
@ -40,11 +64,8 @@ export type GenerateResult = {
*/
export type RerankDocumentResult = {
file: string;
relevant: boolean;
confidence: number;
score: number;
rawToken: string;
logprob: number;
index: number;
};
/**
@ -61,15 +82,14 @@ export type RerankResult = {
export type ModelInfo = {
name: string;
exists: boolean;
size?: number;
modifiedAt?: string;
path?: string;
};
/**
* Options for embedding
*/
export type EmbedOptions = {
model: string;
model?: string;
isQuery?: boolean;
title?: string;
};
@ -78,20 +98,25 @@ export type EmbedOptions = {
* Options for text generation
*/
export type GenerateOptions = {
model: string;
model?: string;
maxTokens?: number;
temperature?: number;
logprobs?: boolean;
raw?: boolean;
stop?: string[];
};
/**
* Options for reranking
*/
export type RerankOptions = {
model: string;
batchSize?: number;
model?: string;
};
/**
* Structured query expansion result
*/
export type ExpandedQuery = {
lexicalQuery: string | null; // Alternative query for BM25/keyword search
vectorQuery: string; // Alternative query for semantic search
hyde: string; // Hypothetical document that would answer the query
};
/**
@ -103,6 +128,19 @@ export type RerankDocument = {
title?: string;
};
// =============================================================================
// Model Configuration
// =============================================================================
// HuggingFace model URIs for node-llama-cpp
// Format: hf:<user>/<repo>/<file>
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";
// Local model cache directory
const MODEL_CACHE_DIR = join(homedir(), ".cache", "qmd", "models");
// =============================================================================
// LLM Interface
// =============================================================================
@ -114,266 +152,297 @@ export interface LLM {
/**
* Get embeddings for text
*/
embed(text: string, options: EmbedOptions): Promise<EmbeddingResult | null>;
embed(text: string, options?: EmbedOptions): Promise<EmbeddingResult | null>;
/**
* Generate text completion
*/
generate(prompt: string, options: GenerateOptions): Promise<GenerateResult | null>;
generate(prompt: string, options?: GenerateOptions): Promise<GenerateResult | null>;
/**
* Check if a model exists
* Check if a model exists/is available
*/
modelExists(model: string): Promise<ModelInfo>;
/**
* Pull a model (download if not available)
*/
pullModel(model: string, onProgress?: (progress: number) => void): Promise<boolean>;
// ==========================================================================
// High-level abstractions
// ==========================================================================
/**
* Expand a search query into multiple variations
*/
expandQuery(query: string, model: string, numVariations?: number): Promise<string[]>;
expandQuery(query: string, numVariations?: number): Promise<string[]>;
/**
* Rerank documents by relevance to a query
* Returns list of documents with relevance scores and boolean judgments
* Returns list of documents with relevance scores (higher = more relevant)
*/
rerank(query: string, documents: RerankDocument[], options: RerankOptions): Promise<RerankResult>;
rerank(query: string, documents: RerankDocument[], options?: RerankOptions): Promise<RerankResult>;
/**
* Quick relevance check - returns just boolean judgments with logprobs
* More efficient than full rerank when you just need yes/no
* Dispose of resources
*/
rerankerLogprobsCheck(query: string, documents: RerankDocument[], options: RerankOptions): Promise<RerankDocumentResult[]>;
dispose(): Promise<void>;
}
// =============================================================================
// Ollama Implementation
// node-llama-cpp Implementation
// =============================================================================
export type OllamaConfig = {
baseUrl?: string;
defaultEmbedModel?: string;
defaultGenerateModel?: string;
defaultRerankModel?: string;
export type LlamaCppConfig = {
embedModel?: string;
generateModel?: string;
rerankModel?: string;
modelCacheDir?: string;
};
const DEFAULT_OLLAMA_URL = "http://localhost:11434";
const DEFAULT_EMBED_MODEL = "embeddinggemma";
const DEFAULT_GENERATE_MODEL = "qwen3:0.6b";
const DEFAULT_RERANK_MODEL = "ExpedientFalcon/qwen3-reranker:0.6b-q8_0";
/**
* Format text for embedding query
* LLM implementation using node-llama-cpp
*/
export function formatQueryForEmbedding(query: string): string {
return `task: search result | query: ${query}`;
}
export class LlamaCpp implements LLM {
private llama: Llama | null = null;
private embedModel: LlamaModel | null = null;
private embedContext: LlamaEmbeddingContext | null = null;
private generateModel: LlamaModel | null = null;
private generateContext: LlamaContext | null = null;
private rerankModel: LlamaModel | null = null;
private rerankContext: Awaited<ReturnType<LlamaModel["createRankingContext"]>> | null = null;
/**
* Format text for embedding document
*/
export function formatDocForEmbedding(text: string, title?: string): string {
return `title: ${title || "none"} | text: ${text}`;
}
private embedModelUri: string;
private generateModelUri: string;
private rerankModelUri: string;
private modelCacheDir: string;
/**
* Ollama LLM implementation
*/
export class Ollama implements LLM {
private baseUrl: string;
private defaultEmbedModel: string;
private defaultGenerateModel: string;
private defaultRerankModel: string;
private initPromise: Promise<void> | null = null;
constructor(config: OllamaConfig = {}) {
this.baseUrl = config.baseUrl || process.env.OLLAMA_URL || DEFAULT_OLLAMA_URL;
this.defaultEmbedModel = config.defaultEmbedModel || DEFAULT_EMBED_MODEL;
this.defaultGenerateModel = config.defaultGenerateModel || DEFAULT_GENERATE_MODEL;
this.defaultRerankModel = config.defaultRerankModel || DEFAULT_RERANK_MODEL;
constructor(config: LlamaCppConfig = {}) {
this.embedModelUri = config.embedModel || DEFAULT_EMBED_MODEL;
this.generateModelUri = config.generateModel || DEFAULT_GENERATE_MODEL;
this.rerankModelUri = config.rerankModel || DEFAULT_RERANK_MODEL;
this.modelCacheDir = config.modelCacheDir || MODEL_CACHE_DIR;
}
/**
* Get the base URL for this Ollama instance
* Ensure model cache directory exists
*/
getBaseUrl(): string {
return this.baseUrl;
private ensureModelCacheDir(): void {
if (!existsSync(this.modelCacheDir)) {
mkdirSync(this.modelCacheDir, { recursive: true });
}
}
/**
* Initialize the llama instance (lazy)
*/
private async ensureLlama(): Promise<Llama> {
if (!this.llama) {
this.llama = await getLlama({ logLevel: "error" });
}
return this.llama;
}
/**
* Resolve a model URI to a local path, downloading if needed
*/
private async resolveModel(modelUri: string): Promise<string> {
this.ensureModelCacheDir();
// resolveModelFile handles HF URIs and downloads to the cache dir
return await resolveModelFile(modelUri, this.modelCacheDir);
}
/**
* Load embedding model and context (lazy)
*/
private async ensureEmbedContext(): Promise<LlamaEmbeddingContext> {
if (!this.embedContext) {
const llama = await this.ensureLlama();
const modelPath = await this.resolveModel(this.embedModelUri);
this.embedModel = await llama.loadModel({ modelPath });
this.embedContext = await this.embedModel.createEmbeddingContext();
}
return this.embedContext;
}
/**
* Load generation model and context (lazy)
*/
private async ensureGenerateContext(): Promise<LlamaContext> {
if (!this.generateContext) {
const llama = await this.ensureLlama();
const modelPath = await this.resolveModel(this.generateModelUri);
this.generateModel = await llama.loadModel({ modelPath });
// Create context with 4 sequences for parallel generation support
this.generateContext = await this.generateModel.createContext({ sequences: 4 });
}
return this.generateContext;
}
/**
* Load rerank model and context (lazy)
*/
private async ensureRerankContext(): Promise<Awaited<ReturnType<LlamaModel["createRankingContext"]>>> {
if (!this.rerankContext) {
const llama = await this.ensureLlama();
const modelPath = await this.resolveModel(this.rerankModelUri);
this.rerankModel = await llama.loadModel({ modelPath });
this.rerankContext = await this.rerankModel.createRankingContext();
}
return this.rerankContext;
}
// ==========================================================================
// Tokenization
// ==========================================================================
/**
* Tokenize text using the embedding model's tokenizer
* Returns array of token IDs
*/
async tokenize(text: string): Promise<number[]> {
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: string): Promise<number> {
const tokens = await this.tokenize(text);
return tokens.length;
}
/**
* Detokenize token IDs back to text
*/
async detokenize(tokens: number[]): Promise<string> {
await this.ensureEmbedContext();
if (!this.embedModel) {
throw new Error("Embed model not loaded");
}
return this.embedModel.detokenize(tokens);
}
// ==========================================================================
// Core API methods
// ==========================================================================
async embed(text: string, options: EmbedOptions): Promise<EmbeddingResult | null> {
const model = options.model || this.defaultEmbedModel;
const formatted = options.isQuery
? formatQueryForEmbedding(text)
: formatDocForEmbedding(text, options.title);
async embed(text: string, options: EmbedOptions = {}): Promise<EmbeddingResult | null> {
try {
const response = await fetch(`${this.baseUrl}/api/embed`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ model, input: formatted }),
});
if (!response.ok) {
return null;
}
const data = await response.json() as { embeddings?: number[][] };
if (!data.embeddings?.[0]) {
return null;
}
const context = await this.ensureEmbedContext();
const embedding = await context.getEmbeddingFor(text);
return {
embedding: data.embeddings[0],
model,
embedding: Array.from(embedding.vector),
model: this.embedModelUri,
};
} catch {
} catch (error) {
console.error("Embedding error:", error);
return null;
}
}
async generate(prompt: string, options: GenerateOptions): Promise<GenerateResult | null> {
const model = options.model || this.defaultGenerateModel;
/**
* Batch embed multiple texts efficiently
* Uses Promise.all for parallel embedding - node-llama-cpp handles batching internally
*/
async embedBatch(texts: string[]): Promise<(EmbeddingResult | null)[]> {
if (texts.length === 0) return [];
const requestBody: Record<string, unknown> = {
model,
prompt,
stream: false,
options: {
num_predict: options.maxTokens ?? 150,
temperature: options.temperature ?? 0,
},
try {
const context = await this.ensureEmbedContext();
// node-llama-cpp handles batching internally when we make parallel requests
const embeddings = await Promise.all(
texts.map(async (text) => {
try {
const embedding = await context.getEmbeddingFor(text);
return {
embedding: Array.from(embedding.vector),
model: this.embedModelUri,
};
} catch (err) {
console.error("Embedding error for text:", err);
return null;
}
})
);
return embeddings;
} catch (error) {
console.error("Batch embedding error:", error);
return texts.map(() => null);
}
}
async generate(prompt: string, options: GenerateOptions = {}): Promise<GenerateResult | null> {
try {
const context = await this.ensureGenerateContext();
const { LlamaChatSession } = await import("node-llama-cpp");
const session = new LlamaChatSession({
contextSequence: context.getSequence(),
});
const maxTokens = options.maxTokens ?? 150;
const temperature = options.temperature ?? 0;
let result = "";
try {
await session.prompt(prompt, {
maxTokens,
temperature,
onTextChunk: (text) => {
result += text;
},
});
} finally {
// Dispose session to release the sequence
await session.dispose();
}
return {
text: result,
model: this.generateModelUri,
done: true,
};
} catch (error) {
console.error("Generation error:", error);
return null;
}
}
async modelExists(modelUri: string): Promise<ModelInfo> {
// 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,
};
if (options.logprobs) {
requestBody.logprobs = true;
}
if (options.raw) {
requestBody.raw = true;
}
if (options.stop) {
(requestBody.options as Record<string, unknown>).stop = options.stop;
}
try {
const response = await fetch(`${this.baseUrl}/api/generate`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(requestBody),
});
if (!response.ok) {
return null;
}
const data = await response.json() as {
response?: string;
done?: boolean;
logprobs?: { tokens?: string[]; token_logprobs?: number[] };
};
// Parse logprobs if present
let logprobs: TokenLogProb[] | undefined;
if (data.logprobs?.tokens && data.logprobs?.token_logprobs) {
logprobs = data.logprobs.tokens.map((token, i) => ({
token,
logprob: data.logprobs!.token_logprobs![i],
}));
}
return {
text: data.response || "",
model,
logprobs,
done: data.done ?? true,
};
} catch {
return null;
}
}
async modelExists(model: string): Promise<ModelInfo> {
try {
const response = await fetch(`${this.baseUrl}/api/show`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: model }),
});
if (!response.ok) {
return { name: model, exists: false };
}
const data = await response.json() as {
size?: number;
modified_at?: string;
};
return {
name: model,
exists: true,
size: data.size,
modifiedAt: data.modified_at,
};
} catch {
return { name: model, exists: false };
}
}
async pullModel(model: string, onProgress?: (progress: number) => void): Promise<boolean> {
try {
const response = await fetch(`${this.baseUrl}/api/pull`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: model, stream: false }),
});
if (!response.ok) {
return false;
}
// For non-streaming, we just wait for completion
await response.json();
onProgress?.(100);
return true;
} catch {
return false;
}
}
// ==========================================================================
// High-level abstractions
// ==========================================================================
async expandQuery(query: string, model?: string, numVariations: number = 2): Promise<string[]> {
const useModel = model || this.defaultGenerateModel;
async expandQuery(query: string, numVariations: number = 2): Promise<string[]> {
const prompt = `You are a search query expander. Given a search query, generate ${numVariations} alternative queries that would help find relevant documents.
Rules:
- Use synonyms and related terminology (e.g., "craft" "craftsmanship", "quality", "excellence")
- Rephrase to capture different angles (e.g., "engineering culture" "technical excellence", "developer practices")
- Keep proper nouns and named concepts exactly as written (e.g., "Build a Business", "Stripe", "Shopify")
- Use synonyms and related terminology
- Rephrase to capture different angles
- Keep proper nouns exactly as written
- Each variation should be 3-8 words, natural search terms
- Do NOT just append words like "search" or "find" or "documents"
- Do NOT append words like "search" or "find"
Query: "${query}"
Output exactly ${numVariations} variations, one per line, no numbering or bullets:`;
const result = await this.generate(prompt, {
model: useModel,
maxTokens: 150,
temperature: 0,
});
@ -392,148 +461,226 @@ Output exactly ${numVariations} variations, one per line, no numbering or bullet
return [query, ...lines.slice(0, numVariations)];
}
/**
* Expand query using structured output with JSON schema grammar.
* Returns different query types optimized for different retrieval methods.
*
* @param query - Original search query
* @param includeLexical - Whether to include lexical query (false for vector-only search)
*/
async expandQueryStructured(query: string, includeLexical: boolean = true): Promise<ExpandedQuery> {
const llama = await this.ensureLlama();
const context = await this.ensureGenerateContext();
// Define JSON schema for structured output
const schema = {
type: "object" as const,
properties: {
lexicalQuery: {
type: "string" as const,
description: "Alternative keyword-based query using synonyms (3-6 words)"
},
vectorQuery: {
type: "string" as const,
description: "Semantically rephrased query capturing the intent (5-10 words)"
},
hyde: {
type: "string" as const,
description: "A hypothetical document snippet that would perfectly answer this query (50-100 words)"
}
},
required: ["vectorQuery", "hyde"] as const
};
const grammar = await llama.createGrammarForJsonSchema(schema);
const systemPrompt = includeLexical
? `You expand search queries into structured alternatives for a hybrid search system.
Given a query, generate:
1. lexicalQuery: Alternative keywords using synonyms (for BM25 keyword search)
2. vectorQuery: Semantically rephrased query (for vector/embedding search)
3. hyde: A hypothetical document excerpt that would answer the query (50-100 words)
Keep proper nouns exactly as written. Be concise.`
: `You expand search queries for semantic search.
Given a query, generate:
1. vectorQuery: Semantically rephrased query capturing the full intent
2. hyde: A hypothetical document excerpt that would answer the query (50-100 words)
Keep proper nouns exactly as written. Be concise. Set lexicalQuery to empty string.`;
const prompt = `Query: "${query}"
Generate the structured expansion:`;
const { LlamaChatSession } = await import("node-llama-cpp");
const session = new LlamaChatSession({
contextSequence: context.getSequence(),
systemPrompt,
});
try {
const result = await session.prompt(prompt, {
grammar,
maxTokens: 300,
temperature: 0,
});
const parsed = grammar.parse(result) as {
lexicalQuery?: string;
vectorQuery: string;
hyde: string;
};
return {
lexicalQuery: includeLexical && parsed.lexicalQuery ? parsed.lexicalQuery : null,
vectorQuery: parsed.vectorQuery || query,
hyde: parsed.hyde || "",
};
} catch (error) {
console.error("Structured query expansion failed:", error);
// Fallback to original query
return {
lexicalQuery: includeLexical ? query : null,
vectorQuery: query,
hyde: "",
};
} finally {
await session.dispose();
}
}
async rerank(
query: string,
documents: RerankDocument[],
options: RerankOptions
options: RerankOptions = {}
): Promise<RerankResult> {
const results = await this.rerankerLogprobsCheck(query, documents, options);
try {
const context = await this.ensureRerankContext();
return {
results: results.sort((a, b) => b.score - a.score),
model: options.model || this.defaultRerankModel,
};
}
// Build a map from document text to original indices (for lookup after sorting)
const textToDoc = new Map<string, { file: string; index: number }>();
documents.forEach((doc, index) => {
textToDoc.set(doc.text, { file: doc.file, index });
});
async rerankerLogprobsCheck(
query: string,
documents: RerankDocument[],
options: RerankOptions
): Promise<RerankDocumentResult[]> {
const model = options.model || this.defaultRerankModel;
const batchSize = options.batchSize || 5;
// Extract just the text for ranking
const texts = documents.map((doc) => doc.text);
const results: RerankDocumentResult[] = [];
// Use the proper ranking API - returns [{document: string, score: number}] sorted by score
const ranked = await context.rankAndSort(query, texts);
// Process in batches
for (let i = 0; i < documents.length; i += batchSize) {
const batch = documents.slice(i, i + batchSize);
const batchResults = await Promise.all(
batch.map((doc) => this.rerankSingle(query, doc, model))
);
results.push(...batchResults);
}
// Map back to our result format using the text-to-doc map
const results: RerankDocumentResult[] = ranked.map((item) => {
const docInfo = textToDoc.get(item.document)!;
return {
file: docInfo.file,
score: item.score,
index: docInfo.index,
};
});
return results;
}
/**
* Rerank a single document - internal helper
*/
private async rerankSingle(
query: string,
doc: RerankDocument,
model: string
): Promise<RerankDocumentResult> {
const systemPrompt = `Judge whether the Document meets the requirements based on the Query and the Instruct provided. Note that the answer can only be "yes" or "no".`;
const instruct = `Given a search query, determine if the following document is relevant to the query. Consider both direct matches and related concepts.`;
const docTitle = doc.title || doc.file.split("/").pop()?.replace(/\.md$/, "") || doc.file;
const docPreview = doc.text.length > 4000 ? doc.text.substring(0, 4000) + "..." : doc.text;
// Qwen3-reranker prompt format with empty think tags
const prompt = `<|im_start|>system
${systemPrompt}<|im_end|>
<|im_start|>user
<Instruct>: ${instruct}
<Query>: ${query}
<Document Title>: ${docTitle}
<Document>: ${docPreview}<|im_end|>
<|im_start|>assistant
<think>
</think>
`;
const result = await this.generate(prompt, {
model,
maxTokens: 1,
temperature: 0,
logprobs: true,
raw: true,
});
if (!result) {
return {
file: doc.file,
relevant: false,
confidence: 0,
score: 0,
rawToken: "",
logprob: 0,
results,
model: this.rerankModelUri,
};
} catch (error) {
console.error("Rerank error:", error);
// Return documents in original order with zero scores on error
return {
results: documents.map((doc, index) => ({
file: doc.file,
score: 0,
index,
})),
model: this.rerankModelUri,
};
}
return this.parseRerankResponse(doc.file, result);
}
/**
* Parse rerank response into structured result
*/
private parseRerankResponse(file: string, result: GenerateResult): RerankDocumentResult {
const token = result.text.toLowerCase().trim();
const logprob = result.logprobs?.[0]?.logprob ?? 0;
const confidence = Math.exp(logprob);
let relevant: boolean;
let score: number;
if (token.startsWith("yes")) {
relevant = true;
// Score: 0.5 base + up to 0.5 from confidence
score = 0.5 + 0.5 * confidence;
} else if (token.startsWith("no")) {
relevant = false;
// Score: up to 0.5 based on uncertainty (1 - confidence)
score = 0.5 * (1 - confidence);
} else {
// Unknown token - neutral score
relevant = false;
score = 0.3;
async dispose(): Promise<void> {
// Dispose contexts
if (this.embedContext) {
await this.embedContext.dispose();
this.embedContext = null;
}
if (this.generateContext) {
await this.generateContext.dispose();
this.generateContext = null;
}
if (this.rerankContext) {
await this.rerankContext.dispose();
this.rerankContext = null;
}
return {
file,
relevant,
confidence,
score,
rawToken: result.logprobs?.[0]?.token ?? token,
logprob,
};
// Dispose models
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;
}
// Dispose llama
if (this.llama) {
await this.llama.dispose();
this.llama = null;
}
}
}
// =============================================================================
// Singleton for default Ollama instance
// Singleton for default LlamaCpp instance
// =============================================================================
let defaultOllama: Ollama | null = null;
let defaultLlamaCpp: LlamaCpp | null = null;
/**
* Get the default Ollama instance (creates one if needed)
* Get the default LlamaCpp instance (creates one if needed)
*/
export function getDefaultOllama(): Ollama {
if (!defaultOllama) {
defaultOllama = new Ollama();
export function getDefaultLlamaCpp(): LlamaCpp {
if (!defaultLlamaCpp) {
defaultLlamaCpp = new LlamaCpp();
}
return defaultOllama;
return defaultLlamaCpp;
}
/**
* Set a custom default Ollama instance (useful for testing)
* Set a custom default LlamaCpp instance (useful for testing)
*/
export function setDefaultOllama(ollama: Ollama | null): void {
defaultOllama = ollama;
export function setDefaultLlamaCpp(llm: LlamaCpp | null): void {
defaultLlamaCpp = llm;
}
/**
* Dispose the default LlamaCpp instance if it exists.
* Call this before process exit to prevent NAPI crashes.
*/
export async function disposeDefaultLlamaCpp(): Promise<void> {
if (defaultLlamaCpp) {
await defaultLlamaCpp.dispose();
defaultLlamaCpp = null;
}
}
// =============================================================================
// Legacy exports for backwards compatibility
// =============================================================================
// Keep Ollama as an alias for now during transition
export { LlamaCpp as Ollama };
export type { LlamaCppConfig as OllamaConfig };
export function getDefaultOllama(): LlamaCpp {
return getDefaultLlamaCpp();
}
export function setDefaultOllama(llm: LlamaCpp | null): void {
setDefaultLlamaCpp(llm);
}

View File

@ -10,68 +10,13 @@ import { Database } from "bun:sqlite";
import * as sqliteVec from "sqlite-vec";
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { setDefaultOllama, Ollama } from "./llm";
import { setDefaultLlamaCpp, LlamaCpp } from "./llm";
import { mkdtemp, writeFile, readdir, unlink, rmdir } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import YAML from "yaml";
import type { CollectionConfig } from "./collections";
// =============================================================================
// Mock Ollama
// =============================================================================
const OLLAMA_URL = "http://localhost:11434";
const originalFetch = globalThis.fetch;
const mockOllamaResponses: Record<string, (body: unknown) => Response> = {
"/api/embed": () => {
const embedding = Array(768).fill(0).map(() => Math.random());
return new Response(JSON.stringify({ embeddings: [embedding] }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
},
"/api/generate": (body: unknown) => {
const reqBody = body as { prompt?: string; logprobs?: boolean };
if (reqBody.prompt?.includes("Judge") || reqBody.prompt?.includes("Document")) {
// Return format matching Ollama API
return new Response(JSON.stringify({
response: "yes",
done: true,
logprobs: reqBody.logprobs ? { tokens: ["yes"], token_logprobs: [-0.1] } : undefined
}), { status: 200, headers: { "Content-Type": "application/json" } });
} else {
return new Response(JSON.stringify({
response: "expanded query variation 1\nexpanded query variation 2",
done: true,
}), { status: 200, headers: { "Content-Type": "application/json" } });
}
},
"/api/show": () => {
return new Response(JSON.stringify({ size: 1000000 }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
},
};
function mockFetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
const url = typeof input === "string" ? input : input.toString();
if (url.startsWith(OLLAMA_URL)) {
const path = url.replace(OLLAMA_URL, "");
const handler = mockOllamaResponses[path];
if (handler) {
const body = init?.body ? JSON.parse(init.body as string) : {};
return Promise.resolve(handler(body));
}
throw new Error(`Unmocked Ollama endpoint: ${path}`);
}
throw new Error(`Unexpected fetch call to: ${url}`);
}
// =============================================================================
// Test Database Setup
// =============================================================================
@ -114,7 +59,7 @@ function initTestDatabase(db: Database): void {
db.exec(`CREATE INDEX IF NOT EXISTS idx_documents_hash ON documents(hash)`);
db.exec(`
CREATE TABLE IF NOT EXISTS ollama_cache (
CREATE TABLE IF NOT EXISTS llm_cache (
hash TEXT PRIMARY KEY,
result TEXT NOT NULL,
created_at TEXT NOT NULL
@ -151,7 +96,7 @@ function initTestDatabase(db: Database): void {
`);
// Create vector table
db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS vectors_vec USING vec0(hash_seq TEXT PRIMARY KEY, embedding float[768])`);
db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS vectors_vec USING vec0(hash_seq TEXT PRIMARY KEY, embedding float[768] distance_metric=cosine)`);
}
function seedTestData(db: Database): void {
@ -251,8 +196,8 @@ import type { RankedResult } from "./store";
describe("MCP Server", () => {
beforeAll(async () => {
globalThis.fetch = mockFetch as typeof fetch;
setDefaultOllama(new Ollama({ baseUrl: OLLAMA_URL }));
// LlamaCpp uses node-llama-cpp for local model inference (no HTTP mocking needed)
setDefaultLlamaCpp(new LlamaCpp());
// Set up test config directory
const configPrefix = join(tmpdir(), `qmd-mcp-config-${Date.now()}-${Math.random().toString(36).slice(2)}`);
@ -280,8 +225,7 @@ describe("MCP Server", () => {
});
afterAll(async () => {
globalThis.fetch = originalFetch;
setDefaultOllama(null);
setDefaultLlamaCpp(null);
testDb.close();
try {
require("fs").unlinkSync(testDbPath);
@ -373,9 +317,10 @@ describe("MCP Server", () => {
describe("qmd_query tool", () => {
test("expands query with variations", async () => {
const queries = await expandQuery("api documentation", DEFAULT_QUERY_MODEL, testDb);
expect(queries.length).toBeGreaterThan(1);
// Always returns at least the original query, may have more if generation succeeds
expect(queries.length).toBeGreaterThanOrEqual(1);
expect(queries[0]).toBe("api documentation");
});
}, 30000); // 30s timeout for model loading
test("performs RRF fusion on multiple result lists", () => {
const list1: RankedResult[] = [

View File

@ -35,6 +35,7 @@ import {
formatDocForEmbedding,
formatQueryForEmbedding,
chunkDocument,
chunkDocumentByTokens,
ensureVecTable,
clearCache,
getCacheKey,
@ -54,7 +55,7 @@ import {
deactivateDocument,
getActiveDocumentPaths,
cleanupOrphanedContent,
deleteOllamaCache,
deleteLLMCache,
deleteInactiveDocuments,
cleanupOrphanedVectors,
cleanupDuplicateCollections,
@ -62,13 +63,13 @@ import {
getCollectionsWithoutContext,
getTopLevelPathsWithoutContext,
handelize,
OLLAMA_URL,
DEFAULT_EMBED_MODEL,
DEFAULT_QUERY_MODEL,
DEFAULT_RERANK_MODEL,
DEFAULT_GLOB,
DEFAULT_MULTI_GET_MAX_BYTES,
} from "./store.js";
import { getDefaultLlamaCpp, disposeDefaultLlamaCpp, type RerankDocument, type ExpandedQuery } from "./llm.js";
import type { SearchResult, RankedResult } from "./store.js";
import {
formatSearchResults,
@ -86,9 +87,6 @@ import {
listAllContexts,
} from "./collections.js";
// Chunking: ~2000 tokens per chunk, ~3 bytes/token = 6KB
const CHUNK_BYTE_SIZE = 6 * 1024;
// Terminal colors (respects NO_COLOR env)
const useColor = !process.env.NO_COLOR && process.stdout.isTTY;
const c = {
@ -192,185 +190,26 @@ function computeDisplayPath(
return filepath;
}
// Auto-pull model if not found
async function ensureModelAvailable(model: string): Promise<void> {
try {
const response = await fetch(`${OLLAMA_URL}/api/show`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: model }),
});
if (response.ok) return;
} catch {
// Continue to pull attempt
}
// Rerank documents using node-llama-cpp cross-encoder model
async function rerank(query: string, documents: { file: string; text: string }[], _model: string = DEFAULT_RERANK_MODEL, _db?: Database): Promise<{ file: string; score: number }[]> {
if (documents.length === 0) return [];
console.log(`Model ${model} not found. Pulling...`);
progress.indeterminate();
const pullResponse = await fetch(`${OLLAMA_URL}/api/pull`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: model, stream: false }),
});
if (!pullResponse.ok) {
progress.error();
throw new Error(`Failed to pull model ${model}: ${pullResponse.status} - ${await pullResponse.text()}`);
}
progress.clear();
console.log(`Model ${model} pulled successfully.`);
}
async function getEmbedding(text: string, model: string, isQuery: boolean = false, title?: string, retried: boolean = false): Promise<number[]> {
const input = isQuery ? formatQueryForEmbedding(text) : formatDocForEmbedding(text, title);
const response = await fetch(`${OLLAMA_URL}/api/embed`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ model, input }),
});
if (!response.ok) {
const errorText = await response.text();
if (!retried && (errorText.includes("not found") || errorText.includes("does not exist"))) {
await ensureModelAvailable(model);
return getEmbedding(text, model, isQuery, title, true);
}
throw new Error(`Ollama API error: ${response.status} - ${errorText}`);
}
const data = await response.json() as { embeddings: number[][] };
return data.embeddings[0];
}
// Qwen3-Reranker prompt format (trained for yes/no relevance classification)
const RERANK_SYSTEM = `Judge whether the Document meets the requirements based on the Query and the Instruct provided. Note that the answer can only be "yes" or "no".`;
function formatRerankPrompt(query: string, title: string, doc: string): string {
return `<Instruct>: Determine if this document from a Shopify knowledge base is relevant to the search query. The query may reference specific Shopify programs, competitions, features, or named concepts (e.g., "Build a Business" competition, "Shop Pay", "Polaris"). Match documents that discuss the queried topic, even if phrasing differs.
<Query>: ${query}
<Document Title>: ${title}
<Document>: ${doc}`;
}
type LogProb = { token: string; logprob: number };
type RerankResponse = {
response: string;
logprobs?: LogProb[];
};
function parseRerankResponse(data: RerankResponse): number {
if (!data.logprobs || data.logprobs.length === 0) {
throw new Error("Reranker response missing logprobs");
}
const firstToken = data.logprobs[0];
const token = firstToken.token.toLowerCase().trim();
const confidence = Math.exp(firstToken.logprob);
if (token === "yes") {
return confidence;
}
if (token === "no") {
return (1 - confidence) * 0.3;
}
throw new Error(`Unexpected reranker token: "${token}"`);
}
async function rerankSingle(prompt: string, model: string, db?: Database, retried: boolean = false): Promise<number> {
// Use generate with raw template for qwen3-reranker format
// Include empty <think> tags as per HuggingFace reference implementation
const fullPrompt = `<|im_start|>system
${RERANK_SYSTEM}<|im_end|>
<|im_start|>user
${prompt}<|im_end|>
<|im_start|>assistant
<think>
</think>
`;
const requestBody = {
model,
prompt: fullPrompt,
raw: true,
stream: false,
logprobs: true,
options: { num_predict: 1 },
};
// Check cache
const cacheKey = db ? getCacheKey(`${OLLAMA_URL}/api/generate`, requestBody) : "";
if (db) {
const cached = getCachedResult(db, cacheKey);
if (cached) {
const data = JSON.parse(cached) as RerankResponse;
return parseRerankResponse(data);
}
}
const response = await fetch(`${OLLAMA_URL}/api/generate`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(requestBody),
});
if (!response.ok) {
const errorText = await response.text();
if (!retried && (errorText.includes("not found") || errorText.includes("does not exist"))) {
await ensureModelAvailable(model);
return rerankSingle(prompt, model, db, true);
}
throw new Error(`Ollama API error: ${response.status} - ${errorText}`);
}
const data = await response.json() as RerankResponse;
// Cache the result
if (db) {
setCachedResult(db, cacheKey, JSON.stringify(data));
}
return parseRerankResponse(data);
}
async function rerank(query: string, documents: { file: string; text: string }[], model: string = DEFAULT_RERANK_MODEL, db?: Database): Promise<{ file: string; score: number }[]> {
const results: { file: string; score: number }[] = [];
const total = documents.length;
const PARALLEL = 5;
process.stderr.write(`Reranking ${total} documents with ${model} (parallel: ${PARALLEL})...\n`);
process.stderr.write(`Reranking ${total} documents...\n`);
progress.indeterminate();
// Process in parallel batches
for (let i = 0; i < documents.length; i += PARALLEL) {
const batch = documents.slice(i, i + PARALLEL);
const batchResults = await Promise.all(
batch.map(async (doc) => {
try {
// Extract title from filename for reranker context
const title = doc.file.split('/').pop()?.replace(/\.md$/, '') || doc.file;
const prompt = formatRerankPrompt(query, title, doc.text.slice(0, 4000));
const score = await rerankSingle(prompt, model, db);
return { file: doc.file, score };
} catch (err) {
return { file: doc.file, score: 0 };
}
})
);
results.push(...batchResults);
const llm = getDefaultLlamaCpp();
const rerankDocs: RerankDocument[] = documents.map((doc) => ({
file: doc.file,
text: doc.text.slice(0, 4000), // Truncate to context limit
}));
const processed = Math.min(i + PARALLEL, total);
progress.set((processed / total) * 100);
process.stderr.write(`\rReranking: ${processed}/${total}`);
}
const result = await llm.rerank(query, rerankDocs);
progress.clear();
process.stderr.write("\n");
return results.sort((a, b) => b.score - a.score);
return result.results.map((r) => ({ file: r.file, score: r.score }));
}
function formatTimeAgo(date: Date): string {
@ -1593,10 +1432,12 @@ async function vectorIndex(model: string = DEFAULT_EMBED_MODEL, force: boolean =
}
// Prepare documents with chunks
type ChunkItem = { hash: string; title: string; text: string; seq: number; pos: number; bytes: number; displayName: string };
type ChunkItem = { hash: string; title: string; text: string; seq: number; pos: number; tokens: number; bytes: number; displayName: string };
const allChunks: ChunkItem[] = [];
let multiChunkDocs = 0;
// Chunk all documents using actual token counts
process.stderr.write(`Chunking ${hashesToEmbed.length} documents by token count...\n`);
for (const item of hashesToEmbed) {
const encoder = new TextEncoder();
const bodyBytes = encoder.encode(item.body).length;
@ -1604,7 +1445,7 @@ async function vectorIndex(model: string = DEFAULT_EMBED_MODEL, force: boolean =
const title = extractTitle(item.body, item.path);
const displayName = item.path;
const chunks = chunkDocument(item.body, CHUNK_BYTE_SIZE);
const chunks = await chunkDocumentByTokens(item.body); // Uses actual tokenizer
if (chunks.length > 1) multiChunkDocs++;
@ -1615,6 +1456,7 @@ async function vectorIndex(model: string = DEFAULT_EMBED_MODEL, force: boolean =
text: chunks[seq].text,
seq,
pos: chunks[seq].pos,
tokens: chunks[seq].tokens,
bytes: encoder.encode(chunks[seq].text).length,
displayName,
});
@ -1642,29 +1484,64 @@ async function vectorIndex(model: string = DEFAULT_EMBED_MODEL, force: boolean =
// Get embedding dimensions from first chunk
progress.indeterminate();
const firstEmbedding = await getEmbedding(allChunks[0].text, model, false, allChunks[0].title);
ensureVecTable(db, firstEmbedding.length);
const llm = getDefaultLlamaCpp();
const firstText = formatDocForEmbedding(allChunks[0].text, allChunks[0].title);
const firstResult = await llm.embed(firstText);
if (!firstResult) {
throw new Error("Failed to get embedding dimensions from first chunk");
}
ensureVecTable(db, firstResult.embedding.length);
let chunksEmbedded = 0, errors = 0, bytesProcessed = 0;
const startTime = Date.now();
// Insert first chunk
insertEmbedding(db, allChunks[0].hash, allChunks[0].seq, allChunks[0].pos, new Float32Array(firstEmbedding), model, now);
chunksEmbedded++;
bytesProcessed += allChunks[0].bytes;
// Batch embedding for better throughput
// Process in batches of 32 to balance memory usage and efficiency
const BATCH_SIZE = 32;
for (let batchStart = 0; batchStart < allChunks.length; batchStart += BATCH_SIZE) {
const batchEnd = Math.min(batchStart + BATCH_SIZE, allChunks.length);
const batch = allChunks.slice(batchStart, batchEnd);
// Format texts for embedding
const texts = batch.map(chunk => formatDocForEmbedding(chunk.text, chunk.title));
for (let i = 1; i < allChunks.length; i++) {
const chunk = allChunks[i];
try {
const embedding = await getEmbedding(chunk.text, model, false, chunk.title);
insertEmbedding(db, chunk.hash, chunk.seq, chunk.pos, new Float32Array(embedding), model, now);
chunksEmbedded++;
bytesProcessed += chunk.bytes;
// Batch embed all texts at once
const embeddings = await llm.embedBatch(texts);
// Insert each embedding
for (let i = 0; i < batch.length; i++) {
const chunk = batch[i];
const embedding = embeddings[i];
if (embedding) {
insertEmbedding(db, chunk.hash, chunk.seq, chunk.pos, new Float32Array(embedding.embedding), model, now);
chunksEmbedded++;
} else {
errors++;
console.error(`\n${c.yellow}⚠ Error embedding "${chunk.displayName}" chunk ${chunk.seq}${c.reset}`);
}
bytesProcessed += chunk.bytes;
}
} catch (err) {
errors++;
bytesProcessed += chunk.bytes;
progress.error();
console.error(`\n${c.yellow}⚠ Error embedding "${chunk.displayName}" chunk ${chunk.seq}: ${err}${c.reset}`);
// If batch fails, try individual embeddings as fallback
for (const chunk of batch) {
try {
const text = formatDocForEmbedding(chunk.text, chunk.title);
const result = await llm.embed(text);
if (result) {
insertEmbedding(db, chunk.hash, chunk.seq, chunk.pos, new Float32Array(result.embedding), model, now);
chunksEmbedded++;
} else {
errors++;
}
} catch (innerErr) {
errors++;
console.error(`\n${c.yellow}⚠ Error embedding "${chunk.displayName}" chunk ${chunk.seq}: ${innerErr}${c.reset}`);
}
bytesProcessed += chunk.bytes;
}
}
const percent = (bytesProcessed / totalBytes) * 100;
@ -2046,17 +1923,25 @@ async function vectorSearch(query: string, opts: OutputOptions, model: string =
// Check index health and warn about issues
checkIndexHealth(db);
// Expand query to multiple variations (with caching)
const queries = await expandQuery(query, DEFAULT_QUERY_MODEL, db);
process.stderr.write(`Searching with ${queries.length} query variations...\n`);
// Expand query using structured output (no lexical for vector-only search)
const expanded = await expandQueryStructured(query, false);
// Build list of queries for vector search: original, vectorQuery, and hyde
const vectorQueries: string[] = [query];
if (expanded.vectorQuery && expanded.vectorQuery !== query) {
vectorQueries.push(expanded.vectorQuery);
}
if (expanded.hyde && expanded.hyde.length > 20) {
vectorQueries.push(expanded.hyde);
}
process.stderr.write(`${c.dim}Searching ${vectorQueries.length} vector queries...${c.reset}\n`);
// Collect results from all query variations
// For --all, fetch more results per query
const perQueryLimit = opts.all ? 500 : 20;
const allResults = new Map<string, { file: string; displayPath: string; title: string; body: string; score: number; hash: string }>();
for (const q of queries) {
// searchVec accepts collection name as number parameter for legacy reasons (will be fixed in store.ts)
for (const q of vectorQueries) {
const vecResults = await searchVec(db, q, model, perQueryLimit, collectionName as any);
for (const r of vecResults) {
const existing = allResults.get(r.filepath);
@ -2081,71 +1966,51 @@ async function vectorSearch(query: string, opts: OutputOptions, model: string =
outputResults(results, query, { ...opts, limit: results.length }); // Already limited
}
async function expandQuery(query: string, model: string = DEFAULT_QUERY_MODEL, db?: Database): Promise<string[]> {
process.stderr.write("Generating query variations...\n");
// Expand query using structured output with JSON schema grammar
async function expandQueryStructured(query: string, includeLexical: boolean = true): Promise<ExpandedQuery> {
process.stderr.write(`${c.dim}Expanding query...${c.reset}\n`);
const prompt = `You are a search query expander. Given a search query, generate 2 alternative queries that would help find relevant documents.
const llm = getDefaultLlamaCpp();
const expanded = await llm.expandQueryStructured(query, includeLexical);
Rules:
- Use synonyms and related terminology (e.g., "craft" "craftsmanship", "quality", "excellence")
- Rephrase to capture different angles (e.g., "engineering culture" "technical excellence", "developer practices")
- Keep proper nouns and named concepts exactly as written (e.g., "Build a Business", "Stripe", "Shopify")
- Each variation should be 3-8 words, natural search terms
- Do NOT just append words like "search" or "find" or "documents"
// Log the expansion as a tree, starting with original query
const lines: string[] = [];
const bothLabel = includeLexical ? ' · (lexical+vector)' : ' · (vector)';
lines.push(`${c.dim}├─ ${query}${bothLabel}${c.reset}`);
Query: "${query}"
Output exactly 2 variations, one per line, no numbering or bullets:`;
const requestBody = {
model,
prompt,
stream: false,
think: false,
options: { num_predict: 150 },
};
// Check cache
const cacheDb = db || getDb();
const cacheKey = getCacheKey(`${OLLAMA_URL}/api/generate`, requestBody);
const cached = getCachedResult(cacheDb, cacheKey);
let responseText: string;
if (cached) {
responseText = cached;
} else {
const response = await fetch(`${OLLAMA_URL}/api/generate`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(requestBody),
});
if (!response.ok) {
const errorText = await response.text();
if (errorText.includes("not found") || errorText.includes("does not exist")) {
await ensureModelAvailable(model);
if (!db) cacheDb.close();
return expandQuery(query, model, db);
}
if (!db) cacheDb.close();
return [query];
}
const data = await response.json() as { response: string };
responseText = data.response;
setCachedResult(cacheDb, cacheKey, responseText);
if (expanded.lexicalQuery && expanded.lexicalQuery !== query) {
lines.push(`${c.dim}├─ ${expanded.lexicalQuery} · (lexical)${c.reset}`);
}
if (expanded.vectorQuery && expanded.vectorQuery !== query) {
lines.push(`${c.dim}├─ ${expanded.vectorQuery} · (vector)${c.reset}`);
}
if (expanded.hyde && expanded.hyde.length > 20) {
// Truncate hyde to first ~60 chars for display
const hydePreview = expanded.hyde.length > 60
? expanded.hyde.substring(0, 60).replace(/\n/g, ' ') + '...'
: expanded.hyde.replace(/\n/g, ' ');
lines.push(`${c.dim}├─ ${hydePreview} · (vector)${c.reset}`);
}
if (!db) cacheDb.close();
// Fix last item to use └─ instead of ├─
if (lines.length > 0) {
lines[lines.length - 1] = lines[lines.length - 1].replace('├─', '└─');
}
const lines = responseText.trim().split('\n')
.map(l => l.replace(/^[\d\.\-\*\"\s]+/, '').replace(/["\s]+$/, '').trim())
.filter(l => l.length > 2 && l.length < 100 && !l.startsWith('<') && !l.toLowerCase().includes('variation'))
.slice(0, 2);
for (const line of lines) {
process.stderr.write(line + '\n');
}
const allQueries = [query, ...lines];
process.stderr.write(`${c.dim}Queries: ${allQueries.join(' | ')}${c.reset}\n`);
return allQueries;
return expanded;
}
// Legacy wrapper for backward compatibility
async function expandQuery(query: string, _model: string = DEFAULT_QUERY_MODEL, _db?: Database): Promise<string[]> {
const expanded = await expandQueryStructured(query, true);
const queries = [query];
if (expanded.lexicalQuery && expanded.lexicalQuery !== query) queries.push(expanded.lexicalQuery);
if (expanded.vectorQuery && expanded.vectorQuery !== query) queries.push(expanded.vectorQuery);
return queries;
}
async function querySearch(query: string, opts: OutputOptions, embedModel: string = DEFAULT_EMBED_MODEL, rerankModel: string = DEFAULT_RERANK_MODEL): Promise<void> {
@ -2166,9 +2031,24 @@ async function querySearch(query: string, opts: OutputOptions, embedModel: strin
// Check index health and warn about issues
checkIndexHealth(db);
// Expand query to multiple variations (with caching)
const queries = await expandQuery(query, DEFAULT_QUERY_MODEL, db);
process.stderr.write(`Searching with ${queries.length} query variations...\n`);
// Expand query using structured output
const expanded = await expandQueryStructured(query, true);
// Build query lists for each retrieval type
const ftsQueries: string[] = [query];
if (expanded.lexicalQuery && expanded.lexicalQuery !== query) {
ftsQueries.push(expanded.lexicalQuery);
}
const vectorQueries: string[] = [query];
if (expanded.vectorQuery && expanded.vectorQuery !== query) {
vectorQueries.push(expanded.vectorQuery);
}
if (expanded.hyde && expanded.hyde.length > 20) {
vectorQueries.push(expanded.hyde);
}
process.stderr.write(`${c.dim}Searching ${ftsQueries.length} lexical + ${vectorQueries.length} vector queries...${c.reset}\n`);
// Collect ranked result lists for RRF fusion
const rankedLists: RankedResult[][] = [];
@ -2177,18 +2057,18 @@ async function querySearch(query: string, opts: OutputOptions, embedModel: strin
// Map to store hash by filepath for final results
const hashMap = new Map<string, string>();
for (const q of queries) {
// FTS search - get ranked results
// searchFTS accepts collection name as number parameter for legacy reasons (will be fixed in store.ts)
// FTS searches with lexical queries
for (const q of ftsQueries) {
const ftsResults = searchFTS(db, q, 20, collectionName as any);
if (ftsResults.length > 0) {
for (const r of ftsResults) hashMap.set(r.filepath, r.hash);
rankedLists.push(ftsResults.map(r => ({ file: r.filepath, displayPath: r.displayPath, title: r.title, body: r.body || "", score: r.score })));
}
}
// Vector search - get ranked results
if (hasVectors) {
// searchVec accepts collection name as number parameter for legacy reasons (will be fixed in store.ts)
// Vector searches with semantic queries + hyde
if (hasVectors) {
for (const q of vectorQueries) {
const vecResults = await searchVec(db, q, embedModel, 20, collectionName as any);
if (vecResults.length > 0) {
for (const r of vecResults) hashMap.set(r.filepath, r.hash);
@ -2209,10 +2089,39 @@ async function querySearch(query: string, opts: OutputOptions, embedModel: strin
return;
}
// Rerank with the original query (with caching)
// Rerank chunks, not full documents
// For each candidate, extract the most relevant chunk to rerank
const chunksToRerank: { file: string; text: string; chunkIdx: number }[] = [];
const docChunkMap = new Map<string, { chunks: { text: string; pos: number }[]; bestChunkIdx: number }>();
for (const c of candidates) {
const chunks = chunkDocument(c.body);
if (chunks.length === 1) {
// Small document - use entire body
chunksToRerank.push({ file: c.file, text: chunks[0].text, chunkIdx: 0 });
docChunkMap.set(c.file, { chunks, bestChunkIdx: 0 });
} else {
// Find the chunk that best matches the query terms (simple keyword heuristic)
const queryTerms = query.toLowerCase().split(/\s+/).filter(t => t.length > 2);
let bestIdx = 0;
let bestScore = 0;
for (let i = 0; i < chunks.length; i++) {
const chunkLower = chunks[i].text.toLowerCase();
const score = queryTerms.reduce((acc, term) => acc + (chunkLower.includes(term) ? 1 : 0), 0);
if (score > bestScore) {
bestScore = score;
bestIdx = i;
}
}
chunksToRerank.push({ file: c.file, text: chunks[bestIdx].text, chunkIdx: bestIdx });
docChunkMap.set(c.file, { chunks, bestChunkIdx: bestIdx });
}
}
// Rerank the focused chunks (with caching)
const reranked = await rerank(
query,
candidates.map(c => ({ file: c.file, text: c.body })),
chunksToRerank.map(c => ({ file: c.file, text: c.text })),
rerankModel,
db
);
@ -2239,11 +2148,16 @@ async function querySearch(query: string, opts: OutputOptions, embedModel: strin
const rrfScore = 1 / rrfRank; // Position-based: 1, 0.5, 0.33...
const blendedScore = rrfWeight * rrfScore + (1 - rrfWeight) * r.score;
const candidate = candidateMap.get(r.file);
// Use the best chunk's text for the body (better for snippets)
const chunkInfo = docChunkMap.get(r.file);
const chunkBody = chunkInfo ? chunkInfo.chunks[chunkInfo.bestChunkIdx].text : candidate?.body || "";
const chunkPos = chunkInfo ? chunkInfo.chunks[chunkInfo.bestChunkIdx].pos : 0;
return {
file: r.file,
displayPath: candidate?.displayPath || "",
title: candidate?.title || "",
body: candidate?.body || "",
body: chunkBody,
chunkPos,
score: blendedScore,
context: getContextForFile(db, r.file),
hash: hashMap.get(r.file) || "",
@ -2341,7 +2255,7 @@ function showHelp(): void {
console.log(" qmd multi-get <pattern> [-l N] [--max-bytes N] - Get multiple docs by glob or comma-separated list");
console.log(" qmd status - Show index status and collections");
console.log(" qmd update [--pull] - Re-index all collections (--pull: git pull first)");
console.log(" qmd embed [-f] - Create vector embeddings (chunks ~6KB each)");
console.log(" qmd embed [-f] - Create vector embeddings (800 tokens/chunk, 15% overlap)");
console.log(" qmd cleanup - Remove cache and orphaned data, vacuum DB");
console.log(" qmd search <query> - Full-text search (BM25)");
console.log(" qmd vsearch <query> - Vector similarity search");
@ -2369,12 +2283,10 @@ function showHelp(): void {
console.log(" --max-bytes <num> - Skip files larger than N bytes (default: 10240)");
console.log(" --json/--csv/--md/--xml/--files - Output format (same as search)");
console.log("");
console.log("Environment:");
console.log(" OLLAMA_URL - Ollama server URL (default: http://localhost:11434)");
console.log("");
console.log("Models:");
console.log(` Embedding: ${DEFAULT_EMBED_MODEL}`);
console.log(` Reranking: ${DEFAULT_RERANK_MODEL}`);
console.log("Models (auto-downloaded from HuggingFace):");
console.log(" Embedding: embeddinggemma-300M-Q8_0");
console.log(" Reranking: qwen3-reranker-0.6b-q8_0");
console.log(" Generation: Qwen3-0.6B-Q8_0");
console.log("");
console.log(`Index: ${getDbPath()}`);
}
@ -2617,8 +2529,8 @@ switch (cli.command) {
case "cleanup": {
const db = getDb();
// 1. Clear ollama_cache
const cacheCount = deleteOllamaCache(db);
// 1. Clear llm_cache
const cacheCount = deleteLLMCache(db);
console.log(`${c.green}${c.reset} Cleared ${cacheCount} cached API responses`);
// 2. Remove orphaned vectors
@ -2648,4 +2560,8 @@ switch (cli.command) {
console.error("Run 'qmd --help' for usage.");
process.exit(1);
}
// Cleanup LlamaCpp instance to prevent NAPI crash on exit
await disposeDefaultLlamaCpp();
} // end if (import.meta.main)

View File

@ -3,7 +3,7 @@
*
* Run with: bun test store.test.ts
*
* Ollama is mocked - tests will fail if any real Ollama calls are made.
* LLM operations use LlamaCpp with local GGUF models (node-llama-cpp).
*/
import { describe, test, expect, beforeAll, afterAll, beforeEach, afterEach, mock, spyOn } from "bun:test";
@ -24,6 +24,7 @@ import {
formatQueryForEmbedding,
formatDocForEmbedding,
chunkDocument,
chunkDocumentByTokens,
reciprocalRankFusion,
extractSnippet,
getCacheKey,
@ -31,7 +32,6 @@ import {
normalizeVirtualPath,
isVirtualPath,
parseVirtualPath,
OLLAMA_URL,
type Store,
type DocumentResult,
type SearchResult,
@ -40,91 +40,11 @@ import {
import type { CollectionConfig } from "./collections.js";
// =============================================================================
// Ollama Mocking
// LlamaCpp Setup
// =============================================================================
// Track original fetch
const originalFetch = globalThis.fetch;
// Mock responses for different Ollama endpoints
const mockOllamaResponses: Record<string, (body: unknown) => Response> = {
"/api/embed": (body: unknown) => {
// Return mock embeddings (768 dimensions)
const embedding = Array(768).fill(0).map(() => Math.random());
return new Response(JSON.stringify({ embeddings: [embedding] }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
},
"/api/generate": (body: unknown) => {
const reqBody = body as { prompt?: string };
// Check if this is a rerank request or query expansion
if (reqBody.prompt?.includes("yes") || reqBody.prompt?.includes("no") || reqBody.prompt?.includes("Judge")) {
// Rerank response
return new Response(JSON.stringify({
response: "yes",
logprobs: [{ token: "yes", logprob: -0.1 }],
}), {
status: 200,
headers: { "Content-Type": "application/json" },
});
} else {
// Query expansion response
return new Response(JSON.stringify({
response: "expanded query variation 1\nexpanded query variation 2",
}), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
},
"/api/show": () => {
// Model exists
return new Response(JSON.stringify({ modelfile: "exists" }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
},
};
// Install mock fetch that intercepts Ollama calls
function installOllamaMock(): void {
globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
// Check if this is an Ollama URL
if (url.startsWith(OLLAMA_URL)) {
const path = url.replace(OLLAMA_URL, "");
const mockHandler = mockOllamaResponses[path];
if (mockHandler) {
const body = init?.body ? JSON.parse(init.body as string) : {};
return mockHandler(body);
}
// Unknown Ollama endpoint - fail the test
throw new Error(`TEST ERROR: Unmocked Ollama endpoint called: ${path}`);
}
// Non-Ollama URLs fail (we shouldn't be making other network calls in tests)
throw new Error(`TEST ERROR: Unexpected network call to: ${url}`);
};
}
// Restore original fetch
function restoreOllamaMock(): void {
globalThis.fetch = originalFetch;
}
// Install mock before all tests
beforeAll(() => {
installOllamaMock();
});
// Restore after all tests
afterAll(() => {
restoreOllamaMock();
});
// Note: LlamaCpp uses node-llama-cpp for local GGUF model inference.
// No HTTP mocking needed - tests use real LlamaCpp calls for integration tests.
// =============================================================================
// Test Utilities
@ -483,7 +403,7 @@ describe("Store Creation", () => {
expect(tableNames).toContain("documents");
expect(tableNames).toContain("documents_fts");
expect(tableNames).toContain("content_vectors");
expect(tableNames).toContain("ollama_cache");
expect(tableNames).toContain("llm_cache");
// Note: path_contexts table removed in favor of YAML-based context storage
await cleanupTestDb(store);
@ -580,7 +500,7 @@ describe("Embedding Formatting", () => {
describe("Document Chunking", () => {
test("chunkDocument returns single chunk for small documents", () => {
const content = "Small document content";
const chunks = chunkDocument(content, 1000);
const chunks = chunkDocument(content, 1000, 0);
expect(chunks).toHaveLength(1);
expect(chunks[0].text).toBe(content);
expect(chunks[0].pos).toBe(0);
@ -588,7 +508,7 @@ describe("Document Chunking", () => {
test("chunkDocument splits large documents", () => {
const content = "A".repeat(10000);
const chunks = chunkDocument(content, 1000);
const chunks = chunkDocument(content, 1000, 0);
expect(chunks.length).toBeGreaterThan(1);
// All chunks should have correct positions
@ -600,9 +520,26 @@ describe("Document Chunking", () => {
}
});
test("chunkDocument with overlap creates overlapping chunks", () => {
const content = "A".repeat(3000);
const chunks = chunkDocument(content, 1000, 150); // 15% overlap
expect(chunks.length).toBeGreaterThan(1);
// With overlap, positions should be closer together than without
// Each new chunk starts 150 chars before where the previous one ended
for (let i = 1; i < chunks.length; i++) {
const prevEnd = chunks[i - 1].pos + chunks[i - 1].text.length;
const currentStart = chunks[i].pos;
// Current chunk should start before the previous chunk ended (overlap)
expect(currentStart).toBeLessThan(prevEnd);
// But should still make forward progress
expect(currentStart).toBeGreaterThan(chunks[i - 1].pos);
}
});
test("chunkDocument prefers paragraph breaks", () => {
const content = "First paragraph.\n\nSecond paragraph.\n\nThird paragraph.".repeat(50);
const chunks = chunkDocument(content, 500);
const chunks = chunkDocument(content, 500, 0);
// Chunks should end at paragraph breaks when possible
for (const chunk of chunks.slice(0, -1)) {
@ -617,13 +554,82 @@ describe("Document Chunking", () => {
test("chunkDocument handles UTF-8 characters correctly", () => {
const content = "こんにちは世界".repeat(500); // Japanese text
const chunks = chunkDocument(content, 1000);
const chunks = chunkDocument(content, 1000, 0);
// Should not split in the middle of a multi-byte character
for (const chunk of chunks) {
expect(() => new TextEncoder().encode(chunk.text)).not.toThrow();
}
});
test("chunkDocument with default params uses 800-token chunks", () => {
// Default is CHUNK_SIZE_CHARS (3200 chars) with CHUNK_OVERLAP_CHARS (480 chars)
const content = "Word ".repeat(2000); // ~10000 chars
const chunks = chunkDocument(content);
expect(chunks.length).toBeGreaterThan(1);
// Each chunk should be around 3200 chars (except last)
expect(chunks[0].text.length).toBeGreaterThan(2500);
expect(chunks[0].text.length).toBeLessThanOrEqual(3200);
});
});
describe("Token-based Chunking", () => {
test("chunkDocumentByTokens returns single chunk for small documents", async () => {
const content = "This is a small document.";
const chunks = await chunkDocumentByTokens(content, 800, 120);
expect(chunks).toHaveLength(1);
expect(chunks[0].text).toBe(content);
expect(chunks[0].pos).toBe(0);
expect(chunks[0].tokens).toBeGreaterThan(0);
expect(chunks[0].tokens).toBeLessThan(800);
});
test("chunkDocumentByTokens splits large documents", async () => {
// Create a document that's definitely more than 800 tokens
const content = "The quick brown fox jumps over the lazy dog. ".repeat(200);
const chunks = await chunkDocumentByTokens(content, 800, 120);
expect(chunks.length).toBeGreaterThan(1);
// Each chunk should have ~800 tokens or less
for (const chunk of chunks) {
expect(chunk.tokens).toBeLessThanOrEqual(850); // Allow slight overage
expect(chunk.tokens).toBeGreaterThan(0);
}
// Chunks should have correct positions
for (let i = 0; i < chunks.length; i++) {
expect(chunks[i].pos).toBeGreaterThanOrEqual(0);
if (i > 0) {
expect(chunks[i].pos).toBeGreaterThan(chunks[i - 1].pos);
}
}
});
test("chunkDocumentByTokens creates overlapping chunks", async () => {
const content = "Word ".repeat(500); // ~500 tokens
const chunks = await chunkDocumentByTokens(content, 200, 30); // 15% overlap
expect(chunks.length).toBeGreaterThan(1);
// With overlap, consecutive chunks should have overlapping positions
for (let i = 1; i < chunks.length; i++) {
const prevEnd = chunks[i - 1].pos + chunks[i - 1].text.length;
const currentStart = chunks[i].pos;
// Current chunk should start before the previous chunk ended (overlap)
expect(currentStart).toBeLessThan(prevEnd);
}
});
test("chunkDocumentByTokens returns actual token counts", async () => {
const content = "Hello world, this is a test.";
const chunks = await chunkDocumentByTokens(content);
expect(chunks).toHaveLength(1);
// The token count should be reasonable (not 0, not equal to char count)
expect(chunks[0].tokens).toBeGreaterThan(0);
expect(chunks[0].tokens).toBeLessThan(content.length); // Tokens < chars for English
});
});
// =============================================================================
@ -1842,10 +1848,10 @@ describe("Legacy Compatibility", () => {
});
// =============================================================================
// Ollama Integration Tests (using mocked Ollama)
// LlamaCpp Integration Tests (using real local models)
// =============================================================================
describe("Ollama Integration (Mocked)", () => {
describe("LlamaCpp Integration", () => {
test("searchVec returns empty when no vector index", async () => {
const store = await createTestStore();
const collectionName = await createTestCollection();
@ -1895,7 +1901,7 @@ describe("Ollama Integration (Mocked)", () => {
const queries = await store.expandQuery("test query");
expect(queries).toContain("test query");
expect(queries[0]).toBe("test query");
// Mock returns 2 variations
// LlamaCpp returns original + variations
expect(queries.length).toBeGreaterThanOrEqual(1);
await cleanupTestDb(store);
@ -1924,7 +1930,7 @@ describe("Ollama Integration (Mocked)", () => {
const results = await store.rerank("topic", docs);
expect(results).toHaveLength(2);
// Mock returns "yes" with high confidence
// LlamaCpp reranker returns relevance scores
expect(results[0].score).toBeGreaterThan(0);
await cleanupTestDb(store);

View File

@ -15,8 +15,8 @@ import { Database } from "bun:sqlite";
import { Glob } from "bun";
import * as sqliteVec from "sqlite-vec";
import {
Ollama,
getDefaultOllama,
LlamaCpp,
getDefaultLlamaCpp,
formatQueryForEmbedding,
formatDocForEmbedding,
type RerankDocument,
@ -47,11 +47,12 @@ export const DEFAULT_QUERY_MODEL = "qwen3:0.6b";
export const DEFAULT_GLOB = "**/*.md";
export const DEFAULT_MULTI_GET_MAX_BYTES = 10 * 1024; // 10KB
// Re-export OLLAMA_URL for backwards compatibility
export const OLLAMA_URL = getDefaultOllama().getBaseUrl();
// Chunking: ~2000 tokens per chunk, ~3 bytes/token = 6KB
const CHUNK_BYTE_SIZE = 6 * 1024;
// Chunking: 800 tokens per chunk with 15% overlap
export const CHUNK_SIZE_TOKENS = 800;
export const CHUNK_OVERLAP_TOKENS = Math.floor(CHUNK_SIZE_TOKENS * 0.15); // 120 tokens (15% overlap)
// Fallback char-based approximation for sync chunking (~4 chars per token)
export const CHUNK_SIZE_CHARS = CHUNK_SIZE_TOKENS * 4; // 3200 chars
export const CHUNK_OVERLAP_CHARS = CHUNK_OVERLAP_TOKENS * 4; // 480 chars
// =============================================================================
// Path utilities
@ -292,9 +293,9 @@ function initializeDatabase(db: Database): void {
db.exec(`CREATE INDEX IF NOT EXISTS idx_documents_hash ON documents(hash)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_documents_path ON documents(path, active)`);
// Cache table for Ollama API calls
// Cache table for LLM API calls (table name kept for backwards compatibility)
db.exec(`
CREATE TABLE IF NOT EXISTS ollama_cache (
CREATE TABLE IF NOT EXISTS llm_cache (
hash TEXT PRIMARY KEY,
result TEXT NOT NULL,
created_at TEXT NOT NULL
@ -372,10 +373,12 @@ function ensureVecTableInternal(db: Database, dimensions: number): void {
if (tableInfo) {
const match = tableInfo.sql.match(/float\[(\d+)\]/);
const hasHashSeq = tableInfo.sql.includes('hash_seq');
if (match && parseInt(match[1]) === dimensions && hasHashSeq) return;
const hasCosine = tableInfo.sql.includes('distance_metric=cosine');
if (match && parseInt(match[1]) === dimensions && hasHashSeq && hasCosine) return;
// Table exists but wrong schema - need to rebuild
db.exec("DROP TABLE IF EXISTS vectors_vec");
}
db.exec(`CREATE VIRTUAL TABLE vectors_vec USING vec0(hash_seq TEXT PRIMARY KEY, embedding float[${dimensions}])`);
db.exec(`CREATE VIRTUAL TABLE vectors_vec USING vec0(hash_seq TEXT PRIMARY KEY, embedding float[${dimensions}] distance_metric=cosine)`);
}
// =============================================================================
@ -400,7 +403,7 @@ export type Store = {
clearCache: () => void;
// Cleanup and maintenance
deleteOllamaCache: () => number;
deleteLLMCache: () => number;
deleteInactiveDocuments: () => number;
cleanupOrphanedContent: () => number;
cleanupOrphanedVectors: () => number;
@ -488,7 +491,7 @@ export function createStore(dbPath?: string): Store {
clearCache: () => clearCache(db),
// Cleanup and maintenance
deleteOllamaCache: () => deleteOllamaCache(db),
deleteLLMCache: () => deleteLLMCache(db),
deleteInactiveDocuments: () => deleteInactiveDocuments(db),
cleanupOrphanedContent: () => cleanupOrphanedContent(db),
cleanupOrphanedVectors: () => cleanupOrphanedVectors(db),
@ -776,20 +779,20 @@ export function getCacheKey(url: string, body: object): string {
}
export function getCachedResult(db: Database, cacheKey: string): string | null {
const row = db.prepare(`SELECT result FROM ollama_cache WHERE hash = ?`).get(cacheKey) as { result: string } | null;
const row = db.prepare(`SELECT result FROM llm_cache WHERE hash = ?`).get(cacheKey) as { result: string } | null;
return row?.result || null;
}
export function setCachedResult(db: Database, cacheKey: string, result: string): void {
const now = new Date().toISOString();
db.prepare(`INSERT OR REPLACE INTO ollama_cache (hash, result, created_at) VALUES (?, ?, ?)`).run(cacheKey, result, now);
db.prepare(`INSERT OR REPLACE INTO llm_cache (hash, result, created_at) VALUES (?, ?, ?)`).run(cacheKey, result, now);
if (Math.random() < 0.01) {
db.exec(`DELETE FROM ollama_cache WHERE hash NOT IN (SELECT hash FROM ollama_cache ORDER BY created_at DESC LIMIT 1000)`);
db.exec(`DELETE FROM llm_cache WHERE hash NOT IN (SELECT hash FROM llm_cache ORDER BY created_at DESC LIMIT 1000)`);
}
}
export function clearCache(db: Database): void {
db.exec(`DELETE FROM ollama_cache`);
db.exec(`DELETE FROM llm_cache`);
}
// =============================================================================
@ -797,11 +800,11 @@ export function clearCache(db: Database): void {
// =============================================================================
/**
* Delete cached Ollama API responses.
* Delete cached LLM API responses.
* Returns the number of cached responses deleted.
*/
export function deleteOllamaCache(db: Database): number {
const result = db.prepare(`DELETE FROM ollama_cache`).run();
export function deleteLLMCache(db: Database): number {
const result = db.prepare(`DELETE FROM llm_cache`).run();
return result.changes;
}
@ -1007,11 +1010,8 @@ export function getActiveDocumentPaths(db: Database, collectionName: string): st
// Re-export from llm.ts for backwards compatibility
export { formatQueryForEmbedding, formatDocForEmbedding };
export function chunkDocument(content: string, maxBytes: number = CHUNK_BYTE_SIZE): { text: string; pos: number }[] {
const encoder = new TextEncoder();
const totalBytes = encoder.encode(content).length;
if (totalBytes <= maxBytes) {
export function chunkDocument(content: string, maxChars: number = CHUNK_SIZE_CHARS, overlapChars: number = CHUNK_OVERLAP_CHARS): { text: string; pos: number }[] {
if (content.length <= maxChars) {
return [{ text: content, pos: 0 }];
}
@ -1019,52 +1019,174 @@ export function chunkDocument(content: string, maxBytes: number = CHUNK_BYTE_SIZ
let charPos = 0;
while (charPos < content.length) {
let endPos = charPos;
let byteCount = 0;
// Calculate end position for this chunk
let endPos = Math.min(charPos + maxChars, content.length);
while (endPos < content.length && byteCount < maxBytes) {
const charBytes = encoder.encode(content[endPos]).length;
if (byteCount + charBytes > maxBytes) break;
byteCount += charBytes;
endPos++;
}
if (endPos < content.length && endPos > charPos) {
// If not at the end, try to find a good break point
if (endPos < content.length) {
const slice = content.slice(charPos, endPos);
const paragraphBreak = slice.lastIndexOf('\n\n');
const sentenceEnd = Math.max(
slice.lastIndexOf('. '),
slice.lastIndexOf('.\n'),
slice.lastIndexOf('? '),
slice.lastIndexOf('?\n'),
slice.lastIndexOf('! '),
slice.lastIndexOf('!\n')
);
const lineBreak = slice.lastIndexOf('\n');
const spaceBreak = slice.lastIndexOf(' ');
let breakPoint = -1;
if (paragraphBreak > slice.length * 0.5) {
breakPoint = paragraphBreak + 2;
} else if (sentenceEnd > slice.length * 0.5) {
breakPoint = sentenceEnd + 2;
} else if (lineBreak > slice.length * 0.3) {
breakPoint = lineBreak + 1;
} else if (spaceBreak > slice.length * 0.3) {
breakPoint = spaceBreak + 1;
// Look for break points in the last 30% of the chunk
const searchStart = Math.floor(slice.length * 0.7);
const searchSlice = slice.slice(searchStart);
// Priority: paragraph > sentence > line > word
let breakOffset = -1;
const paragraphBreak = searchSlice.lastIndexOf('\n\n');
if (paragraphBreak >= 0) {
breakOffset = searchStart + paragraphBreak + 2;
} else {
const sentenceEnd = Math.max(
searchSlice.lastIndexOf('. '),
searchSlice.lastIndexOf('.\n'),
searchSlice.lastIndexOf('? '),
searchSlice.lastIndexOf('?\n'),
searchSlice.lastIndexOf('! '),
searchSlice.lastIndexOf('!\n')
);
if (sentenceEnd >= 0) {
breakOffset = searchStart + sentenceEnd + 2;
} else {
const lineBreak = searchSlice.lastIndexOf('\n');
if (lineBreak >= 0) {
breakOffset = searchStart + lineBreak + 1;
} else {
const spaceBreak = searchSlice.lastIndexOf(' ');
if (spaceBreak >= 0) {
breakOffset = searchStart + spaceBreak + 1;
}
}
}
}
if (breakPoint > 0) {
endPos = charPos + breakPoint;
if (breakOffset > 0) {
endPos = charPos + breakOffset;
}
}
// Ensure we make progress
if (endPos <= charPos) {
endPos = charPos + 1;
endPos = Math.min(charPos + maxChars, content.length);
}
chunks.push({ text: content.slice(charPos, endPos), pos: charPos });
charPos = endPos;
// Move forward, but overlap with previous chunk
// For last chunk, don't overlap (just go to the end)
if (endPos >= content.length) {
break;
}
charPos = endPos - overlapChars;
if (charPos <= chunks[chunks.length - 1].pos) {
// Prevent infinite loop - move forward at least a bit
charPos = endPos;
}
}
return chunks;
}
/**
* Chunk a document by actual token count using the LLM tokenizer.
* More accurate than character-based chunking but requires async.
*/
export async function chunkDocumentByTokens(
content: string,
maxTokens: number = CHUNK_SIZE_TOKENS,
overlapTokens: number = CHUNK_OVERLAP_TOKENS
): Promise<{ text: string; pos: number; tokens: number }[]> {
const llm = getDefaultLlamaCpp();
// For small documents, check if we need chunking at all
const totalTokens = await llm.countTokens(content);
if (totalTokens <= maxTokens) {
return [{ text: content, pos: 0, tokens: totalTokens }];
}
const chunks: { text: string; pos: number; tokens: number }[] = [];
let charPos = 0;
while (charPos < content.length) {
// Binary search to find the right chunk end position
// Start with an estimate based on average tokens per char
const avgCharsPerToken = content.length / totalTokens;
let estimatedEnd = Math.min(charPos + Math.floor(maxTokens * avgCharsPerToken * 1.1), content.length);
// Get token count for this slice
let slice = content.slice(charPos, estimatedEnd);
let sliceTokens = await llm.countTokens(slice);
// Adjust until we're close to maxTokens
while (sliceTokens > maxTokens && estimatedEnd > charPos + 100) {
// Reduce by ~10%
estimatedEnd = charPos + Math.floor((estimatedEnd - charPos) * 0.9);
slice = content.slice(charPos, estimatedEnd);
sliceTokens = await llm.countTokens(slice);
}
// If we're under, try to expand (but not past content end)
while (sliceTokens < maxTokens * 0.9 && estimatedEnd < content.length) {
const newEnd = Math.min(estimatedEnd + Math.floor((estimatedEnd - charPos) * 0.1), content.length);
if (newEnd === estimatedEnd) break;
const newSlice = content.slice(charPos, newEnd);
const newTokens = await llm.countTokens(newSlice);
if (newTokens > maxTokens) break;
estimatedEnd = newEnd;
slice = newSlice;
sliceTokens = newTokens;
}
// Find a good break point in the last 30% of the chunk
if (estimatedEnd < content.length) {
const searchStart = charPos + Math.floor((estimatedEnd - charPos) * 0.7);
const searchSlice = content.slice(searchStart, estimatedEnd);
let breakOffset = -1;
const paragraphBreak = searchSlice.lastIndexOf('\n\n');
if (paragraphBreak >= 0) {
breakOffset = paragraphBreak + 2;
} else {
const sentenceEnd = Math.max(
searchSlice.lastIndexOf('. '),
searchSlice.lastIndexOf('.\n'),
searchSlice.lastIndexOf('? '),
searchSlice.lastIndexOf('?\n'),
searchSlice.lastIndexOf('! '),
searchSlice.lastIndexOf('!\n')
);
if (sentenceEnd >= 0) {
breakOffset = sentenceEnd + 2;
} else {
const lineBreak = searchSlice.lastIndexOf('\n');
if (lineBreak >= 0) {
breakOffset = lineBreak + 1;
} else {
const spaceBreak = searchSlice.lastIndexOf(' ');
if (spaceBreak >= 0) {
breakOffset = spaceBreak + 1;
}
}
}
}
if (breakOffset >= 0) {
estimatedEnd = searchStart + breakOffset;
slice = content.slice(charPos, estimatedEnd);
sliceTokens = await llm.countTokens(slice);
}
}
chunks.push({ text: slice, pos: charPos, tokens: sliceTokens });
// Move forward with overlap
if (estimatedEnd >= content.length) break;
// Calculate overlap in characters based on token ratio
const overlapChars = Math.floor(overlapTokens * (slice.length / sliceTokens));
charPos = estimatedEnd - overlapChars;
if (charPos <= chunks[chunks.length - 1].pos) {
charPos = estimatedEnd; // Prevent infinite loop
}
}
return chunks;
@ -1675,7 +1797,7 @@ export async function searchVec(db: Database, query: string, model: string, limi
bodyLength: row.body.length,
body: row.body,
context: getContextForFile(db, row.filepath),
score: 1 / (1 + row.distance),
score: 1 - row.distance, // Cosine similarity = 1 - cosine distance
source: "vec" as const,
chunkPos: row.pos,
};
@ -1687,8 +1809,10 @@ export async function searchVec(db: Database, query: string, model: string, limi
// =============================================================================
async function getEmbedding(text: string, model: string, isQuery: boolean): Promise<number[] | null> {
const ollama = getDefaultOllama();
const result = await ollama.embed(text, { model, isQuery });
const llm = getDefaultLlamaCpp();
// Format text using the appropriate prompt template
const formattedText = isQuery ? formatQueryForEmbedding(text) : formatDocForEmbedding(text);
const result = await llm.embed(formattedText, { model, isQuery });
return result?.embedding || null;
}
@ -1750,8 +1874,9 @@ export async function expandQuery(query: string, model: string = DEFAULT_QUERY_M
return [query, ...lines.slice(0, 2)];
}
const ollama = getDefaultOllama();
const results = await ollama.expandQuery(query, model, 2);
const llm = getDefaultLlamaCpp();
// Note: LlamaCpp uses hardcoded model, model parameter is ignored
const results = await llm.expandQuery(query, 2);
// Cache the expanded queries (excluding original)
if (results.length > 1) {
@ -1780,10 +1905,10 @@ export async function rerank(query: string, documents: { file: string; text: str
}
}
// Rerank uncached documents using Ollama
// Rerank uncached documents using LlamaCpp
if (uncachedDocs.length > 0) {
const ollama = getDefaultOllama();
const rerankResult = await ollama.rerank(query, uncachedDocs, { model });
const llm = getDefaultLlamaCpp();
const rerankResult = await llm.rerank(query, uncachedDocs, { model });
// Cache results
for (const result of rerankResult.results) {