codesearch-mcp 0.1.0-alpha → 0.2.0-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +24 -1
  2. package/README.md +141 -39
  3. package/dist/app.js +2 -2
  4. package/dist/src/commands/IndexCommand.js +42 -1
  5. package/dist/src/config-error.d.ts +7 -0
  6. package/dist/src/config-error.js +14 -0
  7. package/dist/src/config-store.d.ts +30 -12
  8. package/dist/src/config-store.js +81 -26
  9. package/dist/src/config.d.ts +5 -4
  10. package/dist/src/config.js +14 -16
  11. package/dist/src/constants.d.ts +8 -2
  12. package/dist/src/constants.js +8 -5
  13. package/dist/src/help.js +1 -1
  14. package/dist/src/interfaces/IDatabase.d.ts +17 -0
  15. package/dist/src/interfaces/ILogger.d.ts +7 -0
  16. package/dist/src/services/AstChunker.d.ts +11 -0
  17. package/dist/src/services/AstChunker.js +44 -0
  18. package/dist/src/services/DatabaseService.d.ts +4 -0
  19. package/dist/src/services/DatabaseService.js +46 -0
  20. package/dist/src/services/EmbedderFactory.d.ts +1 -1
  21. package/dist/src/services/EmbedderFactory.js +2 -5
  22. package/dist/src/services/IndexerService.d.ts +4 -0
  23. package/dist/src/services/IndexerService.js +27 -7
  24. package/dist/src/services/InfinityEmbedder.d.ts +14 -55
  25. package/dist/src/services/InfinityEmbedder.js +30 -141
  26. package/dist/src/services/LoggerService.d.ts +3 -1
  27. package/dist/src/services/LoggerService.js +17 -3
  28. package/dist/src/services/SearcherService.d.ts +15 -1
  29. package/dist/src/services/SearcherService.js +105 -23
  30. package/dist/src/services/WebServerService.d.ts +41 -3
  31. package/dist/src/services/WebServerService.js +90 -2
  32. package/dist/src/services/infinity-provisioning.d.ts +115 -0
  33. package/dist/src/services/infinity-provisioning.js +516 -0
  34. package/dist/src/services/legacy-ollama-index.d.ts +24 -0
  35. package/dist/src/services/legacy-ollama-index.js +61 -0
  36. package/dist/src/services/reranker.d.ts +3 -3
  37. package/dist/src/services/reranker.js +3 -3
  38. package/dist/src/setup.d.ts +37 -0
  39. package/dist/src/setup.js +111 -157
  40. package/docker/docker-compose.yml +5 -15
  41. package/docs/web-dashboard.md +36 -17
  42. package/package.json +1 -5
  43. package/dist/src/services/OllamaEmbedder.d.ts +0 -25
  44. package/dist/src/services/OllamaEmbedder.js +0 -111
package/CHANGELOG.md CHANGED
@@ -3,7 +3,30 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  Format based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
5
5
 
6
- ## [Unreleased]
6
+ ## [0.2.0-alpha] - 2026-09-29
7
+
8
+ ### Added
9
+
10
+ - **Index consistency — orphan cleanup and project pruning** — `rag-index` now removes orphaned rows left by deleted files, model changes, and deselected projects. Deleted files and `.ragignore`-excluded files are removed from all four index tables (`code_chunk`, `code_symbol`, `code_call`, `code_dependency`) in one transaction per project. `find_symbol`, `get_callers`, `get_dependencies`, and `get_dependents` no longer return results from files that no longer exist on disk. Projects outside the current selection are automatically pruned from the index; each gets a `Removed project <name> (not selected)` log line. A failure pruning one project does not block others or stop indexing, but exits non-zero.
11
+ - **Candidate floor for reranking** — the reranker's candidate pool no longer scales only with `limit`. A new `reranker.candidateFloor` (default `80`) guarantees a minimum pool regardless of `limit`: `fetchLimit = min(max(limit × overfetch, candidateFloor), 200)` when reranking is enabled. Fixes recall starvation at small limits (the default `limit = 5` previously fed the reranker only 15 candidates, so its best matches never entered the window). `reranker.overfetch` default raised `3 → 5`. Applies immediately to search — no re-index.
12
+ - **`logging.searchMetrics` search-tuning telemetry** — a new boolean (default `false`) that emits one structured line per `search_codebase` query at `info`, **independent of `logging.level`**, with the signals needed to size the pool for your hardware: `fetchLimit`, `candidatesReturned`, `poolSaturated`, `maxCosineRankSurvived`, `distinctFilesInResult`, `topFileShare`, `best/worstResultScore`, and `embed/rerank/db/totalMs`. Never logs chunk content, embeddings, or credentials.
13
+ - **Infinity auto-recreates on a missing model, verifying embed + reranker together** — a managed Infinity container that doesn't yet serve the required model set is now automatically recreated (previously only the embedding model was checked, and only in the unreachable branch loaded the reranker). `rag-setup` and the web dashboard verify the full set (embedding + reranker, when on the same server); indexing verifies the embedding model only, so a bad reranker model never blocks or restarts indexing. Classification of managed-vs-external now also checks the compose file's actual published port (not just that a compose file exists), and a crashed or restart-looping container fails fast during the readiness wait instead of hanging out the full 30-minute timeout.
14
+ - **Managed compose file is now the durable source of truth for Infinity's model set** — the model set is written into `INFINITY_MODEL_ID` as a literal (no more `${INFINITY_MODEL_ID:-…}` env interpolation), so a plain `docker compose up -d` after a host reboot or manual restart serves the configured models instead of falling back to a hard-coded default. The write is atomic and preserves the file's permissions.
15
+ - **Web dashboard recreates Infinity in the background** — saving a config that changes the required Infinity model set (e.g. enabling reranking) now triggers the recreate automatically, without blocking the `PUT /api/config` response. New `GET /api/backend-status` endpoint reports progress (`idle`/`recreating`/`ready`/`failed`); the `PUT` response gains a `backend: { action, message }` field. Concurrent saves queue safely — one Docker run at a time, with at most one follow-up applying the latest save.
16
+ - **`InfinityEmbedder` implements `listModels()`** — lists served models via `/models` (2s limit), so the dashboard's model suggestions keep working for the default provider now that `OllamaEmbedder` is gone.
17
+
18
+ ### Changed
19
+
20
+ - **Reranking enabled by default** — `reranker.enabled` is now `true` by default. The shipped `docker-compose.yml` template includes both the embedding model (`jinaai/jina-embeddings-v2-base-code`) and the reranker model (`mixedbread-ai/mxbai-rerank-xsmall-v1`) in `INFINITY_MODEL_ID`, so a fresh `rag-setup` serves both without extra steps. Cloud embedding providers (OpenAI, Voyage) don't expose a `/rerank` endpoint — disable reranking with `rag-config set reranker.enabled false` or point `reranker.baseURL` at a separate server.
21
+ - **Import/re-export-only chunks are no longer indexed** — the AST chunker now drops chunks whose only significant content is `import` / `export … from` / `export *` / `export {…}` statements (≈5–8% of chunks on a typical corpus). They are never a search answer and previously crowded out real results. The dependency graph is unaffected (edges come from the parser, not chunk text). Takes effect on the next per-file change or `rag-index --reindex`.
22
+
23
+ ### Changed (breaking)
24
+
25
+ - **The `ollama` embedding provider is removed** — `OllamaEmbedder` and the `ollama` provider enum value are gone; the managed `ollama` Docker service is removed from the shipped `docker-compose.yml`. Ollama remains fully usable by pointing `embedding.provider: openai-compatible` at `http://localhost:11434/v1` (it already exposes an OpenAI-style `/v1/embeddings` endpoint) — there is no capability loss, only the dedicated code path and the auto-pull-on-index convenience.
26
+ - **Default provider is now `infinity`, default model `jinaai/jina-embeddings-v2-base-code`** (was `ollama` / `bge-m3`); default `reranker.model` is now `mixedbread-ai/mxbai-rerank-xsmall-v1`.
27
+ - **A stored config that still selects Ollama now fails to load, with guidance, instead of being silently reinterpreted.** `embedding.provider: ollama`, a legacy top-level `ollama` block, or a provider-less `embedding` block that still points at Ollama's defaults (port `11434`, or a model name Ollama used to own such as `bge-m3`) all raise a clear error naming the fix (`openai-compatible` at `http://localhost:11434/v1`, via `rag-config edit` or `rag-setup`) and the required `rag-index --reindex` afterward. `rag-config set`/`rag-config reset` cannot fix it (they load the same invalid config); `rag-config edit` can, since it opens the file even while invalid.
28
+ - **`rag-index` refuses to silently rebuild an index that was built by `ollama`** — including an install predating the `index_meta` table (treated as a legacy `ollama`/`bge-m3` index). A plain `rag-index` against such an index now prints an explanation to stderr and exits non-zero before touching the backend or the database; `rag-index --reindex` is the explicit confirmation to rebuild it under the new provider. More generally, `rag-index` now exits non-zero on **any** backend-readiness or schema-reconciliation failure — previously it printed "Embedding setup failed" and exited 0, which a script could not detect. `rag-mcp`/`search_codebase` apply the same "pre-`index_meta` index is `ollama`/`bge-m3`" interpretation read-only, so a stale local install fails with the re-index instruction instead of an opaque pgvector dimension-mismatch error.
29
+ - **`rag-setup` now saves the config before provisioning Infinity** — previously Infinity provisioning ran first, so a Docker failure there could abandon the whole wizard without ever writing `config.json`. The database-container step keeps its original position and abort-without-saving behavior.
7
30
 
8
31
  ## [0.1.0-alpha] - 2026-07-12
9
32
 
package/README.md CHANGED
@@ -1,8 +1,17 @@
1
1
  # codesearch-mcp
2
2
 
3
- Semantic codebase search for AI agents. Indexes your local projects into a PostgreSQL vector database and exposes search via the Model Context Protocol (MCP).
3
+ Semantic code search for AI agents — finds code by **meaning**, not keywords. Indexes your local projects into a PostgreSQL vector database and exposes search via the Model Context Protocol (MCP), so Claude can search your entire codebase automatically.
4
4
 
5
- Ask your AI agent things like *"where is the payment retry logic?"* or *"show me how auth tokens are validated"* — across all your projects at once.
5
+ Ask your AI agent things like *"where is the payment retry logic?"* or *"show me how auth tokens are validated"* — it will find the right code even if those exact words never appear.
6
+
7
+ **Why codesearch-mcp:**
8
+
9
+ - **Dramatically fewer tokens** — without RAG, answering a cross-project question means discovery calls, directory listings, and reading several wrong files before finding the right one; a workspace of 10 projects can easily mean 50 000+ lines to sift through. With RAG, one `search_codebase` call returns 5 targeted chunks (~200–500 lines total) from across *all* projects at once — the model gets the right code immediately, with no exploration overhead
10
+ - **Reranker narrows the result further** — a cross-encoder reranker (enabled by default) re-scores the candidate pool so the top results are the most relevant, not just the closest vector; fewer results means less context consumed per answer
11
+ - **Semantic, not grep** — embeddings capture intent; queries in plain English or pseudocode find the relevant code regardless of naming
12
+ - **Local and private** — default setup runs entirely on your machine (Infinity embeddings + PostgreSQL); no code leaves your network
13
+ - **Code-structure aware** — chunks align to function and class boundaries; each result shows which declaration it belongs to (`InvoiceService.create`, `collectFiles`, …), so the model understands context without reading the surrounding file
14
+ - **Import graph** — `include_graph_context` appends direct imports and dependents inline with each result, replacing separate `get_dependencies` / `get_dependents` calls
6
15
 
7
16
  > **Alpha** — works well locally, API may change before 1.0.
8
17
 
@@ -10,16 +19,17 @@ See `CHANGELOG.md` for version history.
10
19
 
11
20
  ## How it works
12
21
 
13
- 1. `rag-index` scans your projects, chunks source files, embeds them with [bge-m3](https://huggingface.co/BAAI/bge-m3) via Ollama, and stores vectors in PostgreSQL
22
+ 1. `rag-index` scans your projects, chunks source files, embeds them using your configured embedding backend (default: [jina-embeddings-v2-base-code](https://huggingface.co/jinaai/jina-embeddings-v2-base-code) via a local [Infinity](https://github.com/michaelfeil/infinity) container), and stores vectors in PostgreSQL
14
23
  2. `rag-mcp` exposes an MCP server — Claude calls it automatically when you ask about code
15
24
  3. Incremental — only changed files are re-indexed on subsequent runs
25
+ 4. Index stays consistent — deleted files, `.ragignore`-excluded files, and deselected projects are removed from all four index tables on the next `rag-index` run
16
26
 
17
- **Stack:** TypeScript · Drizzle ORM · PostgreSQL + pgvector · Ollama (bge-m3) · LangChain text splitters · ts-morph
27
+ **Stack:** TypeScript · Drizzle ORM · PostgreSQL + pgvector · Infinity (jina-embeddings-v2-base-code) · LangChain text splitters · ts-morph
18
28
 
19
29
  ## Requirements
20
30
 
21
31
  - Node.js 18+
22
- - Docker (for local DB + Ollama) — or bring your own PostgreSQL with pgvector + Ollama
32
+ - Docker (for local DB + Infinity) — or bring your own PostgreSQL with pgvector + an embedding backend (see [Embedding providers](#embedding-providers))
23
33
 
24
34
  ## Installation
25
35
 
@@ -35,11 +45,19 @@ rag-setup
35
45
 
36
46
  Walks you through:
37
47
  - **Database** — enter connection details; optionally starts Docker containers automatically
38
- - **Embedding backend** — pick a provider (`ollama` / `infinity` / `openai-compatible` / `voyage`), its model, and an API key if the provider needs one (see [Embedding providers](#embedding-providers))
48
+ - **Embedding backend** — pick a provider (`infinity` / `openai-compatible` / `voyage`), its model, and an API key if the provider needs one (see [Embedding providers](#embedding-providers))
39
49
  - **Logging** — choose log level and directory (defaults: `error` level, `~/.config/codesearch-mcp/logs/`)
40
50
 
41
51
  Config is saved to `~/.config/codesearch-mcp/config.json`.
42
52
 
53
+ ## Register with Claude Code
54
+
55
+ ```bash
56
+ claude mcp add codesearch-mcp -s user -- rag-mcp
57
+ ```
58
+
59
+ Then ask Claude anything about your code — it will use the tools below automatically.
60
+
43
61
  ## Index your projects
44
62
 
45
63
  ```bash
@@ -62,14 +80,6 @@ If `workspaceRoot` is an [Nx](https://nx.dev) workspace (detected by `nx.json`),
62
80
 
63
81
  No configuration needed — detection is automatic. Plain (non-Nx) workspaces keep the original behavior: each immediate subdirectory with a `package.json` is one project.
64
82
 
65
- ## Register with Claude Code
66
-
67
- ```bash
68
- claude mcp add codesearch-mcp -s user -- rag-mcp
69
- ```
70
-
71
- Then ask Claude anything about your code — it will use the tools below automatically.
72
-
73
83
  ## Web dashboard
74
84
 
75
85
  ```bash
@@ -78,15 +88,15 @@ rag-web # http://127.0.0.1:4319 (--port to change)
78
88
 
79
89
  A local web UI (bound to `127.0.0.1` only) with a schema-driven config editor,
80
90
  an overview of indexed projects, and the interactive dependency graph. The
81
- config form shows the applied default for any unset key and offers dropdowns for
82
- model fields (the embedding model is restricted to dimension-compatible models).
91
+ config form shows the applied default for any unset key and suggests dimension-compatible
92
+ models in the model fields (free text is always accepted).
83
93
  See [docs/web-dashboard.md](docs/web-dashboard.md) for the full reference.
84
94
 
85
95
  ## MCP Tools
86
96
 
87
97
  | Tool | Description |
88
98
  |------|-------------|
89
- | `search_codebase` | Semantic search across all indexed projects. Accepts `include_graph_context` to append direct imports/dependents to each result, and `include_neighbors` for surrounding chunks |
99
+ | `search_codebase` | Semantic search across all indexed projects. Accepts `include_graph_context` to append direct imports/dependents to each result, `include_neighbors` for surrounding chunks, and `threshold` to override the similarity threshold for that query |
90
100
  | `list_projects` | List projects with metadata and chunk counts |
91
101
  | `find_symbol` | Find a function, class, or type by exact name — faster than semantic search |
92
102
  | `list_files` | Browse the directory tree of a project |
@@ -104,6 +114,7 @@ Add `.ragmeta.json` to a project root to enrich `list_projects` and generate a v
104
114
  {
105
115
  "description": "Main API server",
106
116
  "stack": "NestJS, GraphQL, PostgreSQL",
117
+ "notes": "Uses optimistic locking — see order-service/docs/concurrency.md",
107
118
  "relations": {
108
119
  "consumed_by": ["crm-frontend"],
109
120
  "consumes": ["billing-service", "auth-service"]
@@ -111,7 +122,7 @@ Add `.ragmeta.json` to a project root to enrich `list_projects` and generate a v
111
122
  }
112
123
  ```
113
124
 
114
- After each indexing run an interactive `project-graph.html` is written to the workspace root.
125
+ After each indexing run an interactive `project-graph.html` is written to the workspace root. The same visualization is available in the web dashboard's Graph tab (`rag-web`).
115
126
 
116
127
  ## Graph context
117
128
 
@@ -166,6 +177,7 @@ rag-config show # print current config (password redacted)
166
177
  rag-config get <key> # print a single value
167
178
  rag-config set <key> <value> # update a value
168
179
  rag-config reset <key> # remove key (reverts to default)
180
+ rag-config edit # open config in $EDITOR; validates on save (use this to recover an invalid config)
169
181
  ```
170
182
 
171
183
  | Key | Default | Description |
@@ -176,33 +188,40 @@ rag-config reset <key> # remove key (reverts to default)
176
188
  | `db.password` | — | PostgreSQL password |
177
189
  | `db.name` | `rag_<os-username>` | Database name — auto-created on first run |
178
190
  | `workspaceRoot` | — | Root of your projects directory |
179
- | `embedding.provider` | `ollama` | Embedding backend: `ollama` \| `infinity` \| `openai-compatible` \| `voyage` |
180
- | `embedding.baseURL` | per-provider | Backend base URL (ollama `http://localhost:11434`, OpenAI `https://api.openai.com/v1`, Infinity/TEI/vLLM `http://localhost:7997`, Voyage `https://api.voyageai.com/v1`) |
191
+ | `embedding.provider` | `infinity` | Embedding backend: `infinity` \| `openai-compatible` \| `voyage`. A stored `ollama` value fails validation with migration guidance (see [Embedding providers](#embedding-providers)) |
192
+ | `embedding.baseURL` | per-provider | Backend base URL (Infinity/TEI/vLLM `http://localhost:7997`, OpenAI `https://api.openai.com/v1`, Voyage `https://api.voyageai.com/v1`; Ollama via `openai-compatible` at `http://localhost:11434/v1`) |
181
193
  | `embedding.apiKey` | `''` | API key, or `env:VAR_NAME` to read from `.env`/environment. Empty for local servers |
182
- | `embedding.model` | `bge-m3` | Embedding model |
194
+ | `embedding.model` | `jinaai/jina-embeddings-v2-base-code` | Embedding model |
183
195
  | `embedding.dimensions` | (derived) | Vector width override; required for models whose native width exceeds 2000 (Matryoshka). Otherwise derived from model → registry → runtime probe |
184
196
  | `embedding.batchSize` | `32` | Texts per batch embedding request |
185
197
  | `search.similarityThreshold` | `0.6` | Max cosine distance — raise if search returns nothing |
186
- | `reranker.enabled` | `false` | Enable cross-encoder reranking; requires `reranker.model` |
187
- | `reranker.provider` | `openai-compatible` | Rerank backend — `openai-compatible` `/rerank` only (Infinity, TEI, vLLM). Ollama has no rerank API |
198
+ | `reranker.enabled` | `true` | Enable cross-encoder reranking; requires `reranker.model` |
199
+ | `reranker.provider` | `openai-compatible` | Rerank backend — `openai-compatible` `/rerank` only (Infinity, TEI, vLLM) |
188
200
  | `reranker.baseURL` | `embedding.baseURL` | Rerank server base URL (Infinity serves embed + rerank from one server) |
189
- | `reranker.model` | `''` | Reranker model name (e.g. `qllama/bge-reranker-v2-m3`) |
201
+ | `reranker.model` | `mixedbread-ai/mxbai-rerank-xsmall-v1` | Reranker model name |
202
+ | `reranker.overfetch` | `5` | Candidate multiplier before reranking (`limit × N`) |
203
+ | `reranker.candidateFloor` | `80` | Minimum candidate pool fed to the reranker regardless of `limit` — `fetchLimit = min(max(limit × overfetch, candidateFloor), 200)`. Raise on GPU/fast hosts for better recall; lower on weak CPU to cut latency |
204
+ | `indexing.chunkSize` | `1500` | Max characters per chunk |
205
+ | `indexing.chunkOverlap` | `200` | Overlap between chunks |
206
+ | `indexing.embedBatchSize` | `5` | Parallel embedding requests per batch |
190
207
  | `indexing.astBatchSize` | `3000` | Max files per ts-morph batch — lower if indexing large projects causes OOM |
191
208
  | `indexing.chunkStrategy` | `ast` | How TS/JS files are chunked: `ast` = boundaries aligned to functions/classes, `character` = fixed-size splitter. Re-index to apply |
209
+ | `search.defaultLimit` | `5` | Default result count returned by `search_codebase` |
210
+ | `search.maxLimit` | `20` | Hard cap on result count |
192
211
  | `logging.level` | `error` | Log level written to file: `error` \| `warn` \| `info` \| `debug` |
193
212
  | `logging.dir` | `~/.config/codesearch-mcp/logs` | Base log directory, or `"false"` to disable |
213
+ | `logging.searchMetrics` | `false` | Emit one structured search-tuning line per query (at `info`, independent of `logging.level`) — use it to size `candidateFloor`/`overfetch`/`threshold` for your hardware |
194
214
 
195
215
  ## Embedding providers
196
216
 
197
- `embedding.provider` selects the backend. All four produce vectors the same way; they differ in where they run and which models they offer.
217
+ `embedding.provider` selects the backend. All three produce vectors the same way; they differ in where they run and which models they offer. There is no dedicated Ollama provider — Ollama exposes an OpenAI-compatible `/v1/embeddings` endpoint, so it's reached through `openai-compatible` (see below).
198
218
 
199
219
  ### Choosing a provider
200
220
 
201
221
  | Provider | When to use | Needs |
202
222
  |----------|-------------|-------|
203
- | `ollama` | Default. Local, zero-setup, general-purpose (`bge-m3`); auto-pulls the model. | Ollama running (Docker or native) |
204
- | `infinity` | Local, best quality **plus a real reranker** in one container; GPU or CPU. | Docker (managed) or a running Infinity server |
205
- | `openai-compatible` | OpenAI cloud, or any OpenAI-style server (TEI, vLLM, LM Studio, LocalAI). | `baseURL` (+ `apiKey` for cloud) |
223
+ | `infinity` | Default. Local, best quality **plus a real reranker** in one container; GPU or CPU. | Docker (managed) or a running Infinity server |
224
+ | `openai-compatible` | OpenAI cloud, or any OpenAI-style server (TEI, vLLM, LM Studio, LocalAI, **or Ollama via its `/v1` endpoint**). | `baseURL` (+ `apiKey` for cloud) |
206
225
  | `voyage` | Voyage AI cloud, code-specialized embeddings. | Voyage `apiKey` |
207
226
 
208
227
  Easiest path: `rag-setup` asks for the provider, model, and any API key. Haven't run setup? It's the place to start. To change things later, edit config directly (below).
@@ -212,11 +231,19 @@ Easiest path: `rag-setup` asks for the provider, model, and any API key. Haven't
212
231
  Any change to the provider, model, or dimension requires a **re-index** — the mismatch guard blocks search until you do it. The index is a rebuildable cache, so there's no data migration.
213
232
 
214
233
  ```bash
215
- # Switch provider (example: OpenAI cloud)
234
+ # Switch to OpenAI cloud
216
235
  rag-config set embedding.provider openai-compatible
217
236
  rag-config set embedding.baseURL https://api.openai.com/v1
218
237
  rag-config set embedding.model text-embedding-3-small
219
238
  rag-config set embedding.apiKey env:OPENAI_API_KEY # key lives in .env, not config.json
239
+ rag-config set reranker.enabled false # OpenAI has no /rerank endpoint
240
+ rag-index --reindex
241
+
242
+ # Switch to Voyage AI
243
+ rag-config set embedding.provider voyage
244
+ rag-config set embedding.model voyage-code-3
245
+ rag-config set embedding.apiKey env:VOYAGE_API_KEY # key lives in .env, not config.json
246
+ rag-config set reranker.enabled false # Voyage has no /rerank endpoint
220
247
  rag-index --reindex
221
248
 
222
249
  # Change only the model on the current provider
@@ -224,31 +251,106 @@ rag-config set embedding.model <model-name>
224
251
  rag-index --reindex
225
252
  ```
226
253
 
254
+ ### Breaking change: the `ollama` provider was removed
255
+
256
+ > **Migrating from an older version?** A config with `embedding.provider: ollama` (or an old-style top-level `ollama` block, or `embedding.baseURL`/`embedding.model` still pointing at Ollama's defaults with no `provider` set) now **fails to load** with an error explaining the fix.
257
+ >
258
+ > Fix it with `rag-config edit` (opens the file even while it's invalid) or by re-running `rag-setup`: set `embedding.provider` to `openai-compatible` and `embedding.baseURL` to `http://localhost:11434/v1` to keep using Ollama, or switch to `infinity` for a managed local backend.
259
+ >
260
+ > Either way, run `rag-index --reindex` afterward. A plain `rag-index` additionally refuses (non-zero exit, no changes made) to silently rebuild an index built by the old `ollama` provider — including an older install with no `index_meta` row, which is assumed to be a pre-migration Ollama/`bge-m3` index.
261
+
227
262
  Per-provider notes:
228
263
 
229
- - **ollama** — `rag-index` auto-pulls the model if it isn't present locally (`bge-m3`, `nomic-embed-text`, `mxbai-embed-large`, …). No API key. Change model: `rag-config set embedding.model nomic-embed-text`.
230
- - **infinity** — the model set is bound at container startup, so changing `embedding.model` (or the reranker model) **recreates** the managed container on the next `rag-index`. Defaults: `jinaai/jina-embeddings-v2-base-code` + reranker `Alibaba-NLP/gte-reranker-modernbert-base`.
231
- - **openai-compatible** — point `embedding.baseURL` at the server; set `embedding.apiKey` (as `env:VAR`) for cloud, leave empty for local servers (TEI/vLLM/LM Studio). E.g. `text-embedding-3-small`, `text-embedding-3-large`.
232
- - **voyage** — set `embedding.apiKey`; e.g. `voyage-code-3`, `voyage-3`.
264
+ - **infinity** — the model set is bound at container startup, so changing `embedding.model` (or the reranker model) **recreates** the managed container on the next `rag-setup`/`rag-index`/dashboard save. Defaults: `jinaai/jina-embeddings-v2-base-code` + reranker `mixedbread-ai/mxbai-rerank-xsmall-v1`. Both models are included in the compose template and reranking is **enabled by default** (`reranker.enabled: true`).
265
+ - **openai-compatible** — point `embedding.baseURL` at the server; set `embedding.apiKey` (as `env:VAR`) for cloud, leave empty for local servers (TEI/vLLM/LM Studio/**Ollama**). E.g. `text-embedding-3-small`, `text-embedding-3-large`. Cloud endpoints (OpenAI, Azure) don't expose `/rerank` — disable the reranker or point it at a separate server (see [Reranker with a cloud embedder](#reranker-with-a-cloud-or-external-embedder)).
266
+ - **Ollama** (via `openai-compatible`) — set `embedding.baseURL` to `http://localhost:11434/v1` and `embedding.model` to any model you've pulled (e.g. `bge-m3`). No dedicated code path, no auto-pull — pull the model yourself with `ollama pull <model>` first.
267
+ - **voyage** — set `embedding.apiKey` (as `env:VAR`); e.g. `voyage-code-3`, `voyage-3`. Voyage doesn't expose a `/rerank` endpoint in the OpenAI-compatible format — disable the reranker or point it at a separate server (see [Reranker with a cloud embedder](#reranker-with-a-cloud-or-external-embedder)).
233
268
 
234
269
  The vector **dimension** is derived automatically (model registry → one-shot probe); set `embedding.dimensions` only to force a Matryoshka-reduced width for models whose native size exceeds 2000.
235
270
 
236
271
  ### Infinity — local embeddings + reranking in one container
237
272
 
238
- Select `infinity` in `rag-setup` (or set `embedding.provider = infinity`) to run [Infinity](https://github.com/michaelfeil/infinity), which serves both `/embeddings` and `/rerank` from a single container — so you get a real cross-encoder reranker locally, which Ollama does not offer. During setup you're asked for an embedding model (default `jinaai/jina-embeddings-v2-base-code`, 768 dims) and an optional reranker model (default `Alibaba-NLP/gte-reranker-modernbert-base`); when a reranker is given, `reranker.*` is auto-wired to the same server.
273
+ `infinity` is the default provider (select it in `rag-setup`, or set `embedding.provider = infinity`) — it runs [Infinity](https://github.com/michaelfeil/infinity), which serves both `/embeddings` and `/rerank` from a single container, so you get a real cross-encoder reranker locally. During setup you're asked for an embedding model (default `jinaai/jina-embeddings-v2-base-code`, 768 dims) and a reranker model (default `mixedbread-ai/mxbai-rerank-xsmall-v1`, enabled by default); `reranker.*` is auto-wired to the same server.
274
+
275
+ Managed vs. external is decided by a `/health` probe at `embedding.baseURL` (default `http://localhost:7997`) plus the managed compose file — no config flag:
276
+
277
+ - **Managed** — the app-owned compose file (`~/.config/codesearch-mcp/docker-compose.yml`) declares the `infinity` service and `embedding.baseURL` points at its published port on `localhost`/`127.0.0.1`/`[::1]`. If nothing answers the probe, or the running container doesn't serve the configured model set, `rag-setup`, `rag-index`, and the web dashboard bring up (or recreate) that container. The full model set (embedding + reranker, when on the same server) is written into the compose file first — a plain `docker compose up -d` afterward serves the same set, no env var needed. It **prefers an NVIDIA GPU and falls back to CPU** automatically (GPU layered via `docker-compose.infinity-gpu.yml` only when `nvidia-smi` is present). To opt out of management entirely, remove the `infinity` service from that compose file.
278
+ - **External** — any other `embedding.baseURL` (a remote host, a server on a different port, or a managed compose file that's absent/unreadable) is used as-is; no Docker command ever runs. If it's missing a required model, you're shown the `--model-id` flag to restart it with.
279
+
280
+ The same "already running?" probe applies everywhere: a reachable backend defaults `rag-setup`'s "start Docker?" prompt to no, and a web-dashboard save that doesn't change the served model set never touches Docker.
239
281
 
240
- Managed vs. external is decided by a `/health` probe at `embedding.baseURL` (default `http://localhost:7997`) — no config flag:
282
+ ### Reranker with a cloud or external embedder
241
283
 
242
- - **Managed** — if nothing answers the probe, `rag-setup` (and `rag-index`) bring up the bundled `infinity` Docker service with your model set. It **prefers an NVIDIA GPU and falls back to CPU** automatically (GPU layered via `docker-compose.infinity-gpu.yml` only when `nvidia-smi` is present). Because Infinity binds its models at startup, changing `embedding.model`/reranker model **recreates** the container.
243
- - **External** — if a server already answers (your own Infinity, remote host, CI), it is used as-is and no Docker command runs. Point `embedding.baseURL` at it.
284
+ The reranker is **enabled by default** (`reranker.enabled: true`) and `reranker.baseURL` defaults to `embedding.baseURL`. Cloud embedding endpoints (OpenAI, Voyage, Azure) don't serve `/rerank` — the reranker call will fail silently and fall back to vector order on every query. Fix it one of two ways:
244
285
 
245
- The same "already running?" probe now also applies to Ollama: a reachable backend defaults `rag-setup`'s "start Docker?" prompt to no.
286
+ **Option A — disable reranking** (simplest, recommended for cloud-only setups):
287
+
288
+ ```bash
289
+ rag-config set reranker.enabled false
290
+ ```
291
+
292
+ **Option B — point the reranker at a separate server** (e.g. a local Infinity or TEI instance while using Voyage/OpenAI for embeddings):
293
+
294
+ ```bash
295
+ rag-config set reranker.baseURL http://localhost:7997
296
+ rag-config set reranker.model mixedbread-ai/mxbai-rerank-xsmall-v1
297
+ # reranker.enabled is already true
298
+ ```
299
+
300
+ The embedding and reranking servers are fully independent — any combination works: Voyage embeddings + local Infinity reranker, OpenAI embeddings + remote TEI reranker, etc.
246
301
 
247
302
  ### Secrets & external embedders
248
303
 
249
304
  API keys live in `~/.config/codesearch-mcp/.env` (chmod 600), **not** in `config.json`. Reference them with `env:VAR_NAME`, e.g. `embedding.apiKey = env:OPENAI_API_KEY` with `OPENAI_API_KEY=sk-…` in `.env`. The file is loaded into the environment at startup (a real environment variable wins over the file), so all binaries — including the client-spawned `rag-mcp` — see it identically.
250
305
 
251
- Switching the embedding model, provider, or dimension is applied by **re-indexing** (`rag-index --reindex`) — the index is a rebuildable cache, so there is no data migration. A single-row `index_meta` table records which model built the index; `search_codebase` and `rag-mcp` refuse with a clear "re-index required" error when the configured model disagrees, rather than returning noise from a foreign vector space. Staying on Ollama + `bge-m3` keeps dimension 1024 and forces no re-index.
306
+ Switching the embedding model, provider, or dimension is applied by **re-indexing** (`rag-index --reindex`) — the index is a rebuildable cache, so there is no data migration. A single-row `index_meta` table records which model built the index; `search_codebase` and `rag-mcp` refuse with a clear "re-index required" error when the configured model disagrees, rather than returning noise from a foreign vector space.
307
+
308
+ ## Tuning search recall
309
+
310
+ When reranking is enabled, `search_codebase` fetches a **candidate pool** by cosine distance, reranks it with the cross-encoder, and returns the top `limit`. Three knobs and one relationship govern how good the results are and how long they take:
311
+
312
+ - **Pool size** — `fetchLimit = min(max(limit × overfetch, candidateFloor), 200)`. `candidateFloor` (default `80`) guarantees the reranker a large enough menu even at the default `limit = 5`; without it a small `limit` starves the reranker (only `5 × 5 = 25` candidates) and its best matches never enter the window.
313
+ - **Threshold ↔ window** — raising `search.similarityThreshold` lets *more* chunks under the bar, so the pool must be wide enough to reach them; a strict threshold with a small pool silently truncates recall. Widen `candidateFloor`/`overfetch` when you raise the threshold.
314
+ - **Latency** — the reranker cost grows with pool size (on CPU, tens of seconds; a rough guide is a large fixed cost plus a per-candidate increment). Bigger pool = better recall, slower query. GPU/fast hosts can raise `candidateFloor`; ultra-constrained hosts lower it.
315
+
316
+ To size these for **your** hardware, turn on telemetry:
317
+
318
+ ```bash
319
+ rag-config set logging.searchMetrics true
320
+ ```
321
+
322
+ Each search then writes one `info` line (independent of `logging.level`) with, among others: `fetchLimit`, `candidatesReturned`, `poolSaturated`, `maxCosineRankSurvived`, `distinctFilesInResult`, `topFileShare`, and `embedMs`/`rerankMs`/`dbMs`/`totalMs`. Read them as:
323
+
324
+ - **`poolSaturated: true`** — more chunks were under the threshold than the window took → recall is being truncated; raise `candidateFloor`/`overfetch`.
325
+ - **`maxCosineRankSurvived`** — the deepest cosine rank that survived reranking. If it stays well below `fetchLimit`, the pool is bigger than needed → shrink it to save latency; if it approaches `fetchLimit`, widen.
326
+
327
+ ## Index consistency
328
+
329
+ `rag-index` keeps the four index tables (`code_chunk`, `code_symbol`, `code_call`, `code_dependency`) consistent after every run:
330
+
331
+ | Event | What happens |
332
+ |---|---|
333
+ | **File deleted from disk** | All rows for that file are removed from the four tables in one transaction on the next `rag-index`. The file disappears from `find_symbol`, `get_callers`, `get_dependencies`, and `get_dependents`. |
334
+ | **File matched by `.ragignore`** | Same as deleted — treated as not collected, removed from all four tables. |
335
+ | **Project deselected** | All rows for that project are removed from the four tables. To bring it back, re-select and re-index — the index is a cache. The removal is printed as `Removed project <name> (not selected)`. |
336
+ | **`workspaceRoot` changed** | All projects indexed under the old root (their paths no longer match anything under the new root) are pruned. Each gets its own `Removed project` line. No confirmation is required — the index is a cache that `rag-index` rebuilds. |
337
+
338
+ A failure pruning one project does not block the others or stop indexing of the selected projects, but `rag-index` exits non-zero to signal the partial failure.
339
+
340
+ ## Architecture
341
+
342
+ **Stack:** TypeScript on Node.js 18+ · PostgreSQL 17 + pgvector (HNSW, cosine distance) via Drizzle ORM · ts-morph for AST/symbol/call-graph extraction · `@langchain/textsplitters` for chunk fallback · MCP SDK over stdio · Zod v4 for config validation and JSON-Schema generation · pino + pino-roll for logging · plain `node:http` dashboard with vis-network · Vitest · Docker Compose for Postgres + Infinity.
343
+
344
+ **Design:**
345
+ - **Composition root + manual DI** — `src/container.ts` builds every service once and wires it through constructors; no module-level singletons, no DI framework.
346
+ - **Dependency inversion** — services depend on interfaces (`IEmbedder`, `IDatabase`, `IAstParser`, `IChunker`, `ISearcher`, `IProjectDiscovery`, `IMonorepoResolver`, `IMcpToolHandler`), so providers (OpenAI-compatible/Voyage/Infinity — Ollama via `openai-compatible`), chunking strategy (`ast`/`character`), and monorepo resolvers are swappable.
347
+ - **Command pattern** — each CLI mode (`rag-index`, `rag-mcp`, `rag-config`, `rag-web`) is its own `ICommand`; `app.ts` only dispatches.
348
+ - **Registry / Open-Closed** — MCP tools are `IMcpToolHandler` implementations registered in a `Map`; adding a tool never touches `McpServer`.
349
+ - **Fallback chains** — vector dimension: config override → model registry → runtime probe; `find_symbol`/`get_dependents`: indexed table → grep; failed rerank → vector order.
350
+ - **Incremental indexing** — mtime fast path, SHA-256 fallback for clones/transfers, AST-aligned chunk boundaries so declarations aren't split arbitrarily.
351
+ - **Index/model consistency guard** — `index_meta` tracks the embedding model that built the index; search refuses with a clear re-index message on mismatch instead of returning noise.
352
+
353
+ Full internals (per-file responsibilities, every pattern with file:line references) are documented in `CLAUDE.md`.
252
354
 
253
355
  ## License
254
356
 
package/dist/app.js CHANGED
@@ -144,7 +144,7 @@ async function main() {
144
144
  }
145
145
  if (mode === 'setup') {
146
146
  await (0, setup_1.runSetup)();
147
- process.exit(0);
147
+ process.exit(process.exitCode ?? 0);
148
148
  }
149
149
  if (mode === 'config') {
150
150
  await new ConfigCommand_1.ConfigCommand(effectiveArgs).execute();
@@ -189,6 +189,6 @@ async function main() {
189
189
  }
190
190
  await factory().execute();
191
191
  await container.db.destroy();
192
- process.exit(0);
192
+ process.exit(process.exitCode ?? 0);
193
193
  }
194
194
  main().catch(err => { console.error(err); process.exit(1); });
@@ -6,6 +6,7 @@ const project_selector_1 = require("../project-selector");
6
6
  const graph_1 = require("../graph");
7
7
  const MonorepoAwareDiscoveryService_1 = require("../services/MonorepoAwareDiscoveryService");
8
8
  const file_utils_1 = require("../file-utils");
9
+ const legacy_ollama_index_1 = require("../services/legacy-ollama-index");
9
10
  class IndexCommand {
10
11
  container;
11
12
  argv1;
@@ -19,7 +20,30 @@ class IndexCommand {
19
20
  const force = args.includes('--reindex');
20
21
  const forceSelect = args.includes('--select');
21
22
  await db.initDbWithSchema();
22
- // Ensure the local model is pulled (Ollama only — no-op for cloud), then
23
+ // A former Ollama installation the config layer cannot recognise (old
24
+ // defaults, no `embedding`/`ollama` block) is protected here: without an
25
+ // explicit `--reindex`, refuse to touch an index built by `ollama` (or a
26
+ // pre-`index_meta` install, which is indistinguishable from one) before
27
+ // any backend readiness check, Docker command, or schema work.
28
+ if (!force) {
29
+ let identity;
30
+ try {
31
+ identity = await (0, legacy_ollama_index_1.resolveIndexedIdentity)(db);
32
+ }
33
+ catch (err) {
34
+ console.error(`\n${err.message}`);
35
+ process.exitCode = 1;
36
+ return;
37
+ }
38
+ if (identity?.provider === 'ollama') {
39
+ console.error(`\nThe existing index was built with ${identity.provider}/${identity.model}, but "ollama" is no ` +
40
+ `longer a supported embedding provider (configured: ${config.embedding.provider}/${config.embedding.model}). ` +
41
+ `Run \`rag-index --reindex\` to confirm rebuilding the index under the configured provider.`);
42
+ process.exitCode = 1;
43
+ return;
44
+ }
45
+ }
46
+ // Ensure the local model is served (Infinity only — no-op for cloud), then
23
47
  // resolve the active embedding dimension and reconcile it with index_meta
24
48
  // (recreate code_chunk on a dimension change, clear rows on a model change)
25
49
  // before scanning. A clear error here beats a mid-run vector width mismatch.
@@ -30,6 +54,7 @@ class IndexCommand {
30
54
  }
31
55
  catch (err) {
32
56
  console.error(`\nEmbedding setup failed: ${err.message}`);
57
+ process.exitCode = 1;
33
58
  return;
34
59
  }
35
60
  const controller = new AbortController();
@@ -42,6 +67,22 @@ class IndexCommand {
42
67
  // Expand any selected nested-monorepo unit (e.g. an Nx repo) into its
43
68
  // sub-projects; ordinary projects pass through unchanged.
44
69
  const projects = (0, MonorepoAwareDiscoveryService_1.toLeafProjects)(selected);
70
+ let pruneResult;
71
+ try {
72
+ pruneResult = await indexer.pruneProjects(projects.map(p => p.name));
73
+ }
74
+ catch (err) {
75
+ console.error(`\nPruning failed: ${err.message}`);
76
+ process.exitCode = 1;
77
+ return;
78
+ }
79
+ const { removed, failed } = pruneResult;
80
+ for (const name of removed) {
81
+ console.log(`Removed project ${name} (not selected)`);
82
+ }
83
+ if (failed.length > 0) {
84
+ process.exitCode = 1;
85
+ }
45
86
  const filesPerProject = projects.map(p => (0, file_utils_1.collectFiles)(p.path, (0, file_utils_1.loadIgnore)(p.path), p.path));
46
87
  const totalFiles = filesPerProject.reduce((sum, f) => sum + f.length, 0);
47
88
  console.log(`\n${projects.length} projects — ${totalFiles} files total`);
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Own module (not `config.ts`) so `config-store.ts` can throw it too without
3
+ * an import cycle — `config.ts` imports from `config-store.ts`.
4
+ */
5
+ export declare class ConfigError extends Error {
6
+ constructor(message: string);
7
+ }
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ConfigError = void 0;
4
+ /**
5
+ * Own module (not `config.ts`) so `config-store.ts` can throw it too without
6
+ * an import cycle — `config.ts` imports from `config-store.ts`.
7
+ */
8
+ class ConfigError extends Error {
9
+ constructor(message) {
10
+ super(message);
11
+ this.name = 'ConfigError';
12
+ }
13
+ }
14
+ exports.ConfigError = ConfigError;
@@ -13,7 +13,6 @@ declare const jsonSchema: z.ZodObject<{
13
13
  }, z.core.$strip>>;
14
14
  embedding: z.ZodOptional<z.ZodObject<{
15
15
  provider: z.ZodOptional<z.ZodEnum<{
16
- ollama: "ollama";
17
16
  "openai-compatible": "openai-compatible";
18
17
  voyage: "voyage";
19
18
  infinity: "infinity";
@@ -49,6 +48,7 @@ declare const jsonSchema: z.ZodObject<{
49
48
  model: z.ZodOptional<z.ZodString>;
50
49
  url: z.ZodOptional<z.ZodString>;
51
50
  overfetch: z.ZodOptional<z.ZodNumber>;
51
+ candidateFloor: z.ZodOptional<z.ZodNumber>;
52
52
  }, z.core.$strip>>;
53
53
  logging: z.ZodOptional<z.ZodObject<{
54
54
  level: z.ZodOptional<z.ZodEnum<{
@@ -58,6 +58,7 @@ declare const jsonSchema: z.ZodObject<{
58
58
  warn: "warn";
59
59
  }>>;
60
60
  dir: z.ZodOptional<z.ZodString>;
61
+ searchMetrics: z.ZodOptional<z.ZodBoolean>;
61
62
  }, z.core.$strip>>;
62
63
  workspaceRoot: z.ZodOptional<z.ZodString>;
63
64
  }, z.core.$strip>;
@@ -77,9 +78,9 @@ export declare const ALL_KEYS: string[];
77
78
  */
78
79
  export declare const CONFIG_DEFAULTS: {
79
80
  readonly 'db.port': 5433;
80
- readonly 'embedding.provider': "ollama";
81
+ readonly 'embedding.provider': "infinity";
81
82
  readonly 'embedding.apiKey': "";
82
- readonly 'embedding.model': "bge-m3";
83
+ readonly 'embedding.model': "jinaai/jina-embeddings-v2-base-code";
83
84
  readonly 'embedding.batchSize': 32;
84
85
  readonly 'indexing.chunkSize': 1500;
85
86
  readonly 'indexing.chunkOverlap': 200;
@@ -89,20 +90,30 @@ export declare const CONFIG_DEFAULTS: {
89
90
  readonly 'search.similarityThreshold': 0.6;
90
91
  readonly 'search.defaultLimit': 5;
91
92
  readonly 'search.maxLimit': 20;
92
- readonly 'reranker.enabled': false;
93
+ readonly 'reranker.enabled': true;
93
94
  readonly 'reranker.provider': "openai-compatible";
94
95
  readonly 'reranker.apiKey': "";
95
- readonly 'reranker.model': "";
96
- readonly 'reranker.overfetch': 3;
96
+ readonly 'reranker.model': "mixedbread-ai/mxbai-rerank-xsmall-v1";
97
+ readonly 'reranker.overfetch': 5;
98
+ readonly 'reranker.candidateFloor': 80;
97
99
  readonly 'logging.level': "error";
100
+ readonly 'logging.searchMetrics': false;
98
101
  };
99
102
  export declare function readConfigJson(): JsonConfig | null;
103
+ /**
104
+ * Detect a raw (pre-migration) config object that still selects the removed
105
+ * `ollama` embedding provider — explicitly, via a legacy top-level `ollama`
106
+ * block, or implicitly (no `provider` set, but `baseURL`/`model` point at
107
+ * Ollama's defaults) — and throw a `ConfigError` with migration guidance
108
+ * instead of letting it validate under a silently reinterpreted provider.
109
+ * Runs before `migrateRawConfig` so a legacy `ollama.*` block is never
110
+ * converted into an `embedding` block first. Pure — never mutates `raw`.
111
+ */
112
+ export declare function detectFormerOllamaConfig(raw: unknown): void;
100
113
  /**
101
114
  * Migrate a legacy raw config object to the current shape, in place of a
102
115
  * versioned migration system. Pure — returns a new object, never mutates input.
103
116
  *
104
- * - Legacy `ollama.host`/`ollama.model` → `embedding.{baseURL,model}` with
105
- * `provider: 'ollama'` (only when `embedding` is not already present).
106
117
  * - Removed translation keys (`search.autoTranslate`, `search.translateModel`)
107
118
  * are dropped so they neither validate-fail nor take effect.
108
119
  * - Legacy `reranker.provider: 'ollama'` (a dead value — Ollama has no rerank
@@ -110,13 +121,20 @@ export declare function readConfigJson(): JsonConfig | null;
110
121
  * dropped so it re-derives to `/rerank`. Otherwise the tightened enum would
111
122
  * fail validation on existing configs and lock the user out.
112
123
  *
113
- * `ollama` itself is left for the schema to strip. Exported for unit testing.
124
+ * A legacy top-level `ollama` block reaches here only when `detectFormerOllamaConfig`
125
+ * did not already reject it (i.e. an `embedding` block is also present); it is
126
+ * dropped rather than merged. Exported for unit testing.
114
127
  */
115
128
  export declare function migrateRawConfig(raw: unknown): unknown;
116
129
  /**
117
- * Validate a parsed config object against the Zod schema. Unknown keys (e.g.
118
- * the "$schema" editor reference) are stripped. Throws with a caller-friendly
119
- * message on validation failure. Pure — no file I/O.
130
+ * Validate a raw (pre-migration) config object against the Zod schema: first
131
+ * `detectFormerOllamaConfig` (a `ConfigError` on a former-Ollama config takes
132
+ * priority over both migration and the regular Zod enum rejection), then
133
+ * `migrateRawConfig`, then the schema itself. Unknown keys (e.g. the "$schema"
134
+ * editor reference) are stripped. Throws with a caller-friendly message on
135
+ * validation failure. The single entry point for both `readConfigJson` and
136
+ * `PUT /api/config`, so both apply identical rules with an identical message.
137
+ * Pure — no file I/O.
120
138
  */
121
139
  export declare function parseConfig(raw: unknown, source?: string): JsonConfig;
122
140
  /** Build the JSON Schema object generated from the Zod config schema. Pure. */