codesearch-mcp 0.1.0-alpha → 0.2.1-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 (64) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +146 -41
  3. package/dist/app.js +4 -19
  4. package/dist/src/commands/IndexCommand.js +68 -7
  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/db/index.d.ts +1 -1
  14. package/dist/src/db/index.js +2 -1
  15. package/dist/src/db/schema.d.ts +75 -0
  16. package/dist/src/db/schema.js +10 -1
  17. package/dist/src/enriched-prompt.d.ts +8 -0
  18. package/dist/src/enriched-prompt.js +87 -0
  19. package/dist/src/help.js +1 -1
  20. package/dist/src/interfaces/IAstParser.d.ts +18 -2
  21. package/dist/src/interfaces/IDatabase.d.ts +31 -0
  22. package/dist/src/interfaces/ILogger.d.ts +7 -0
  23. package/dist/src/interfaces/ISearcher.d.ts +3 -0
  24. package/dist/src/services/AstChunker.d.ts +11 -0
  25. package/dist/src/services/AstChunker.js +44 -0
  26. package/dist/src/services/AstParserService.d.ts +39 -7
  27. package/dist/src/services/AstParserService.js +504 -104
  28. package/dist/src/services/DatabaseService.d.ts +17 -0
  29. package/dist/src/services/DatabaseService.js +130 -2
  30. package/dist/src/services/EmbedderFactory.d.ts +1 -1
  31. package/dist/src/services/EmbedderFactory.js +2 -5
  32. package/dist/src/services/IndexerService.d.ts +4 -14
  33. package/dist/src/services/IndexerService.js +54 -156
  34. package/dist/src/services/InfinityEmbedder.d.ts +14 -55
  35. package/dist/src/services/InfinityEmbedder.js +30 -141
  36. package/dist/src/services/LoggerService.d.ts +3 -1
  37. package/dist/src/services/LoggerService.js +17 -3
  38. package/dist/src/services/McpServer.js +3 -3
  39. package/dist/src/services/SearcherService.d.ts +39 -4
  40. package/dist/src/services/SearcherService.js +254 -81
  41. package/dist/src/services/WebServerService.d.ts +41 -3
  42. package/dist/src/services/WebServerService.js +90 -2
  43. package/dist/src/services/infinity-provisioning.d.ts +115 -0
  44. package/dist/src/services/infinity-provisioning.js +516 -0
  45. package/dist/src/services/legacy-ollama-index.d.ts +24 -0
  46. package/dist/src/services/legacy-ollama-index.js +61 -0
  47. package/dist/src/services/reranker.d.ts +3 -3
  48. package/dist/src/services/reranker.js +3 -3
  49. package/dist/src/setup.d.ts +37 -0
  50. package/dist/src/setup.js +111 -157
  51. package/dist/src/symbol-name.d.ts +44 -0
  52. package/dist/src/symbol-name.js +68 -0
  53. package/dist/src/symbol-rows.d.ts +12 -0
  54. package/dist/src/symbol-rows.js +38 -0
  55. package/dist/src/tools/FindSymbolTool.js +30 -7
  56. package/dist/src/tools/GetCallersTool.js +20 -5
  57. package/docker/docker-compose.yml +5 -15
  58. package/docs/architecture.svg +103 -0
  59. package/docs/web-dashboard.md +36 -17
  60. package/package.json +1 -6
  61. package/dist/src/commands/BackfillCommand.d.ts +0 -7
  62. package/dist/src/commands/BackfillCommand.js +0 -37
  63. package/dist/src/services/OllamaEmbedder.d.ts +0 -25
  64. package/dist/src/services/OllamaEmbedder.js +0 -111
package/CHANGELOG.md CHANGED
@@ -5,6 +5,45 @@ Format based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.2.1-alpha] - 2026-10-02
9
+
10
+ ### Added
11
+
12
+ - **`find_symbol` indexes every member kind, not just top-level declarations** — class methods (`abstract`, `static`, `async`, generators, `#private`, computed and string-literal names), get/set accessors, constructors, class properties initialised with a function, object-literal methods, and interface/type-literal method signatures all get a `code_symbol` row now, with the dotted `container` of their enclosing scopes (`RepairsService`, `NS.Parser`, `api.handlers`) and a `static` flag. Classes are collected from the whole file — inside namespaces, as class expressions, as `export default class {}`, and inside functions or methods — not only at the top level.
13
+ - **Qualified, quoted and computed lookup** — `find_symbol` accepts `Class.method` to disambiguate same-named members (exact container first, then a `.`-boundary suffix match), a quoted name for string-literal members (`Router.'kebab-name'`), and a computed name (`Stack.[Symbol.iterator]`), alongside the existing bare-name lookup. `find_symbol` falls back to grep whenever the index returns **no hits** (not only when it errors), and marks grep-sourced hits so the caller knows they may be stale.
14
+ - **Qualified call graph** — `code_call.fromSymbol`/`calleeSymbol` now carry the qualified name of the enclosing/resolved declaration (`RepairsService.createRepairOrder`, including constructors and accessors, which previously had no `fromSymbol` at all). `get_callers` accepts a qualified symbol with the same suffix-matching rules as `find_symbol`.
15
+ - **Per-project symbol-index version, enforced full re-index** — `rag-index` without `--reindex` now refuses to run on a project indexed before member indexing (mixed old/new-format rows would silently hide methods from `find_symbol`); `find_symbol`/`get_callers` show a warning when a project in scope is outdated.
16
+
17
+ ### Changed (breaking)
18
+
19
+ - **Requires PostgreSQL 15+** — the symbol index's new unique index relies on `NULLS NOT DISTINCT`, added in PostgreSQL 15. `rag-index` checks the server version before any schema change and refuses on an older server.
20
+ - **Every project must be re-indexed** — run `rag-index --reindex` on each project after upgrading. Old-format rows (no members, bare call-graph names) are incompatible with the new lookup and are cleared by `--reindex`.
21
+
22
+ ## [0.2.0-alpha] - 2026-09-29
23
+
24
+ ### Added
25
+
26
+ - **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.
27
+ - **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.
28
+ - **`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.
29
+ - **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.
30
+ - **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.
31
+ - **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.
32
+ - **`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.
33
+
34
+ ### Changed
35
+
36
+ - **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.
37
+ - **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`.
38
+
39
+ ### Changed (breaking)
40
+
41
+ - **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.
42
+ - **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`.
43
+ - **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.
44
+ - **`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.
45
+ - **`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.
46
+
8
47
  ## [0.1.0-alpha] - 2026-07-12
9
48
 
10
49
  ### Added
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,20 @@ 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
+ ![Architecture](docs/architecture.svg)
23
+
24
+ 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
25
  2. `rag-mcp` exposes an MCP server — Claude calls it automatically when you ask about code
15
26
  3. Incremental — only changed files are re-indexed on subsequent runs
27
+ 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
28
 
17
- **Stack:** TypeScript · Drizzle ORM · PostgreSQL + pgvector · Ollama (bge-m3) · LangChain text splitters · ts-morph
29
+ **Stack:** TypeScript · Drizzle ORM · PostgreSQL + pgvector · Infinity (jina-embeddings-v2-base-code) · LangChain text splitters · ts-morph
18
30
 
19
31
  ## Requirements
20
32
 
21
33
  - Node.js 18+
22
- - Docker (for local DB + Ollama) — or bring your own PostgreSQL with pgvector + Ollama
34
+ - PostgreSQL 15+ with pgvector (the symbol index's unique index relies on `NULLS NOT DISTINCT`, added in PostgreSQL 15)
35
+ - Docker (for local DB + Infinity) — or bring your own PostgreSQL with pgvector + an embedding backend (see [Embedding providers](#embedding-providers))
23
36
 
24
37
  ## Installation
25
38
 
@@ -35,11 +48,19 @@ rag-setup
35
48
 
36
49
  Walks you through:
37
50
  - **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))
51
+ - **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
52
  - **Logging** — choose log level and directory (defaults: `error` level, `~/.config/codesearch-mcp/logs/`)
40
53
 
41
54
  Config is saved to `~/.config/codesearch-mcp/config.json`.
42
55
 
56
+ ## Register with Claude Code
57
+
58
+ ```bash
59
+ claude mcp add codesearch-mcp -s user -- rag-mcp
60
+ ```
61
+
62
+ Then ask Claude anything about your code — it will use the tools below automatically.
63
+
43
64
  ## Index your projects
44
65
 
45
66
  ```bash
@@ -62,14 +83,6 @@ If `workspaceRoot` is an [Nx](https://nx.dev) workspace (detected by `nx.json`),
62
83
 
63
84
  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
85
 
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
86
  ## Web dashboard
74
87
 
75
88
  ```bash
@@ -78,23 +91,23 @@ rag-web # http://127.0.0.1:4319 (--port to change)
78
91
 
79
92
  A local web UI (bound to `127.0.0.1` only) with a schema-driven config editor,
80
93
  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).
94
+ config form shows the applied default for any unset key and suggests dimension-compatible
95
+ models in the model fields (free text is always accepted).
83
96
  See [docs/web-dashboard.md](docs/web-dashboard.md) for the full reference.
84
97
 
85
98
  ## MCP Tools
86
99
 
87
100
  | Tool | Description |
88
101
  |------|-------------|
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 |
102
+ | `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
103
  | `list_projects` | List projects with metadata and chunk counts |
91
- | `find_symbol` | Find a function, class, or type by exact name — faster than semantic search |
104
+ | `find_symbol` | Find a declaration or member by exact name — bare, qualified (`Class.method`), quoted, or computed — faster than semantic search |
92
105
  | `list_files` | Browse the directory tree of a project |
93
106
  | `read_file` | Read a complete file by absolute path |
94
107
  | `read_file_range` | Read a specific line range from an indexed file |
95
108
  | `get_dependencies` | Show what files a given file imports (direct or transitive) |
96
109
  | `get_dependents` | Show which files import a given file (direct or transitive) |
97
- | `get_callers` | Find all call sites of a function or method by exact name |
110
+ | `get_callers` | Find all call sites of a function or method by exact name (bare or qualified) |
98
111
 
99
112
  ## Project metadata
100
113
 
@@ -104,6 +117,7 @@ Add `.ragmeta.json` to a project root to enrich `list_projects` and generate a v
104
117
  {
105
118
  "description": "Main API server",
106
119
  "stack": "NestJS, GraphQL, PostgreSQL",
120
+ "notes": "Uses optimistic locking — see order-service/docs/concurrency.md",
107
121
  "relations": {
108
122
  "consumed_by": ["crm-frontend"],
109
123
  "consumes": ["billing-service", "auth-service"]
@@ -111,7 +125,7 @@ Add `.ragmeta.json` to a project root to enrich `list_projects` and generate a v
111
125
  }
112
126
  ```
113
127
 
114
- After each indexing run an interactive `project-graph.html` is written to the workspace root.
128
+ 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
129
 
116
130
  ## Graph context
117
131
 
@@ -166,6 +180,7 @@ rag-config show # print current config (password redacted)
166
180
  rag-config get <key> # print a single value
167
181
  rag-config set <key> <value> # update a value
168
182
  rag-config reset <key> # remove key (reverts to default)
183
+ rag-config edit # open config in $EDITOR; validates on save (use this to recover an invalid config)
169
184
  ```
170
185
 
171
186
  | Key | Default | Description |
@@ -176,33 +191,40 @@ rag-config reset <key> # remove key (reverts to default)
176
191
  | `db.password` | — | PostgreSQL password |
177
192
  | `db.name` | `rag_<os-username>` | Database name — auto-created on first run |
178
193
  | `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`) |
194
+ | `embedding.provider` | `infinity` | Embedding backend: `infinity` \| `openai-compatible` \| `voyage`. A stored `ollama` value fails validation with migration guidance (see [Embedding providers](#embedding-providers)) |
195
+ | `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
196
  | `embedding.apiKey` | `''` | API key, or `env:VAR_NAME` to read from `.env`/environment. Empty for local servers |
182
- | `embedding.model` | `bge-m3` | Embedding model |
197
+ | `embedding.model` | `jinaai/jina-embeddings-v2-base-code` | Embedding model |
183
198
  | `embedding.dimensions` | (derived) | Vector width override; required for models whose native width exceeds 2000 (Matryoshka). Otherwise derived from model → registry → runtime probe |
184
199
  | `embedding.batchSize` | `32` | Texts per batch embedding request |
185
200
  | `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 |
201
+ | `reranker.enabled` | `true` | Enable cross-encoder reranking; requires `reranker.model` |
202
+ | `reranker.provider` | `openai-compatible` | Rerank backend — `openai-compatible` `/rerank` only (Infinity, TEI, vLLM) |
188
203
  | `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`) |
204
+ | `reranker.model` | `mixedbread-ai/mxbai-rerank-xsmall-v1` | Reranker model name |
205
+ | `reranker.overfetch` | `5` | Candidate multiplier before reranking (`limit × N`) |
206
+ | `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 |
207
+ | `indexing.chunkSize` | `1500` | Max characters per chunk |
208
+ | `indexing.chunkOverlap` | `200` | Overlap between chunks |
209
+ | `indexing.embedBatchSize` | `5` | Parallel embedding requests per batch |
190
210
  | `indexing.astBatchSize` | `3000` | Max files per ts-morph batch — lower if indexing large projects causes OOM |
191
211
  | `indexing.chunkStrategy` | `ast` | How TS/JS files are chunked: `ast` = boundaries aligned to functions/classes, `character` = fixed-size splitter. Re-index to apply |
212
+ | `search.defaultLimit` | `5` | Default result count returned by `search_codebase` |
213
+ | `search.maxLimit` | `20` | Hard cap on result count |
192
214
  | `logging.level` | `error` | Log level written to file: `error` \| `warn` \| `info` \| `debug` |
193
215
  | `logging.dir` | `~/.config/codesearch-mcp/logs` | Base log directory, or `"false"` to disable |
216
+ | `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
217
 
195
218
  ## Embedding providers
196
219
 
197
- `embedding.provider` selects the backend. All four produce vectors the same way; they differ in where they run and which models they offer.
220
+ `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
221
 
199
222
  ### Choosing a provider
200
223
 
201
224
  | Provider | When to use | Needs |
202
225
  |----------|-------------|-------|
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) |
226
+ | `infinity` | Default. Local, best quality **plus a real reranker** in one container; GPU or CPU. | Docker (managed) or a running Infinity server |
227
+ | `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
228
  | `voyage` | Voyage AI cloud, code-specialized embeddings. | Voyage `apiKey` |
207
229
 
208
230
  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 +234,19 @@ Easiest path: `rag-setup` asks for the provider, model, and any API key. Haven't
212
234
  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
235
 
214
236
  ```bash
215
- # Switch provider (example: OpenAI cloud)
237
+ # Switch to OpenAI cloud
216
238
  rag-config set embedding.provider openai-compatible
217
239
  rag-config set embedding.baseURL https://api.openai.com/v1
218
240
  rag-config set embedding.model text-embedding-3-small
219
241
  rag-config set embedding.apiKey env:OPENAI_API_KEY # key lives in .env, not config.json
242
+ rag-config set reranker.enabled false # OpenAI has no /rerank endpoint
243
+ rag-index --reindex
244
+
245
+ # Switch to Voyage AI
246
+ rag-config set embedding.provider voyage
247
+ rag-config set embedding.model voyage-code-3
248
+ rag-config set embedding.apiKey env:VOYAGE_API_KEY # key lives in .env, not config.json
249
+ rag-config set reranker.enabled false # Voyage has no /rerank endpoint
220
250
  rag-index --reindex
221
251
 
222
252
  # Change only the model on the current provider
@@ -224,31 +254,106 @@ rag-config set embedding.model <model-name>
224
254
  rag-index --reindex
225
255
  ```
226
256
 
257
+ ### Breaking change: the `ollama` provider was removed
258
+
259
+ > **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.
260
+ >
261
+ > 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.
262
+ >
263
+ > 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.
264
+
227
265
  Per-provider notes:
228
266
 
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`.
267
+ - **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`).
268
+ - **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)).
269
+ - **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.
270
+ - **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
271
 
234
272
  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
273
 
236
274
  ### Infinity — local embeddings + reranking in one container
237
275
 
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.
276
+ `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.
277
+
278
+ Managed vs. external is decided by a `/health` probe at `embedding.baseURL` (default `http://localhost:7997`) plus the managed compose file — no config flag:
279
+
280
+ - **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.
281
+ - **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.
282
+
283
+ 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
284
 
240
- Managed vs. external is decided by a `/health` probe at `embedding.baseURL` (default `http://localhost:7997`) — no config flag:
285
+ ### Reranker with a cloud or external embedder
241
286
 
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.
287
+ 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
288
 
245
- The same "already running?" probe now also applies to Ollama: a reachable backend defaults `rag-setup`'s "start Docker?" prompt to no.
289
+ **Option A — disable reranking** (simplest, recommended for cloud-only setups):
290
+
291
+ ```bash
292
+ rag-config set reranker.enabled false
293
+ ```
294
+
295
+ **Option B — point the reranker at a separate server** (e.g. a local Infinity or TEI instance while using Voyage/OpenAI for embeddings):
296
+
297
+ ```bash
298
+ rag-config set reranker.baseURL http://localhost:7997
299
+ rag-config set reranker.model mixedbread-ai/mxbai-rerank-xsmall-v1
300
+ # reranker.enabled is already true
301
+ ```
302
+
303
+ The embedding and reranking servers are fully independent — any combination works: Voyage embeddings + local Infinity reranker, OpenAI embeddings + remote TEI reranker, etc.
246
304
 
247
305
  ### Secrets & external embedders
248
306
 
249
307
  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
308
 
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.
309
+ 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.
310
+
311
+ ## Tuning search recall
312
+
313
+ 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:
314
+
315
+ - **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.
316
+ - **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.
317
+ - **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.
318
+
319
+ To size these for **your** hardware, turn on telemetry:
320
+
321
+ ```bash
322
+ rag-config set logging.searchMetrics true
323
+ ```
324
+
325
+ 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:
326
+
327
+ - **`poolSaturated: true`** — more chunks were under the threshold than the window took → recall is being truncated; raise `candidateFloor`/`overfetch`.
328
+ - **`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.
329
+
330
+ ## Index consistency
331
+
332
+ `rag-index` keeps the four index tables (`code_chunk`, `code_symbol`, `code_call`, `code_dependency`) consistent after every run:
333
+
334
+ | Event | What happens |
335
+ |---|---|
336
+ | **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`. |
337
+ | **File matched by `.ragignore`** | Same as deleted — treated as not collected, removed from all four tables. |
338
+ | **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)`. |
339
+ | **`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. |
340
+
341
+ 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.
342
+
343
+ ## Architecture
344
+
345
+ **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.
346
+
347
+ **Design:**
348
+ - **Composition root + manual DI** — `src/container.ts` builds every service once and wires it through constructors; no module-level singletons, no DI framework.
349
+ - **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.
350
+ - **Command pattern** — each CLI mode (`rag-index`, `rag-mcp`, `rag-config`, `rag-web`) is its own `ICommand`; `app.ts` only dispatches.
351
+ - **Registry / Open-Closed** — MCP tools are `IMcpToolHandler` implementations registered in a `Map`; adding a tool never touches `McpServer`.
352
+ - **Fallback chains** — vector dimension: config override → model registry → runtime probe; `find_symbol`/`get_dependents`: indexed table → grep; failed rerank → vector order.
353
+ - **Incremental indexing** — mtime fast path, SHA-256 fallback for clones/transfers, AST-aligned chunk boundaries so declarations aren't split arbitrarily.
354
+ - **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.
355
+
356
+ Full internals (per-file responsibilities, every pattern with file:line references) are documented in `CLAUDE.md`.
252
357
 
253
358
  ## License
254
359
 
package/dist/app.js CHANGED
@@ -42,7 +42,6 @@ const config_1 = require("./src/config");
42
42
  const config_store_1 = require("./src/config-store");
43
43
  const IndexCommand_1 = require("./src/commands/IndexCommand");
44
44
  const McpCommand_1 = require("./src/commands/McpCommand");
45
- const BackfillCommand_1 = require("./src/commands/BackfillCommand");
46
45
  const ConfigCommand_1 = require("./src/commands/ConfigCommand");
47
46
  const WebCommand_1 = require("./src/commands/WebCommand");
48
47
  const setup_1 = require("./src/setup");
@@ -87,9 +86,7 @@ async function main() {
87
86
  const isSetup = argv1 === 'rag-setup' || process.argv[2] === 'setup' || process.argv[2] === 'rag-setup';
88
87
  const isConfig = argv1 === 'rag-config' || process.argv[2] === 'config' || process.argv[2] === 'rag-config';
89
88
  const isWeb = argv1 === 'rag-web' || process.argv[2] === 'web' || process.argv[2] === 'rag-web';
90
- const isBackfill = process.argv[2] === 'backfill' ||
91
- (process.argv[2] === 'rag-index' && process.argv[3] === 'backfill');
92
- const mode = isBackfill ? 'backfill' : isIndex ? 'index' : isMcp ? 'mcp' : isSetup ? 'setup' : isConfig ? 'config' : isWeb ? 'web' : (process.argv[2] ?? '');
89
+ const mode = isIndex ? 'index' : isMcp ? 'mcp' : isSetup ? 'setup' : isConfig ? 'config' : isWeb ? 'web' : (process.argv[2] ?? '');
93
90
  const isBinary = ['rag-index', 'rag-mcp', 'rag-setup', 'rag-config', 'rag-web'].includes(argv1);
94
91
  if (process.argv.includes('--help') || process.argv.includes('-h')) {
95
92
  (0, help_1.printHelp)(isBinary ? argv1 : mode);
@@ -131,27 +128,16 @@ async function main() {
131
128
  process.exit(1);
132
129
  }
133
130
  }
134
- else if (mode === 'backfill') {
135
- const knownBackfillFlags = new Set(['--calls']);
136
- const backfillIdx = process.argv.indexOf('backfill');
137
- const backfillArgs = backfillIdx >= 0 ? process.argv.slice(backfillIdx + 1) : [];
138
- const unknown = backfillArgs.filter(a => !knownBackfillFlags.has(a));
139
- if (unknown.length > 0) {
140
- console.error(`Unexpected argument: ${unknown[0]}\n`);
141
- (0, help_1.printHelp)('rag-index');
142
- process.exit(1);
143
- }
144
- }
145
131
  if (mode === 'setup') {
146
132
  await (0, setup_1.runSetup)();
147
- process.exit(0);
133
+ process.exit(process.exitCode ?? 0);
148
134
  }
149
135
  if (mode === 'config') {
150
136
  await new ConfigCommand_1.ConfigCommand(effectiveArgs).execute();
151
137
  process.exit(0);
152
138
  }
153
139
  // All other modes require a valid config.json
154
- const loggerName = mode === 'index' || mode === 'backfill' ? 'indexer' : mode === 'mcp' ? 'mcp' : mode === 'web' ? 'web' : undefined;
140
+ const loggerName = mode === 'index' ? 'indexer' : mode === 'mcp' ? 'mcp' : mode === 'web' ? 'web' : undefined;
155
141
  let container;
156
142
  try {
157
143
  container = (0, container_1.createContainer)(loggerName);
@@ -176,7 +162,6 @@ async function main() {
176
162
  const factories = {
177
163
  index: () => new IndexCommand_1.IndexCommand(container, argv1),
178
164
  mcp: () => new McpCommand_1.McpCommand(container),
179
- backfill: () => new BackfillCommand_1.BackfillCommand(container),
180
165
  web: () => new WebCommand_1.WebCommand(container, effectiveArgs),
181
166
  };
182
167
  const factory = factories[mode];
@@ -189,6 +174,6 @@ async function main() {
189
174
  }
190
175
  await factory().execute();
191
176
  await container.db.destroy();
192
- process.exit(0);
177
+ process.exit(process.exitCode ?? 0);
193
178
  }
194
179
  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,57 @@ 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
+ const discovered = discovery.discover(config.workspaceRoot);
47
+ const selected = await new project_selector_1.ProjectSelector(config.workspaceRoot).select(discovered, forceSelect);
48
+ if (selected.length === 0)
49
+ return;
50
+ // Expand any selected nested-monorepo unit (e.g. an Nx repo) into its
51
+ // sub-projects; ordinary projects pass through unchanged.
52
+ const projects = (0, MonorepoAwareDiscoveryService_1.toLeafProjects)(selected);
53
+ // A project indexed before member extraction mixes old-format rows (no
54
+ // members, bare calleeSymbol) with new ones if left to an incremental run.
55
+ // Refuse before any backend readiness check, Docker command, or schema
56
+ // work — the same pattern as the legacy-Ollama guard above.
57
+ if (!force) {
58
+ let outdated;
59
+ try {
60
+ outdated = await db.listOutdatedProjects(projects.map(p => p.name));
61
+ }
62
+ catch (err) {
63
+ console.error(`\n${err.message}`);
64
+ process.exitCode = 1;
65
+ return;
66
+ }
67
+ if (outdated.length > 0) {
68
+ console.error(`\nIndexed before member indexing: ${[...outdated].sort().join(', ')} — run \`rag-index --reindex\`.`);
69
+ process.exitCode = 1;
70
+ return;
71
+ }
72
+ }
73
+ // Ensure the local model is served (Infinity only — no-op for cloud), then
23
74
  // resolve the active embedding dimension and reconcile it with index_meta
24
75
  // (recreate code_chunk on a dimension change, clear rows on a model change)
25
76
  // before scanning. A clear error here beats a mid-run vector width mismatch.
@@ -30,18 +81,28 @@ class IndexCommand {
30
81
  }
31
82
  catch (err) {
32
83
  console.error(`\nEmbedding setup failed: ${err.message}`);
84
+ process.exitCode = 1;
33
85
  return;
34
86
  }
35
87
  const controller = new AbortController();
36
88
  const teardown = (0, interrupt_1.setupInterrupt)(controller);
37
89
  try {
38
- const discovered = discovery.discover(config.workspaceRoot);
39
- const selected = await new project_selector_1.ProjectSelector(config.workspaceRoot).select(discovered, forceSelect);
40
- if (selected.length === 0)
90
+ let pruneResult;
91
+ try {
92
+ pruneResult = await indexer.pruneProjects(projects.map(p => p.name));
93
+ }
94
+ catch (err) {
95
+ console.error(`\nPruning failed: ${err.message}`);
96
+ process.exitCode = 1;
41
97
  return;
42
- // Expand any selected nested-monorepo unit (e.g. an Nx repo) into its
43
- // sub-projects; ordinary projects pass through unchanged.
44
- const projects = (0, MonorepoAwareDiscoveryService_1.toLeafProjects)(selected);
98
+ }
99
+ const { removed, failed } = pruneResult;
100
+ for (const name of removed) {
101
+ console.log(`Removed project ${name} (not selected)`);
102
+ }
103
+ if (failed.length > 0) {
104
+ process.exitCode = 1;
105
+ }
45
106
  const filesPerProject = projects.map(p => (0, file_utils_1.collectFiles)(p.path, (0, file_utils_1.loadIgnore)(p.path), p.path));
46
107
  const totalFiles = filesPerProject.reduce((sum, f) => sum + f.length, 0);
47
108
  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;