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:
parent
a3703c069a
commit
d383b5c226
@ -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
|
||||
|
||||
|
||||
60
README.md
60
README.md
@ -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
409
bun.lock
@ -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=="],
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
1132
src/llm.test.ts
1132
src/llm.test.ts
File diff suppressed because it is too large
Load Diff
793
src/llm.ts
793
src/llm.ts
@ -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);
|
||||
}
|
||||
|
||||
@ -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[] = [
|
||||
|
||||
472
src/qmd.ts
472
src/qmd.ts
@ -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)
|
||||
|
||||
@ -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);
|
||||
|
||||
259
src/store.ts
259
src/store.ts
@ -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) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user