codesearch-mcp 0.0.18-alpha → 0.1.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 (86) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +75 -6
  3. package/dist/app.js +36 -0
  4. package/dist/src/commands/IndexCommand.js +14 -1
  5. package/dist/src/commands/McpCommand.js +6 -2
  6. package/dist/src/config-store.d.ts +49 -11
  7. package/dist/src/config-store.js +79 -19
  8. package/dist/src/config.d.ts +32 -8
  9. package/dist/src/config.js +84 -20
  10. package/dist/src/constants.d.ts +36 -2
  11. package/dist/src/constants.js +58 -7
  12. package/dist/src/container.js +5 -3
  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 +111 -0
  16. package/dist/src/db/schema.js +15 -1
  17. package/dist/src/help.js +5 -4
  18. package/dist/src/interfaces/IAstParser.d.ts +14 -0
  19. package/dist/src/interfaces/IChunker.d.ts +24 -0
  20. package/dist/src/interfaces/IChunker.js +2 -0
  21. package/dist/src/interfaces/IDatabase.d.ts +14 -0
  22. package/dist/src/interfaces/IEmbedder.d.ts +14 -2
  23. package/dist/src/interfaces/ISearcher.d.ts +1 -0
  24. package/dist/src/interfaces/index.d.ts +2 -1
  25. package/dist/src/owner-path.d.ts +10 -0
  26. package/dist/src/owner-path.js +27 -0
  27. package/dist/src/services/AstChunker.d.ts +31 -0
  28. package/dist/src/services/AstChunker.js +114 -0
  29. package/dist/src/services/AstParserService.d.ts +6 -0
  30. package/dist/src/services/AstParserService.js +31 -2
  31. package/dist/src/services/DatabaseService.d.ts +13 -1
  32. package/dist/src/services/DatabaseService.js +70 -25
  33. package/dist/src/services/EmbedderFactory.d.ts +9 -0
  34. package/dist/src/services/EmbedderFactory.js +29 -0
  35. package/dist/src/services/IndexerService.d.ts +17 -5
  36. package/dist/src/services/IndexerService.js +101 -38
  37. package/dist/src/services/InfinityEmbedder.d.ts +79 -0
  38. package/dist/src/services/InfinityEmbedder.js +199 -0
  39. package/dist/src/services/OllamaEmbedder.d.ts +16 -5
  40. package/dist/src/services/OllamaEmbedder.js +84 -36
  41. package/dist/src/services/OpenAICompatibleEmbedder.d.ts +25 -0
  42. package/dist/src/services/OpenAICompatibleEmbedder.js +75 -0
  43. package/dist/src/services/SearcherService.d.ts +7 -1
  44. package/dist/src/services/SearcherService.js +35 -16
  45. package/dist/src/services/VoyageEmbedder.d.ts +22 -0
  46. package/dist/src/services/VoyageEmbedder.js +72 -0
  47. package/dist/src/services/WebServerService.d.ts +16 -11
  48. package/dist/src/services/WebServerService.js +44 -24
  49. package/dist/src/services/http-retry.d.ts +19 -0
  50. package/dist/src/services/http-retry.js +54 -0
  51. package/dist/src/services/reranker.d.ts +15 -0
  52. package/dist/src/services/reranker.js +44 -0
  53. package/dist/src/setup.d.ts +6 -0
  54. package/dist/src/setup.js +243 -25
  55. package/dist/src/tools/SearchCodebaseTool.js +1 -1
  56. package/dist/src/tools/format-helpers.js +2 -1
  57. package/dist/src/web/dashboard.js +5 -0
  58. package/docker/docker-compose.infinity-gpu.yml +19 -0
  59. package/docker/docker-compose.yml +25 -0
  60. package/package.json +21 -2
  61. package/dist/src/ast.d.ts +0 -2
  62. package/dist/src/ast.js +0 -110
  63. package/dist/src/commands/SetupCommand.d.ts +0 -7
  64. package/dist/src/commands/SetupCommand.js +0 -14
  65. package/dist/src/db/data-source.d.ts +0 -2
  66. package/dist/src/db/data-source.js +0 -13
  67. package/dist/src/db/entities/code-chunk.entity.d.ts +0 -9
  68. package/dist/src/db/entities/code-chunk.entity.js +0 -54
  69. package/dist/src/db/entities/code-dependency.entity.d.ts +0 -9
  70. package/dist/src/db/entities/code-dependency.entity.js +0 -54
  71. package/dist/src/db/entities/code-symbol.entity.d.ts +0 -10
  72. package/dist/src/db/entities/code-symbol.entity.js +0 -59
  73. package/dist/src/db/init.d.ts +0 -2
  74. package/dist/src/db/init.js +0 -62
  75. package/dist/src/env-loader.d.ts +0 -1
  76. package/dist/src/env-loader.js +0 -41
  77. package/dist/src/indexer.d.ts +0 -17
  78. package/dist/src/indexer.js +0 -242
  79. package/dist/src/mcp.d.ts +0 -1
  80. package/dist/src/mcp.js +0 -403
  81. package/dist/src/ollama.d.ts +0 -2
  82. package/dist/src/ollama.js +0 -6
  83. package/dist/src/searcher.d.ts +0 -74
  84. package/dist/src/searcher.js +0 -446
  85. package/dist/vitest.config.d.ts +0 -2
  86. package/dist/vitest.config.js +0 -9
package/CHANGELOG.md CHANGED
@@ -5,6 +5,29 @@ Format based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.1.0-alpha] - 2026-07-12
9
+
10
+ ### Added
11
+
12
+ - **`infinity` embedding provider** — a first-class local backend (`embedding.provider = infinity`) that serves both `/embeddings` and `/rerank` from a single [Infinity](https://github.com/michaelfeil/infinity) container (e.g. `jinaai/jina-embeddings-v2-base-code` + `Alibaba-NLP/gte-reranker-modernbert-base`), so local reranking works out of the box — something Ollama does not provide. Reuses the OpenAI-compatible HTTP path and adds only container-readiness behavior. Selecting `infinity` in `rag-setup` optionally provisions a reranker model and auto-wires `reranker.*` to the same server. Managed provisioning vs. an external/already-running server is decided by a `/health` probe (no config flag) — the same "already running?" probe now also defaults `rag-setup`'s "start Docker?" prompt to no for a reachable Ollama. The managed container prefers an NVIDIA GPU and falls back to CPU, and is recreated when the configured model set changes (Infinity binds models at startup). The managed container also gets `restart: unless-stopped` (survives host reboot) and disables Infinity's model warmup on CPU (a multi-minute benchmark at every boot; kept on for GPU where it is cheap). `jinaai/jina-embeddings-v2-base-code` (768 dims, the Infinity default) and `BAAI/bge-m3` (1024) are added to the model registry so their dimension resolves without a probe.
13
+ - **Reranking is `openai-compatible`-only** — `reranker.provider` no longer accepts `ollama` (Ollama has no rerank API — the previous `/api/rerank` path always 404'd and silently fell back to vector order); the only valid backend is an OpenAI-compatible `/rerank` server (Infinity, TEI, vLLM). The default provider is now `openai-compatible`, and an existing `reranker.provider: 'ollama'` is migrated automatically on read. The embedding provider stays independent of the rerank server, so Ollama embeddings + an Infinity reranker is a valid combination.
14
+ - **Pluggable embedding backends** — the embedder is now selected by `embedding.provider` via a factory, with three implementations: `ollama` (native `/api/embed`, the **backward-compatible default**), `openai-compatible` (one code path covering **OpenAI cloud, Infinity, TEI, vLLM, LM Studio, LocalAI** via a configurable `embedding.baseURL`), and `voyage` (native schema with mandatory `input_type`). To run a faster local backend or a cloud embedder you now edit config, not code.
15
+ - **Batch embedding with retry** — `IEmbedder` gains `embedBatch(texts, inputType?)`; indexing sends one request per batch (`embedding.batchSize`, default 32) instead of one call per chunk, with bounded exponential backoff on HTTP 429/5xx. Makes cloud/Infinity indexing of a whole monorepo practical.
16
+ - **Query/document input-type asymmetry** — `embed`/`embedBatch` take an optional `inputType` (`query` | `document`); search embeds as `query`, indexing as `document`. Voyage forwards it as `input_type`; Ollama and the OpenAI-compatible `/embeddings` endpoint ignore it (OpenAI has no such field).
17
+ - **Flexible vector dimension** — the `code_chunk.embedding` column width now derives from the active model (`embedding.dimensions` override → model registry → one-shot runtime probe) instead of a hardcoded 1024, capped at ≤ 2000 (pgvector HNSW limit); larger cloud models reduce via the Matryoshka `dimensions`/`output_dimension` request parameter.
18
+ - **`index_meta` source-of-truth table + mismatch guard** — a single-row table records `{ provider, model, dimensions }`. `search_codebase` (before embedding a query) and `rag-mcp` (at start) refuse with a clear "run `rag-index --reindex`" error when the configured model disagrees with the indexed one, instead of returning noise from a foreign vector space. A dimension change drops+recreates `code_chunk` at the new width on re-index; a same-dimension model change clears its rows.
19
+ - **API-key secrets via `.env`** — keys live in `~/.config/codesearch-mcp/.env` (chmod 600), loaded into the environment at startup before config is read (a real env var wins over the file), so the client-spawned `rag-mcp` sees them too. Config values support an `env:VAR_NAME` reference; a missing referenced variable fails with a clear error. The dashboard shows `✔ set via <VAR>` for `env:` refs and masks raw keys.
20
+ - **Auto-pull of local Ollama models with live progress** — `rag-index` now pulls the configured Ollama model if it is not present locally (only for `provider: ollama`; a no-op for cloud providers), so switching to (or back to) a local model that was never pulled just works without re-running `rag-setup`. The pull streams progress: a live single-line bar on a TTY (`downloading [██████░░░░] 62% (245.7/393.2 MB)`), de-duplicated status lines otherwise. `rag-setup`'s in-container pull now renders Ollama's native progress bar (allocates a TTY via `docker exec -t` when interactive) and pulls the model you actually chose (previously hardcoded to `bge-m3`).
21
+ - **Cloud providers skip the Ollama container** — when a non-Ollama provider is selected, `rag-setup`'s Docker helper starts only the database service (`docker compose up -d db`) and skips the Ollama wait + model pull, instead of bringing up an unused Ollama container.
22
+ - **Owner-symbol breadcrumb in search results** — each `search_codebase` result now shows a `Symbol:` line naming the declaration the chunk belongs to (`IndexerService.ingestFile`, `collectFiles`, …), so the enclosing class/method is visible without opening the file. Computed at index time into a new nullable `code_chunk.ownerPath` column (added via `ADD COLUMN IF NOT EXISTS`): the deepest declaration containing the chunk's start line wins, so a method beats its enclosing class. Method/constructor/accessor ranges are extracted purely for this breadcrumb and are **not** written to `code_symbol`, so `find_symbol` is unchanged. Module-scope chunks (imports) and rows indexed before this release show no breadcrumb; re-index to populate. No embedding-dimension change.
23
+ - **AST-aware chunking** — TS/JS files are now chunked along top-level declaration boundaries (functions, classes, interfaces, types, exported variables) instead of at fixed character offsets, so a declaration is no longer split across chunks and search-hit line ranges map to coherent units of code. Declarations larger than `indexing.chunkSize` and the gaps between them (imports, top-level code) are sub-split by the recursive splitter, so no chunk exceeds the size limit and no source line is dropped. Reuses the symbol ranges already produced during indexing — no extra parse. New config key `indexing.chunkStrategy` (`ast` \| `character`, default `ast`); set it to `character` to restore the previous fixed-size behavior. Schema files (`.prisma`/`.graphql`/`.gql`) and files without symbol analysis keep the character splitter. Takes effect on the next re-index; no embedding-dimension or DB-schema change.
24
+
25
+ ### Changed (breaking)
26
+
27
+ - **Config restructure** — the `ollama.*` section is replaced by `embedding.*` (`provider`, `baseURL`, `apiKey`, `model`, `dimensions`, `batchSize`); `reranker.*` gains `provider`/`baseURL`/`apiKey` mirroring the embedding taxonomy (Infinity can serve both from one server). **Legacy `ollama.host`/`ollama.model` configs are migrated to `embedding.*` automatically on read**, so existing installs keep working without manual edits.
28
+ - **Query translation removed** — the `IEmbedder.translate` method and the `search.autoTranslate` / `search.translateModel` config keys are gone; queries are embedded as-is. The keys are silently ignored if still present.
29
+ - **Re-index required on model/dimension change** — switching the embedding model, provider, or dimension now requires `rag-index --reindex` (a dimension change recreates `code_chunk`). Staying on Ollama + `bge-m3` keeps dimension 1024, backfills `index_meta`, and forces no re-index. The mismatch guard makes any disagreement loud rather than silently corrupting results.
30
+
8
31
  ## [0.0.18-alpha] - 2026-07-04
9
32
 
10
33
  ### Added
package/README.md CHANGED
@@ -35,7 +35,7 @@ rag-setup
35
35
 
36
36
  Walks you through:
37
37
  - **Database** — enter connection details; optionally starts Docker containers automatically
38
- - **Ollama** — configure API URL
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))
39
39
  - **Logging** — choose log level and directory (defaults: `error` level, `~/.config/codesearch-mcp/logs/`)
40
40
 
41
41
  Config is saved to `~/.config/codesearch-mcp/config.json`.
@@ -118,7 +118,9 @@ After each indexing run an interactive `project-graph.html` is written to the wo
118
118
  Pass `include_graph_context: true` to `search_codebase` to see direct imports and dependents inline with each result — without extra `get_dependencies` / `get_dependents` calls:
119
119
 
120
120
  ```
121
- **File:** src/services/InvoiceService.ts **Line:** 45
121
+ **File:** src/services/InvoiceService.ts
122
+ **Symbol:** InvoiceService.create
123
+ **Line:** 45
122
124
  ...code...
123
125
  **Relevance:** 0.32
124
126
  **Imports:** src/repositories/InvoiceRepo.ts · src/dto/CreateInvoiceDto.ts
@@ -127,6 +129,8 @@ Pass `include_graph_context: true` to `search_codebase` to see direct imports an
127
129
 
128
130
  Useful for immediately understanding a file's role in the dependency graph.
129
131
 
132
+ Each result also shows a **Symbol** breadcrumb — the declaration the chunk belongs to (`InvoiceService.create`, `collectFiles`, …) — so you can see the enclosing class/method without opening the file. It is omitted for module-scope chunks and for projects indexed before this feature (re-index to populate).
133
+
130
134
  ## Excluding files
131
135
 
132
136
  Create `.ragignore` in a project root — same syntax as `.gitignore`:
@@ -172,15 +176,80 @@ rag-config reset <key> # remove key (reverts to default)
172
176
  | `db.password` | — | PostgreSQL password |
173
177
  | `db.name` | `rag_<os-username>` | Database name — auto-created on first run |
174
178
  | `workspaceRoot` | — | Root of your projects directory |
175
- | `ollama.host` | `http://localhost:11434` | Ollama API URL |
176
- | `ollama.model` | `bge-m3` | Embedding model |
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`) |
181
+ | `embedding.apiKey` | `''` | API key, or `env:VAR_NAME` to read from `.env`/environment. Empty for local servers |
182
+ | `embedding.model` | `bge-m3` | Embedding model |
183
+ | `embedding.dimensions` | (derived) | Vector width override; required for models whose native width exceeds 2000 (Matryoshka). Otherwise derived from model → registry → runtime probe |
184
+ | `embedding.batchSize` | `32` | Texts per batch embedding request |
177
185
  | `search.similarityThreshold` | `0.6` | Max cosine distance — raise if search returns nothing |
178
- | `search.autoTranslate` | `false` | Translate non-English queries to English before embedding |
179
- | `search.translateModel` | `llama3.2` | Ollama model for translation (requires `ollama pull llama3.2`) |
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 |
188
+ | `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`) |
180
190
  | `indexing.astBatchSize` | `3000` | Max files per ts-morph batch — lower if indexing large projects causes OOM |
191
+ | `indexing.chunkStrategy` | `ast` | How TS/JS files are chunked: `ast` = boundaries aligned to functions/classes, `character` = fixed-size splitter. Re-index to apply |
181
192
  | `logging.level` | `error` | Log level written to file: `error` \| `warn` \| `info` \| `debug` |
182
193
  | `logging.dir` | `~/.config/codesearch-mcp/logs` | Base log directory, or `"false"` to disable |
183
194
 
195
+ ## Embedding providers
196
+
197
+ `embedding.provider` selects the backend. All four produce vectors the same way; they differ in where they run and which models they offer.
198
+
199
+ ### Choosing a provider
200
+
201
+ | Provider | When to use | Needs |
202
+ |----------|-------------|-------|
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) |
206
+ | `voyage` | Voyage AI cloud, code-specialized embeddings. | Voyage `apiKey` |
207
+
208
+ 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).
209
+
210
+ ### Switching provider or changing the model
211
+
212
+ 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
+
214
+ ```bash
215
+ # Switch provider (example: OpenAI cloud)
216
+ rag-config set embedding.provider openai-compatible
217
+ rag-config set embedding.baseURL https://api.openai.com/v1
218
+ rag-config set embedding.model text-embedding-3-small
219
+ rag-config set embedding.apiKey env:OPENAI_API_KEY # key lives in .env, not config.json
220
+ rag-index --reindex
221
+
222
+ # Change only the model on the current provider
223
+ rag-config set embedding.model <model-name>
224
+ rag-index --reindex
225
+ ```
226
+
227
+ Per-provider notes:
228
+
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`.
233
+
234
+ 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
+
236
+ ### Infinity — local embeddings + reranking in one container
237
+
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.
239
+
240
+ Managed vs. external is decided by a `/health` probe at `embedding.baseURL` (default `http://localhost:7997`) — no config flag:
241
+
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.
244
+
245
+ The same "already running?" probe now also applies to Ollama: a reachable backend defaults `rag-setup`'s "start Docker?" prompt to no.
246
+
247
+ ### Secrets & external embedders
248
+
249
+ 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
+
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.
252
+
184
253
  ## License
185
254
 
186
255
  MIT
package/dist/app.js CHANGED
@@ -34,17 +34,53 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  };
35
35
  })();
36
36
  Object.defineProperty(exports, "__esModule", { value: true });
37
+ const fs = __importStar(require("fs"));
37
38
  const path = __importStar(require("path"));
38
39
  const help_1 = require("./src/help");
39
40
  const container_1 = require("./src/container");
40
41
  const config_1 = require("./src/config");
42
+ const config_store_1 = require("./src/config-store");
41
43
  const IndexCommand_1 = require("./src/commands/IndexCommand");
42
44
  const McpCommand_1 = require("./src/commands/McpCommand");
43
45
  const BackfillCommand_1 = require("./src/commands/BackfillCommand");
44
46
  const ConfigCommand_1 = require("./src/commands/ConfigCommand");
45
47
  const WebCommand_1 = require("./src/commands/WebCommand");
46
48
  const setup_1 = require("./src/setup");
49
+ /**
50
+ * Load `~/.config/codesearch-mcp/.env` into `process.env` before config is read.
51
+ * All five binaries share this file — `rag-mcp` in particular is spawned by the
52
+ * MCP client and does not inherit shell exports. A real `process.env` value
53
+ * always wins over the file. Minimal `KEY=VALUE` parser (no `dotenv` dep):
54
+ * skips blanks and `#` comments, strips one layer of surrounding quotes.
55
+ */
56
+ function loadDotEnv(file) {
57
+ let text;
58
+ try {
59
+ text = fs.readFileSync(file, 'utf-8');
60
+ }
61
+ catch {
62
+ return; // absent / unreadable → no-op
63
+ }
64
+ for (const rawLine of text.split(/\r?\n/)) {
65
+ const line = rawLine.trim();
66
+ if (!line || line.startsWith('#'))
67
+ continue;
68
+ const eq = line.indexOf('=');
69
+ if (eq === -1)
70
+ continue;
71
+ const key = line.slice(0, eq).trim();
72
+ if (!key)
73
+ continue;
74
+ let val = line.slice(eq + 1).trim();
75
+ if (val.length >= 2 && ((val[0] === '"' && val.at(-1) === '"') || (val[0] === "'" && val.at(-1) === "'"))) {
76
+ val = val.slice(1, -1);
77
+ }
78
+ if (!(key in process.env))
79
+ process.env[key] = val; // real env wins
80
+ }
81
+ }
47
82
  async function main() {
83
+ loadDotEnv(config_store_1.ENV_FILE);
48
84
  const argv1 = path.basename(process.argv[1] ?? '');
49
85
  const isIndex = argv1 === 'rag-index' || process.argv[2] === 'index' || process.argv[2] === 'rag-index';
50
86
  const isMcp = argv1 === 'rag-mcp' || process.argv[2] === 'mcp' || process.argv[2] === 'rag-mcp';
@@ -14,11 +14,24 @@ class IndexCommand {
14
14
  this.argv1 = argv1;
15
15
  }
16
16
  async execute() {
17
- const { db, indexer, searcher, config, discovery } = this.container;
17
+ const { db, indexer, searcher, config, discovery, embedder } = this.container;
18
18
  const args = process.argv.slice(this.argv1 === 'rag-index' ? 2 : 3);
19
19
  const force = args.includes('--reindex');
20
20
  const forceSelect = args.includes('--select');
21
21
  await db.initDbWithSchema();
22
+ // Ensure the local model is pulled (Ollama only — no-op for cloud), then
23
+ // resolve the active embedding dimension and reconcile it with index_meta
24
+ // (recreate code_chunk on a dimension change, clear rows on a model change)
25
+ // before scanning. A clear error here beats a mid-run vector width mismatch.
26
+ try {
27
+ await embedder.ensureModelAvailable?.();
28
+ const meta = await indexer.prepareEmbeddingSchema();
29
+ console.log(`Embedding: ${meta.provider} / ${meta.model} (${meta.dimensions} dims)`);
30
+ }
31
+ catch (err) {
32
+ console.error(`\nEmbedding setup failed: ${err.message}`);
33
+ return;
34
+ }
22
35
  const controller = new AbortController();
23
36
  const teardown = (0, interrupt_1.setupInterrupt)(controller);
24
37
  try {
@@ -7,14 +7,18 @@ class McpCommand {
7
7
  this.container = container;
8
8
  }
9
9
  async execute() {
10
- const { db, mcp, logger } = this.container;
10
+ const { db, mcp, searcher, logger } = this.container;
11
11
  let startupError;
12
12
  try {
13
13
  await db.initDb();
14
+ // Refuse to serve searches embedded into a foreign vector space — surface
15
+ // the mismatch through the existing startup-error plumbing so every tool
16
+ // call reports it clearly.
17
+ await searcher.assertIndexMatches();
14
18
  }
15
19
  catch (err) {
16
20
  startupError = err instanceof Error ? err : new Error(String(err));
17
- logger.error('McpCommand: database connection failed', { reason: startupError.message });
21
+ logger.error('McpCommand: startup check failed', { reason: startupError.message });
18
22
  }
19
23
  try {
20
24
  await mcp.start(startupError);
@@ -2,6 +2,7 @@ import { z } from 'zod';
2
2
  export declare const CONFIG_DIR: string;
3
3
  export declare const CONFIG_FILE: string;
4
4
  export declare const SCHEMA_FILE: string;
5
+ export declare const ENV_FILE: string;
5
6
  declare const jsonSchema: z.ZodObject<{
6
7
  db: z.ZodOptional<z.ZodObject<{
7
8
  host: z.ZodOptional<z.ZodString>;
@@ -10,25 +11,41 @@ declare const jsonSchema: z.ZodObject<{
10
11
  password: z.ZodOptional<z.ZodString>;
11
12
  name: z.ZodOptional<z.ZodString>;
12
13
  }, z.core.$strip>>;
13
- ollama: z.ZodOptional<z.ZodObject<{
14
- host: z.ZodOptional<z.ZodString>;
14
+ embedding: z.ZodOptional<z.ZodObject<{
15
+ provider: z.ZodOptional<z.ZodEnum<{
16
+ ollama: "ollama";
17
+ "openai-compatible": "openai-compatible";
18
+ voyage: "voyage";
19
+ infinity: "infinity";
20
+ }>>;
21
+ baseURL: z.ZodOptional<z.ZodString>;
22
+ apiKey: z.ZodOptional<z.ZodString>;
15
23
  model: z.ZodOptional<z.ZodString>;
24
+ dimensions: z.ZodOptional<z.ZodNumber>;
25
+ batchSize: z.ZodOptional<z.ZodNumber>;
16
26
  }, z.core.$strip>>;
17
27
  indexing: z.ZodOptional<z.ZodObject<{
18
28
  chunkSize: z.ZodOptional<z.ZodNumber>;
19
29
  chunkOverlap: z.ZodOptional<z.ZodNumber>;
20
30
  embedBatchSize: z.ZodOptional<z.ZodNumber>;
21
31
  astBatchSize: z.ZodOptional<z.ZodNumber>;
32
+ chunkStrategy: z.ZodOptional<z.ZodEnum<{
33
+ ast: "ast";
34
+ character: "character";
35
+ }>>;
22
36
  }, z.core.$strip>>;
23
37
  search: z.ZodOptional<z.ZodObject<{
24
38
  similarityThreshold: z.ZodOptional<z.ZodNumber>;
25
39
  defaultLimit: z.ZodOptional<z.ZodNumber>;
26
40
  maxLimit: z.ZodOptional<z.ZodNumber>;
27
- autoTranslate: z.ZodOptional<z.ZodBoolean>;
28
- translateModel: z.ZodOptional<z.ZodString>;
29
41
  }, z.core.$strip>>;
30
42
  reranker: z.ZodOptional<z.ZodObject<{
31
43
  enabled: z.ZodOptional<z.ZodBoolean>;
44
+ provider: z.ZodOptional<z.ZodEnum<{
45
+ "openai-compatible": "openai-compatible";
46
+ }>>;
47
+ baseURL: z.ZodOptional<z.ZodString>;
48
+ apiKey: z.ZodOptional<z.ZodString>;
32
49
  model: z.ZodOptional<z.ZodString>;
33
50
  url: z.ZodOptional<z.ZodString>;
34
51
  overfetch: z.ZodOptional<z.ZodNumber>;
@@ -52,29 +69,50 @@ export declare const ALL_KEYS: string[];
52
69
  * `resolvedSchema` in `src/config.ts`, whose `.default(...)` calls reference
53
70
  * these values — so the runtime default and the dashboard-displayed default
54
71
  * cannot diverge. `as const` keeps the literal types so those `.default()`
55
- * lookups stay type-safe. Keys omitted here have no static default (required
56
- * fields like db.host, or values derived at runtime like db.name → rag_<user>
57
- * and reranker.url → ${ollama.host}/api/rerank).
72
+ * lookups stay type-safe. Keys omitted here have no static default: required
73
+ * fields (db.host/user/password) and values derived at runtime — db.name →
74
+ * rag_<user>, and the provider-dependent URLs (embedding.baseURL,
75
+ * reranker.baseURL, reranker.url), which are computed in the `Config`
76
+ * constructor per provider rather than through a schema `.default(...)`.
58
77
  */
59
78
  export declare const CONFIG_DEFAULTS: {
60
79
  readonly 'db.port': 5433;
61
- readonly 'ollama.host': "http://localhost:11434";
62
- readonly 'ollama.model': string;
80
+ readonly 'embedding.provider': "ollama";
81
+ readonly 'embedding.apiKey': "";
82
+ readonly 'embedding.model': "bge-m3";
83
+ readonly 'embedding.batchSize': 32;
63
84
  readonly 'indexing.chunkSize': 1500;
64
85
  readonly 'indexing.chunkOverlap': 200;
65
86
  readonly 'indexing.embedBatchSize': 5;
66
87
  readonly 'indexing.astBatchSize': 3000;
88
+ readonly 'indexing.chunkStrategy': "ast";
67
89
  readonly 'search.similarityThreshold': 0.6;
68
90
  readonly 'search.defaultLimit': 5;
69
91
  readonly 'search.maxLimit': 20;
70
- readonly 'search.autoTranslate': false;
71
- readonly 'search.translateModel': "llama3.2";
72
92
  readonly 'reranker.enabled': false;
93
+ readonly 'reranker.provider': "openai-compatible";
94
+ readonly 'reranker.apiKey': "";
73
95
  readonly 'reranker.model': "";
74
96
  readonly 'reranker.overfetch': 3;
75
97
  readonly 'logging.level': "error";
76
98
  };
77
99
  export declare function readConfigJson(): JsonConfig | null;
100
+ /**
101
+ * Migrate a legacy raw config object to the current shape, in place of a
102
+ * versioned migration system. Pure — returns a new object, never mutates input.
103
+ *
104
+ * - Legacy `ollama.host`/`ollama.model` → `embedding.{baseURL,model}` with
105
+ * `provider: 'ollama'` (only when `embedding` is not already present).
106
+ * - Removed translation keys (`search.autoTranslate`, `search.translateModel`)
107
+ * are dropped so they neither validate-fail nor take effect.
108
+ * - Legacy `reranker.provider: 'ollama'` (a dead value — Ollama has no rerank
109
+ * API) is rewritten to `openai-compatible`, and a stale `/api/rerank` URL is
110
+ * dropped so it re-derives to `/rerank`. Otherwise the tightened enum would
111
+ * fail validation on existing configs and lock the user out.
112
+ *
113
+ * `ollama` itself is left for the schema to strip. Exported for unit testing.
114
+ */
115
+ export declare function migrateRawConfig(raw: unknown): unknown;
78
116
  /**
79
117
  * Validate a parsed config object against the Zod schema. Unknown keys (e.g.
80
118
  * the "$schema" editor reference) are stripped. Throws with a caller-friendly
@@ -33,8 +33,9 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.CONFIG_DEFAULTS = exports.ALL_KEYS = exports.SCHEMA_FILE = exports.CONFIG_FILE = exports.CONFIG_DIR = void 0;
36
+ exports.CONFIG_DEFAULTS = exports.ALL_KEYS = exports.ENV_FILE = exports.SCHEMA_FILE = exports.CONFIG_FILE = exports.CONFIG_DIR = void 0;
37
37
  exports.readConfigJson = readConfigJson;
38
+ exports.migrateRawConfig = migrateRawConfig;
38
39
  exports.parseConfig = parseConfig;
39
40
  exports.buildJsonSchema = buildJsonSchema;
40
41
  exports.writeConfigJson = writeConfigJson;
@@ -50,6 +51,7 @@ const constants_1 = require("./constants");
50
51
  exports.CONFIG_DIR = path.join(os.homedir(), '.config', 'codesearch-mcp');
51
52
  exports.CONFIG_FILE = path.join(exports.CONFIG_DIR, 'config.json');
52
53
  exports.SCHEMA_FILE = path.join(exports.CONFIG_DIR, 'config.schema.json');
54
+ exports.ENV_FILE = path.join(exports.CONFIG_DIR, '.env');
53
55
  // Relative $schema reference written into config.json so editors resolve the
54
56
  // sibling schema file for autocomplete / inline validation.
55
57
  const SCHEMA_REF = './config.schema.json';
@@ -63,27 +65,33 @@ const jsonSchema = zod_1.z.object({
63
65
  password: zod_1.z.string().describe('PostgreSQL password').optional(),
64
66
  name: zod_1.z.string().describe('Database name; defaults to rag_<os-username>, auto-created on first run').optional(),
65
67
  }).optional(),
66
- ollama: zod_1.z.object({
67
- host: zod_1.z.string().describe('Ollama API URL (default http://localhost:11434)').optional(),
68
+ embedding: zod_1.z.object({
69
+ provider: zod_1.z.enum(['ollama', 'openai-compatible', 'voyage', 'infinity']).describe('Embedding backend (default ollama)').optional(),
70
+ baseURL: zod_1.z.string().describe('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').optional(),
71
+ apiKey: zod_1.z.string().describe('API key. Use env:VAR_NAME to read from ~/.config/codesearch-mcp/.env or the environment (recommended). Empty for local servers.').optional(),
68
72
  model: zod_1.z.string().describe('Embedding model (default bge-m3)').optional(),
73
+ dimensions: zod_1.z.number().int().describe('Vector width override; required for models whose native width exceeds 2000 (Matryoshka reduction). Omit to derive from the model/registry/probe.').optional(),
74
+ batchSize: zod_1.z.number().int().describe('Texts per batch embedding request (default 32)').optional(),
69
75
  }).optional(),
70
76
  indexing: zod_1.z.object({
71
77
  chunkSize: zod_1.z.number().int().describe('Max chars per chunk (default 1500)').optional(),
72
78
  chunkOverlap: zod_1.z.number().int().describe('Overlap between chunks (default 200)').optional(),
73
- embedBatchSize: zod_1.z.number().int().describe('Parallel embeddings per batch (default 5)').optional(),
79
+ embedBatchSize: zod_1.z.number().int().describe('Rows per DB-insert batch when writing chunks (default 5). Embedding request size is embedding.batchSize.').optional(),
74
80
  astBatchSize: zod_1.z.number().int().describe('Max files per ts-morph batch; larger projects skip dependency resolution (default 3000)').optional(),
81
+ chunkStrategy: zod_1.z.enum(['ast', 'character']).describe('How TS/JS files are chunked: ast = symbol-aligned boundaries, character = fixed-size recursive splitter (default ast)').optional(),
75
82
  }).optional(),
76
83
  search: zod_1.z.object({
77
84
  similarityThreshold: zod_1.z.number().min(0).max(1).describe('Max cosine distance 0–1; lower = stricter (default 0.6)').optional(),
78
85
  defaultLimit: zod_1.z.number().int().describe('Default result count (default 5)').optional(),
79
86
  maxLimit: zod_1.z.number().int().describe('Hard cap on results (default 20)').optional(),
80
- autoTranslate: zod_1.z.boolean().describe('Translate non-English queries before embedding').optional(),
81
- translateModel: zod_1.z.string().describe('Ollama model for translation (default llama3.2)').optional(),
82
87
  }).optional(),
83
88
  reranker: zod_1.z.object({
84
89
  enabled: zod_1.z.boolean().describe('Enable cross-encoder reranking; requires reranker.model').optional(),
90
+ provider: zod_1.z.enum(['openai-compatible']).describe('Rerank backend. Only openai-compatible /rerank servers (Infinity, TEI, vLLM) — Ollama has no rerank API.').optional(),
91
+ baseURL: zod_1.z.string().describe('Rerank server base URL (defaults to embedding.baseURL — Infinity serves both).').optional(),
92
+ apiKey: zod_1.z.string().describe('API key, or env:VAR_NAME. Empty for local servers.').optional(),
85
93
  model: zod_1.z.string().describe('Reranker model name (e.g. qllama/bge-reranker-v2-m3)').optional(),
86
- url: zod_1.z.string().describe('Full URL of the rerank endpoint (default ${ollama.host}/api/rerank)').optional(),
94
+ url: zod_1.z.string().describe('Full URL of the rerank endpoint (default derived from provider + baseURL)').optional(),
87
95
  overfetch: zod_1.z.number().int().min(1).max(20).describe('Candidate multiplier before reranking (limit × N, default 3)').optional(),
88
96
  }).optional(),
89
97
  logging: zod_1.z.object({
@@ -98,18 +106,24 @@ const FIELD_TYPES = {
98
106
  'db.user': 'string',
99
107
  'db.password': 'string',
100
108
  'db.name': 'string',
101
- 'ollama.host': 'string',
102
- 'ollama.model': 'string',
109
+ 'embedding.provider': 'string',
110
+ 'embedding.baseURL': 'string',
111
+ 'embedding.apiKey': 'string',
112
+ 'embedding.model': 'string',
113
+ 'embedding.dimensions': 'number',
114
+ 'embedding.batchSize': 'number',
103
115
  'indexing.chunkSize': 'number',
104
116
  'indexing.chunkOverlap': 'number',
105
117
  'indexing.embedBatchSize': 'number',
106
118
  'indexing.astBatchSize': 'number',
119
+ 'indexing.chunkStrategy': 'string',
107
120
  'search.similarityThreshold': 'number',
108
121
  'search.defaultLimit': 'number',
109
122
  'search.maxLimit': 'number',
110
- 'search.autoTranslate': 'boolean',
111
- 'search.translateModel': 'string',
112
123
  'reranker.enabled': 'boolean',
124
+ 'reranker.provider': 'string',
125
+ 'reranker.baseURL': 'string',
126
+ 'reranker.apiKey': 'string',
113
127
  'reranker.model': 'string',
114
128
  'reranker.url': 'string',
115
129
  'reranker.overfetch': 'number',
@@ -124,24 +138,29 @@ exports.ALL_KEYS = Object.keys(FIELD_TYPES);
124
138
  * `resolvedSchema` in `src/config.ts`, whose `.default(...)` calls reference
125
139
  * these values — so the runtime default and the dashboard-displayed default
126
140
  * cannot diverge. `as const` keeps the literal types so those `.default()`
127
- * lookups stay type-safe. Keys omitted here have no static default (required
128
- * fields like db.host, or values derived at runtime like db.name → rag_<user>
129
- * and reranker.url → ${ollama.host}/api/rerank).
141
+ * lookups stay type-safe. Keys omitted here have no static default: required
142
+ * fields (db.host/user/password) and values derived at runtime — db.name →
143
+ * rag_<user>, and the provider-dependent URLs (embedding.baseURL,
144
+ * reranker.baseURL, reranker.url), which are computed in the `Config`
145
+ * constructor per provider rather than through a schema `.default(...)`.
130
146
  */
131
147
  exports.CONFIG_DEFAULTS = {
132
148
  'db.port': 5433,
133
- 'ollama.host': 'http://localhost:11434',
134
- 'ollama.model': constants_1.DEFAULT_EMBED_MODEL,
149
+ 'embedding.provider': 'ollama',
150
+ 'embedding.apiKey': '',
151
+ 'embedding.model': constants_1.DEFAULT_EMBED_MODEL,
152
+ 'embedding.batchSize': 32,
135
153
  'indexing.chunkSize': 1500,
136
154
  'indexing.chunkOverlap': 200,
137
155
  'indexing.embedBatchSize': 5,
138
156
  'indexing.astBatchSize': 3000,
157
+ 'indexing.chunkStrategy': 'ast',
139
158
  'search.similarityThreshold': 0.6,
140
159
  'search.defaultLimit': 5,
141
160
  'search.maxLimit': 20,
142
- 'search.autoTranslate': false,
143
- 'search.translateModel': 'llama3.2',
144
161
  'reranker.enabled': false,
162
+ 'reranker.provider': 'openai-compatible',
163
+ 'reranker.apiKey': '',
145
164
  'reranker.model': '',
146
165
  'reranker.overfetch': 3,
147
166
  'logging.level': 'error',
@@ -164,7 +183,48 @@ function readConfigJson() {
164
183
  throw new Error(`Invalid JSON in ${exports.CONFIG_FILE}: ${e.message}\n` +
165
184
  `Fix the file manually or delete it and run: rag-setup`);
166
185
  }
167
- return parseConfig(parsed, exports.CONFIG_FILE);
186
+ return parseConfig(migrateRawConfig(parsed), exports.CONFIG_FILE);
187
+ }
188
+ /**
189
+ * Migrate a legacy raw config object to the current shape, in place of a
190
+ * versioned migration system. Pure — returns a new object, never mutates input.
191
+ *
192
+ * - Legacy `ollama.host`/`ollama.model` → `embedding.{baseURL,model}` with
193
+ * `provider: 'ollama'` (only when `embedding` is not already present).
194
+ * - Removed translation keys (`search.autoTranslate`, `search.translateModel`)
195
+ * are dropped so they neither validate-fail nor take effect.
196
+ * - Legacy `reranker.provider: 'ollama'` (a dead value — Ollama has no rerank
197
+ * API) is rewritten to `openai-compatible`, and a stale `/api/rerank` URL is
198
+ * dropped so it re-derives to `/rerank`. Otherwise the tightened enum would
199
+ * fail validation on existing configs and lock the user out.
200
+ *
201
+ * `ollama` itself is left for the schema to strip. Exported for unit testing.
202
+ */
203
+ function migrateRawConfig(raw) {
204
+ if (raw == null || typeof raw !== 'object')
205
+ return raw;
206
+ const obj = JSON.parse(JSON.stringify(raw));
207
+ const legacyOllama = obj.ollama;
208
+ if (!obj.embedding && legacyOllama && typeof legacyOllama === 'object') {
209
+ const embedding = { provider: 'ollama' };
210
+ if (typeof legacyOllama.host === 'string')
211
+ embedding.baseURL = legacyOllama.host;
212
+ if (typeof legacyOllama.model === 'string')
213
+ embedding.model = legacyOllama.model;
214
+ obj.embedding = embedding;
215
+ }
216
+ delete obj.ollama;
217
+ if (obj.search && typeof obj.search === 'object') {
218
+ delete obj.search.autoTranslate;
219
+ delete obj.search.translateModel;
220
+ }
221
+ if (obj.reranker && typeof obj.reranker === 'object') {
222
+ if (obj.reranker.provider === 'ollama')
223
+ obj.reranker.provider = 'openai-compatible';
224
+ if (typeof obj.reranker.url === 'string' && /\/api\/rerank\/?(\?.*)?$/.test(obj.reranker.url))
225
+ delete obj.reranker.url;
226
+ }
227
+ return obj;
168
228
  }
169
229
  /**
170
230
  * Validate a parsed config object against the Zod schema. Unknown keys (e.g.
@@ -1,6 +1,14 @@
1
1
  export declare class ConfigError extends Error {
2
2
  constructor(message: string);
3
3
  }
4
+ /**
5
+ * Resolve a config value that may reference an environment variable. A value of
6
+ * the form `env:VAR_NAME` resolves to `process.env.VAR_NAME`; any other string
7
+ * (including empty) is returned verbatim. A referenced-but-unset variable throws
8
+ * a clear error naming the variable and the `.env` file. Pure aside from the
9
+ * injected env map. See `src/app.ts` for how `.env` is loaded into the process.
10
+ */
11
+ export declare function resolveEnvRef(value: string, env?: NodeJS.ProcessEnv): string;
4
12
  export declare class Config {
5
13
  private readonly data;
6
14
  get db(): {
@@ -10,26 +18,42 @@ export declare class Config {
10
18
  password: string;
11
19
  database: string;
12
20
  };
13
- get ollama(): {
14
- host: string;
21
+ /**
22
+ * Embedding config. `apiKey` is returned **raw** (possibly an `env:VAR` ref) —
23
+ * callers resolve it lazily via `resolveEnvRef` at the moment they build a
24
+ * request, so an unset referenced variable fails only when the key is actually
25
+ * needed (during embed/search) rather than crashing container construction and
26
+ * locking the user out of `rag-web`/`rag-mcp`.
27
+ */
28
+ get embedding(): {
29
+ provider: "ollama" | "openai-compatible" | "voyage" | "infinity";
30
+ baseURL: string;
31
+ apiKey: string;
32
+ model: string;
33
+ dimensions: number | null;
34
+ batchSize: number;
35
+ };
36
+ /** Reranker config. `apiKey` is raw — resolve lazily with `resolveEnvRef`. */
37
+ get reranker(): {
38
+ enabled: boolean;
39
+ provider: "openai-compatible";
40
+ baseURL: string;
41
+ apiKey: string;
15
42
  model: string;
16
- rerankerModel: string;
17
- rerankerUrl: string;
43
+ url: string;
44
+ overfetch: number;
18
45
  };
19
46
  get indexer(): {
20
47
  chunkSize: number;
21
48
  chunkOverlap: number;
22
49
  batchSize: number;
23
50
  astBatchSize: number;
51
+ chunkStrategy: "ast" | "character";
24
52
  };
25
53
  get search(): {
26
54
  similarityThreshold: number;
27
55
  defaultLimit: number;
28
56
  maxLimit: number;
29
- autoTranslate: boolean;
30
- translateModel: string;
31
- rerankerEnabled: boolean;
32
- rerankerOverfetch: number;
33
57
  };
34
58
  get logging(): {
35
59
  level: "error" | "debug" | "info" | "warn";