codesearch-mcp 0.0.18-alpha → 0.2.0-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/CHANGELOG.md +47 -1
  2. package/README.md +194 -23
  3. package/dist/app.js +38 -2
  4. package/dist/src/commands/IndexCommand.js +55 -1
  5. package/dist/src/commands/McpCommand.js +6 -2
  6. package/dist/src/config-error.d.ts +7 -0
  7. package/dist/src/config-error.js +14 -0
  8. package/dist/src/config-store.d.ts +73 -17
  9. package/dist/src/config-store.js +142 -27
  10. package/dist/src/config.d.ts +36 -11
  11. package/dist/src/config.js +89 -27
  12. package/dist/src/constants.d.ts +42 -2
  13. package/dist/src/constants.js +61 -7
  14. package/dist/src/container.js +5 -3
  15. package/dist/src/db/index.d.ts +1 -1
  16. package/dist/src/db/index.js +2 -1
  17. package/dist/src/db/schema.d.ts +111 -0
  18. package/dist/src/db/schema.js +15 -1
  19. package/dist/src/help.js +6 -5
  20. package/dist/src/interfaces/IAstParser.d.ts +14 -0
  21. package/dist/src/interfaces/IChunker.d.ts +24 -0
  22. package/dist/src/interfaces/IChunker.js +2 -0
  23. package/dist/src/interfaces/IDatabase.d.ts +31 -0
  24. package/dist/src/interfaces/IEmbedder.d.ts +14 -2
  25. package/dist/src/interfaces/ILogger.d.ts +7 -0
  26. package/dist/src/interfaces/ISearcher.d.ts +1 -0
  27. package/dist/src/interfaces/index.d.ts +2 -1
  28. package/dist/src/owner-path.d.ts +10 -0
  29. package/dist/src/owner-path.js +27 -0
  30. package/dist/src/services/AstChunker.d.ts +42 -0
  31. package/dist/src/services/AstChunker.js +158 -0
  32. package/dist/src/services/AstParserService.d.ts +6 -0
  33. package/dist/src/services/AstParserService.js +31 -2
  34. package/dist/src/services/DatabaseService.d.ts +17 -1
  35. package/dist/src/services/DatabaseService.js +116 -25
  36. package/dist/src/services/EmbedderFactory.d.ts +9 -0
  37. package/dist/src/services/EmbedderFactory.js +26 -0
  38. package/dist/src/services/IndexerService.d.ts +21 -5
  39. package/dist/src/services/IndexerService.js +127 -44
  40. package/dist/src/services/InfinityEmbedder.d.ts +38 -0
  41. package/dist/src/services/InfinityEmbedder.js +88 -0
  42. package/dist/src/services/LoggerService.d.ts +3 -1
  43. package/dist/src/services/LoggerService.js +17 -3
  44. package/dist/src/services/OpenAICompatibleEmbedder.d.ts +25 -0
  45. package/dist/src/services/OpenAICompatibleEmbedder.js +75 -0
  46. package/dist/src/services/SearcherService.d.ts +21 -1
  47. package/dist/src/services/SearcherService.js +129 -28
  48. package/dist/src/services/VoyageEmbedder.d.ts +22 -0
  49. package/dist/src/services/VoyageEmbedder.js +72 -0
  50. package/dist/src/services/WebServerService.d.ts +57 -14
  51. package/dist/src/services/WebServerService.js +134 -26
  52. package/dist/src/services/http-retry.d.ts +19 -0
  53. package/dist/src/services/http-retry.js +54 -0
  54. package/dist/src/services/infinity-provisioning.d.ts +115 -0
  55. package/dist/src/services/infinity-provisioning.js +516 -0
  56. package/dist/src/services/legacy-ollama-index.d.ts +24 -0
  57. package/dist/src/services/legacy-ollama-index.js +61 -0
  58. package/dist/src/services/reranker.d.ts +15 -0
  59. package/dist/src/services/reranker.js +44 -0
  60. package/dist/src/setup.d.ts +43 -0
  61. package/dist/src/setup.js +227 -55
  62. package/dist/src/tools/SearchCodebaseTool.js +1 -1
  63. package/dist/src/tools/format-helpers.js +2 -1
  64. package/dist/src/web/dashboard.js +5 -0
  65. package/docker/docker-compose.infinity-gpu.yml +19 -0
  66. package/docker/docker-compose.yml +20 -5
  67. package/docs/web-dashboard.md +36 -17
  68. package/package.json +18 -3
  69. package/dist/src/ast.d.ts +0 -2
  70. package/dist/src/ast.js +0 -110
  71. package/dist/src/commands/SetupCommand.d.ts +0 -7
  72. package/dist/src/commands/SetupCommand.js +0 -14
  73. package/dist/src/db/data-source.d.ts +0 -2
  74. package/dist/src/db/data-source.js +0 -13
  75. package/dist/src/db/entities/code-chunk.entity.d.ts +0 -9
  76. package/dist/src/db/entities/code-chunk.entity.js +0 -54
  77. package/dist/src/db/entities/code-dependency.entity.d.ts +0 -9
  78. package/dist/src/db/entities/code-dependency.entity.js +0 -54
  79. package/dist/src/db/entities/code-symbol.entity.d.ts +0 -10
  80. package/dist/src/db/entities/code-symbol.entity.js +0 -59
  81. package/dist/src/db/init.d.ts +0 -2
  82. package/dist/src/db/init.js +0 -62
  83. package/dist/src/env-loader.d.ts +0 -1
  84. package/dist/src/env-loader.js +0 -41
  85. package/dist/src/indexer.d.ts +0 -17
  86. package/dist/src/indexer.js +0 -242
  87. package/dist/src/mcp.d.ts +0 -1
  88. package/dist/src/mcp.js +0 -403
  89. package/dist/src/ollama.d.ts +0 -2
  90. package/dist/src/ollama.js +0 -6
  91. package/dist/src/searcher.d.ts +0 -74
  92. package/dist/src/searcher.js +0 -446
  93. package/dist/src/services/OllamaEmbedder.d.ts +0 -14
  94. package/dist/src/services/OllamaEmbedder.js +0 -63
  95. package/dist/vitest.config.d.ts +0 -2
  96. package/dist/vitest.config.js +0 -9
@@ -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,28 +11,44 @@ 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
+ "openai-compatible": "openai-compatible";
17
+ voyage: "voyage";
18
+ infinity: "infinity";
19
+ }>>;
20
+ baseURL: z.ZodOptional<z.ZodString>;
21
+ apiKey: z.ZodOptional<z.ZodString>;
15
22
  model: z.ZodOptional<z.ZodString>;
23
+ dimensions: z.ZodOptional<z.ZodNumber>;
24
+ batchSize: z.ZodOptional<z.ZodNumber>;
16
25
  }, z.core.$strip>>;
17
26
  indexing: z.ZodOptional<z.ZodObject<{
18
27
  chunkSize: z.ZodOptional<z.ZodNumber>;
19
28
  chunkOverlap: z.ZodOptional<z.ZodNumber>;
20
29
  embedBatchSize: z.ZodOptional<z.ZodNumber>;
21
30
  astBatchSize: z.ZodOptional<z.ZodNumber>;
31
+ chunkStrategy: z.ZodOptional<z.ZodEnum<{
32
+ ast: "ast";
33
+ character: "character";
34
+ }>>;
22
35
  }, z.core.$strip>>;
23
36
  search: z.ZodOptional<z.ZodObject<{
24
37
  similarityThreshold: z.ZodOptional<z.ZodNumber>;
25
38
  defaultLimit: z.ZodOptional<z.ZodNumber>;
26
39
  maxLimit: z.ZodOptional<z.ZodNumber>;
27
- autoTranslate: z.ZodOptional<z.ZodBoolean>;
28
- translateModel: z.ZodOptional<z.ZodString>;
29
40
  }, z.core.$strip>>;
30
41
  reranker: z.ZodOptional<z.ZodObject<{
31
42
  enabled: z.ZodOptional<z.ZodBoolean>;
43
+ provider: z.ZodOptional<z.ZodEnum<{
44
+ "openai-compatible": "openai-compatible";
45
+ }>>;
46
+ baseURL: z.ZodOptional<z.ZodString>;
47
+ apiKey: z.ZodOptional<z.ZodString>;
32
48
  model: z.ZodOptional<z.ZodString>;
33
49
  url: z.ZodOptional<z.ZodString>;
34
50
  overfetch: z.ZodOptional<z.ZodNumber>;
51
+ candidateFloor: z.ZodOptional<z.ZodNumber>;
35
52
  }, z.core.$strip>>;
36
53
  logging: z.ZodOptional<z.ZodObject<{
37
54
  level: z.ZodOptional<z.ZodEnum<{
@@ -41,6 +58,7 @@ declare const jsonSchema: z.ZodObject<{
41
58
  warn: "warn";
42
59
  }>>;
43
60
  dir: z.ZodOptional<z.ZodString>;
61
+ searchMetrics: z.ZodOptional<z.ZodBoolean>;
44
62
  }, z.core.$strip>>;
45
63
  workspaceRoot: z.ZodOptional<z.ZodString>;
46
64
  }, z.core.$strip>;
@@ -52,33 +70,71 @@ export declare const ALL_KEYS: string[];
52
70
  * `resolvedSchema` in `src/config.ts`, whose `.default(...)` calls reference
53
71
  * these values — so the runtime default and the dashboard-displayed default
54
72
  * 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).
73
+ * lookups stay type-safe. Keys omitted here have no static default: required
74
+ * fields (db.host/user/password) and values derived at runtime — db.name →
75
+ * rag_<user>, and the provider-dependent URLs (embedding.baseURL,
76
+ * reranker.baseURL, reranker.url), which are computed in the `Config`
77
+ * constructor per provider rather than through a schema `.default(...)`.
58
78
  */
59
79
  export declare const CONFIG_DEFAULTS: {
60
80
  readonly 'db.port': 5433;
61
- readonly 'ollama.host': "http://localhost:11434";
62
- readonly 'ollama.model': string;
81
+ readonly 'embedding.provider': "infinity";
82
+ readonly 'embedding.apiKey': "";
83
+ readonly 'embedding.model': "jinaai/jina-embeddings-v2-base-code";
84
+ readonly 'embedding.batchSize': 32;
63
85
  readonly 'indexing.chunkSize': 1500;
64
86
  readonly 'indexing.chunkOverlap': 200;
65
87
  readonly 'indexing.embedBatchSize': 5;
66
88
  readonly 'indexing.astBatchSize': 3000;
89
+ readonly 'indexing.chunkStrategy': "ast";
67
90
  readonly 'search.similarityThreshold': 0.6;
68
91
  readonly 'search.defaultLimit': 5;
69
92
  readonly 'search.maxLimit': 20;
70
- readonly 'search.autoTranslate': false;
71
- readonly 'search.translateModel': "llama3.2";
72
- readonly 'reranker.enabled': false;
73
- readonly 'reranker.model': "";
74
- readonly 'reranker.overfetch': 3;
93
+ readonly 'reranker.enabled': true;
94
+ readonly 'reranker.provider': "openai-compatible";
95
+ readonly 'reranker.apiKey': "";
96
+ readonly 'reranker.model': "mixedbread-ai/mxbai-rerank-xsmall-v1";
97
+ readonly 'reranker.overfetch': 5;
98
+ readonly 'reranker.candidateFloor': 80;
75
99
  readonly 'logging.level': "error";
100
+ readonly 'logging.searchMetrics': false;
76
101
  };
77
102
  export declare function readConfigJson(): JsonConfig | null;
78
103
  /**
79
- * Validate a parsed config object against the Zod schema. Unknown keys (e.g.
80
- * the "$schema" editor reference) are stripped. Throws with a caller-friendly
81
- * message on validation failure. Pure — no file I/O.
104
+ * Detect a raw (pre-migration) config object that still selects the removed
105
+ * `ollama` embedding provider — explicitly, via a legacy top-level `ollama`
106
+ * block, or implicitly (no `provider` set, but `baseURL`/`model` point at
107
+ * Ollama's defaults) — and throw a `ConfigError` with migration guidance
108
+ * instead of letting it validate under a silently reinterpreted provider.
109
+ * Runs before `migrateRawConfig` so a legacy `ollama.*` block is never
110
+ * converted into an `embedding` block first. Pure — never mutates `raw`.
111
+ */
112
+ export declare function detectFormerOllamaConfig(raw: unknown): void;
113
+ /**
114
+ * Migrate a legacy raw config object to the current shape, in place of a
115
+ * versioned migration system. Pure — returns a new object, never mutates input.
116
+ *
117
+ * - Removed translation keys (`search.autoTranslate`, `search.translateModel`)
118
+ * are dropped so they neither validate-fail nor take effect.
119
+ * - Legacy `reranker.provider: 'ollama'` (a dead value — Ollama has no rerank
120
+ * API) is rewritten to `openai-compatible`, and a stale `/api/rerank` URL is
121
+ * dropped so it re-derives to `/rerank`. Otherwise the tightened enum would
122
+ * fail validation on existing configs and lock the user out.
123
+ *
124
+ * A legacy top-level `ollama` block reaches here only when `detectFormerOllamaConfig`
125
+ * did not already reject it (i.e. an `embedding` block is also present); it is
126
+ * dropped rather than merged. Exported for unit testing.
127
+ */
128
+ export declare function migrateRawConfig(raw: unknown): unknown;
129
+ /**
130
+ * Validate a raw (pre-migration) config object against the Zod schema: first
131
+ * `detectFormerOllamaConfig` (a `ConfigError` on a former-Ollama config takes
132
+ * priority over both migration and the regular Zod enum rejection), then
133
+ * `migrateRawConfig`, then the schema itself. Unknown keys (e.g. the "$schema"
134
+ * editor reference) are stripped. Throws with a caller-friendly message on
135
+ * validation failure. The single entry point for both `readConfigJson` and
136
+ * `PUT /api/config`, so both apply identical rules with an identical message.
137
+ * Pure — no file I/O.
82
138
  */
83
139
  export declare function parseConfig(raw: unknown, source?: string): JsonConfig;
84
140
  /** Build the JSON Schema object generated from the Zod config schema. Pure. */
@@ -33,8 +33,10 @@ 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.detectFormerOllamaConfig = detectFormerOllamaConfig;
39
+ exports.migrateRawConfig = migrateRawConfig;
38
40
  exports.parseConfig = parseConfig;
39
41
  exports.buildJsonSchema = buildJsonSchema;
40
42
  exports.writeConfigJson = writeConfigJson;
@@ -47,9 +49,11 @@ const path = __importStar(require("path"));
47
49
  const os = __importStar(require("os"));
48
50
  const zod_1 = require("zod");
49
51
  const constants_1 = require("./constants");
52
+ const config_error_1 = require("./config-error");
50
53
  exports.CONFIG_DIR = path.join(os.homedir(), '.config', 'codesearch-mcp');
51
54
  exports.CONFIG_FILE = path.join(exports.CONFIG_DIR, 'config.json');
52
55
  exports.SCHEMA_FILE = path.join(exports.CONFIG_DIR, 'config.schema.json');
56
+ exports.ENV_FILE = path.join(exports.CONFIG_DIR, '.env');
53
57
  // Relative $schema reference written into config.json so editors resolve the
54
58
  // sibling schema file for autocomplete / inline validation.
55
59
  const SCHEMA_REF = './config.schema.json';
@@ -63,32 +67,40 @@ const jsonSchema = zod_1.z.object({
63
67
  password: zod_1.z.string().describe('PostgreSQL password').optional(),
64
68
  name: zod_1.z.string().describe('Database name; defaults to rag_<os-username>, auto-created on first run').optional(),
65
69
  }).optional(),
66
- ollama: zod_1.z.object({
67
- host: zod_1.z.string().describe('Ollama API URL (default http://localhost:11434)').optional(),
68
- model: zod_1.z.string().describe('Embedding model (default bge-m3)').optional(),
70
+ embedding: zod_1.z.object({
71
+ provider: zod_1.z.enum(['openai-compatible', 'voyage', 'infinity']).describe('Embedding backend (default infinity)').optional(),
72
+ baseURL: zod_1.z.string().describe('Backend base URL. Infinity/TEI/vLLM/Ollama-via-openai-compatible: http://localhost:7997 (Infinity default) · OpenAI: https://api.openai.com/v1 · Voyage: https://api.voyageai.com/v1').optional(),
73
+ 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(),
74
+ model: zod_1.z.string().describe('Embedding model (default jinaai/jina-embeddings-v2-base-code)').optional(),
75
+ 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(),
76
+ batchSize: zod_1.z.number().int().describe('Texts per batch embedding request (default 32)').optional(),
69
77
  }).optional(),
70
78
  indexing: zod_1.z.object({
71
79
  chunkSize: zod_1.z.number().int().describe('Max chars per chunk (default 1500)').optional(),
72
80
  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(),
81
+ 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
82
  astBatchSize: zod_1.z.number().int().describe('Max files per ts-morph batch; larger projects skip dependency resolution (default 3000)').optional(),
83
+ 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
84
  }).optional(),
76
85
  search: zod_1.z.object({
77
86
  similarityThreshold: zod_1.z.number().min(0).max(1).describe('Max cosine distance 0–1; lower = stricter (default 0.6)').optional(),
78
87
  defaultLimit: zod_1.z.number().int().describe('Default result count (default 5)').optional(),
79
88
  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
89
  }).optional(),
83
90
  reranker: zod_1.z.object({
84
91
  enabled: zod_1.z.boolean().describe('Enable cross-encoder reranking; requires reranker.model').optional(),
92
+ provider: zod_1.z.enum(['openai-compatible']).describe('Rerank backend. Only openai-compatible /rerank servers (Infinity, TEI, vLLM).').optional(),
93
+ baseURL: zod_1.z.string().describe('Rerank server base URL (defaults to embedding.baseURL — Infinity serves both).').optional(),
94
+ apiKey: zod_1.z.string().describe('API key, or env:VAR_NAME. Empty for local servers.').optional(),
85
95
  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(),
87
- overfetch: zod_1.z.number().int().min(1).max(20).describe('Candidate multiplier before reranking (limit × N, default 3)').optional(),
96
+ url: zod_1.z.string().describe('Full URL of the rerank endpoint (default derived from provider + baseURL)').optional(),
97
+ overfetch: zod_1.z.number().int().min(1).max(20).describe('Candidate multiplier before reranking (limit × N, default 5)').optional(),
98
+ candidateFloor: zod_1.z.number().int().min(1).describe('Minimum candidate pool fed to the reranker regardless of limit; fetchLimit = min(max(limit × overfetch, candidateFloor), 200) (default 80)').optional(),
88
99
  }).optional(),
89
100
  logging: zod_1.z.object({
90
101
  level: zod_1.z.enum(['debug', 'info', 'warn', 'error']).describe('File log level (default error)').optional(),
91
102
  dir: zod_1.z.string().describe('Base log directory; "false" to disable file logging').optional(),
103
+ searchMetrics: zod_1.z.boolean().describe('Emit one structured search-tuning line (info level) per search, independent of level (default false)').optional(),
92
104
  }).optional(),
93
105
  workspaceRoot: zod_1.z.string().describe('Full path to the directory containing all your projects').optional(),
94
106
  });
@@ -98,23 +110,31 @@ const FIELD_TYPES = {
98
110
  'db.user': 'string',
99
111
  'db.password': 'string',
100
112
  'db.name': 'string',
101
- 'ollama.host': 'string',
102
- 'ollama.model': 'string',
113
+ 'embedding.provider': 'string',
114
+ 'embedding.baseURL': 'string',
115
+ 'embedding.apiKey': 'string',
116
+ 'embedding.model': 'string',
117
+ 'embedding.dimensions': 'number',
118
+ 'embedding.batchSize': 'number',
103
119
  'indexing.chunkSize': 'number',
104
120
  'indexing.chunkOverlap': 'number',
105
121
  'indexing.embedBatchSize': 'number',
106
122
  'indexing.astBatchSize': 'number',
123
+ 'indexing.chunkStrategy': 'string',
107
124
  'search.similarityThreshold': 'number',
108
125
  'search.defaultLimit': 'number',
109
126
  'search.maxLimit': 'number',
110
- 'search.autoTranslate': 'boolean',
111
- 'search.translateModel': 'string',
112
127
  'reranker.enabled': 'boolean',
128
+ 'reranker.provider': 'string',
129
+ 'reranker.baseURL': 'string',
130
+ 'reranker.apiKey': 'string',
113
131
  'reranker.model': 'string',
114
132
  'reranker.url': 'string',
115
133
  'reranker.overfetch': 'number',
134
+ 'reranker.candidateFloor': 'number',
116
135
  'logging.level': 'string',
117
136
  'logging.dir': 'string',
137
+ 'logging.searchMetrics': 'boolean',
118
138
  'workspaceRoot': 'string',
119
139
  };
120
140
  exports.ALL_KEYS = Object.keys(FIELD_TYPES);
@@ -124,27 +144,34 @@ exports.ALL_KEYS = Object.keys(FIELD_TYPES);
124
144
  * `resolvedSchema` in `src/config.ts`, whose `.default(...)` calls reference
125
145
  * these values — so the runtime default and the dashboard-displayed default
126
146
  * 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).
147
+ * lookups stay type-safe. Keys omitted here have no static default: required
148
+ * fields (db.host/user/password) and values derived at runtime — db.name →
149
+ * rag_<user>, and the provider-dependent URLs (embedding.baseURL,
150
+ * reranker.baseURL, reranker.url), which are computed in the `Config`
151
+ * constructor per provider rather than through a schema `.default(...)`.
130
152
  */
131
153
  exports.CONFIG_DEFAULTS = {
132
154
  'db.port': 5433,
133
- 'ollama.host': 'http://localhost:11434',
134
- 'ollama.model': constants_1.DEFAULT_EMBED_MODEL,
155
+ 'embedding.provider': 'infinity',
156
+ 'embedding.apiKey': '',
157
+ 'embedding.model': constants_1.DEFAULT_EMBED_MODEL,
158
+ 'embedding.batchSize': 32,
135
159
  'indexing.chunkSize': 1500,
136
160
  'indexing.chunkOverlap': 200,
137
161
  'indexing.embedBatchSize': 5,
138
162
  'indexing.astBatchSize': 3000,
163
+ 'indexing.chunkStrategy': 'ast',
139
164
  'search.similarityThreshold': 0.6,
140
165
  'search.defaultLimit': 5,
141
166
  'search.maxLimit': 20,
142
- 'search.autoTranslate': false,
143
- 'search.translateModel': 'llama3.2',
144
- 'reranker.enabled': false,
145
- 'reranker.model': '',
146
- 'reranker.overfetch': 3,
167
+ 'reranker.enabled': true,
168
+ 'reranker.provider': 'openai-compatible',
169
+ 'reranker.apiKey': '',
170
+ 'reranker.model': 'mixedbread-ai/mxbai-rerank-xsmall-v1',
171
+ 'reranker.overfetch': 5,
172
+ 'reranker.candidateFloor': 80,
147
173
  'logging.level': 'error',
174
+ 'logging.searchMetrics': false,
148
175
  };
149
176
  function readConfigJson() {
150
177
  if (!fs.existsSync(exports.CONFIG_FILE))
@@ -166,13 +193,101 @@ function readConfigJson() {
166
193
  }
167
194
  return parseConfig(parsed, exports.CONFIG_FILE);
168
195
  }
196
+ const OLLAMA_MIGRATION_REMEDY = 'The "ollama" embedding provider has been removed. Point Ollama through the ' +
197
+ '"openai-compatible" provider instead — set embedding.provider to "openai-compatible" ' +
198
+ 'and embedding.baseURL to "http://localhost:11434/v1" — via `rag-config edit` (or by ' +
199
+ 'editing the config file directly) or by re-running `rag-setup`. After switching, rebuild ' +
200
+ 'the index with `rag-index --reindex`.';
201
+ /** Strip a trailing `:tag` (Docker-style) from a model name, if present. */
202
+ function stripModelTag(model) {
203
+ return model.replace(/:[^:]*$/, '');
204
+ }
205
+ /**
206
+ * Detect a raw (pre-migration) config object that still selects the removed
207
+ * `ollama` embedding provider — explicitly, via a legacy top-level `ollama`
208
+ * block, or implicitly (no `provider` set, but `baseURL`/`model` point at
209
+ * Ollama's defaults) — and throw a `ConfigError` with migration guidance
210
+ * instead of letting it validate under a silently reinterpreted provider.
211
+ * Runs before `migrateRawConfig` so a legacy `ollama.*` block is never
212
+ * converted into an `embedding` block first. Pure — never mutates `raw`.
213
+ */
214
+ function detectFormerOllamaConfig(raw) {
215
+ if (raw == null || typeof raw !== 'object')
216
+ return;
217
+ const obj = raw;
218
+ const embedding = obj.embedding;
219
+ if (embedding != null && typeof embedding === 'object') {
220
+ const e = embedding;
221
+ if (e.provider === 'ollama') {
222
+ throw new config_error_1.ConfigError(`Config selects the "ollama" embedding provider, which is no longer supported. ${OLLAMA_MIGRATION_REMEDY}`);
223
+ }
224
+ if (e.provider === undefined) {
225
+ if (typeof e.baseURL === 'string') {
226
+ let port;
227
+ try {
228
+ port = new URL(e.baseURL).port;
229
+ }
230
+ catch { /* unparsable — not a signal */ }
231
+ if (port === '11434') {
232
+ throw new config_error_1.ConfigError(`Config's embedding.baseURL targets Ollama's default port (11434) with no embedding.provider set. ${OLLAMA_MIGRATION_REMEDY}`);
233
+ }
234
+ }
235
+ if (typeof e.model === 'string' && constants_1.FORMER_OLLAMA_MODELS.includes(stripModelTag(e.model))) {
236
+ throw new config_error_1.ConfigError(`Config's embedding.model ("${e.model}") was formerly registered for Ollama, with no embedding.provider set. ${OLLAMA_MIGRATION_REMEDY}`);
237
+ }
238
+ }
239
+ return;
240
+ }
241
+ if (obj.ollama != null && typeof obj.ollama === 'object') {
242
+ throw new config_error_1.ConfigError(`Config has a legacy "ollama" configuration block. ${OLLAMA_MIGRATION_REMEDY}`);
243
+ }
244
+ }
245
+ /**
246
+ * Migrate a legacy raw config object to the current shape, in place of a
247
+ * versioned migration system. Pure — returns a new object, never mutates input.
248
+ *
249
+ * - Removed translation keys (`search.autoTranslate`, `search.translateModel`)
250
+ * are dropped so they neither validate-fail nor take effect.
251
+ * - Legacy `reranker.provider: 'ollama'` (a dead value — Ollama has no rerank
252
+ * API) is rewritten to `openai-compatible`, and a stale `/api/rerank` URL is
253
+ * dropped so it re-derives to `/rerank`. Otherwise the tightened enum would
254
+ * fail validation on existing configs and lock the user out.
255
+ *
256
+ * A legacy top-level `ollama` block reaches here only when `detectFormerOllamaConfig`
257
+ * did not already reject it (i.e. an `embedding` block is also present); it is
258
+ * dropped rather than merged. Exported for unit testing.
259
+ */
260
+ function migrateRawConfig(raw) {
261
+ if (raw == null || typeof raw !== 'object')
262
+ return raw;
263
+ const obj = JSON.parse(JSON.stringify(raw));
264
+ delete obj.ollama;
265
+ if (obj.search && typeof obj.search === 'object') {
266
+ delete obj.search.autoTranslate;
267
+ delete obj.search.translateModel;
268
+ }
269
+ if (obj.reranker && typeof obj.reranker === 'object') {
270
+ if (obj.reranker.provider === 'ollama')
271
+ obj.reranker.provider = 'openai-compatible';
272
+ if (typeof obj.reranker.url === 'string' && /\/api\/rerank\/?(\?.*)?$/.test(obj.reranker.url))
273
+ delete obj.reranker.url;
274
+ }
275
+ return obj;
276
+ }
169
277
  /**
170
- * Validate a parsed config object against the Zod schema. Unknown keys (e.g.
171
- * the "$schema" editor reference) are stripped. Throws with a caller-friendly
172
- * message on validation failure. Pure — no file I/O.
278
+ * Validate a raw (pre-migration) config object against the Zod schema: first
279
+ * `detectFormerOllamaConfig` (a `ConfigError` on a former-Ollama config takes
280
+ * priority over both migration and the regular Zod enum rejection), then
281
+ * `migrateRawConfig`, then the schema itself. Unknown keys (e.g. the "$schema"
282
+ * editor reference) are stripped. Throws with a caller-friendly message on
283
+ * validation failure. The single entry point for both `readConfigJson` and
284
+ * `PUT /api/config`, so both apply identical rules with an identical message.
285
+ * Pure — no file I/O.
173
286
  */
174
287
  function parseConfig(raw, source = exports.CONFIG_FILE) {
175
- const result = jsonSchema.safeParse(raw);
288
+ detectFormerOllamaConfig(raw);
289
+ const migrated = migrateRawConfig(raw);
290
+ const result = jsonSchema.safeParse(migrated);
176
291
  if (!result.success) {
177
292
  const issues = result.error.issues.map(i => ` ${i.path.join('.')}: ${i.message}`).join('\n');
178
293
  throw new Error(`Invalid config in ${source}:\n${issues}\n` +
@@ -1,6 +1,13 @@
1
- export declare class ConfigError extends Error {
2
- constructor(message: string);
3
- }
1
+ import { ConfigError } from './config-error';
2
+ export { ConfigError };
3
+ /**
4
+ * Resolve a config value that may reference an environment variable. A value of
5
+ * the form `env:VAR_NAME` resolves to `process.env.VAR_NAME`; any other string
6
+ * (including empty) is returned verbatim. A referenced-but-unset variable throws
7
+ * a clear error naming the variable and the `.env` file. Pure aside from the
8
+ * injected env map. See `src/app.ts` for how `.env` is loaded into the process.
9
+ */
10
+ export declare function resolveEnvRef(value: string, env?: NodeJS.ProcessEnv): string;
4
11
  export declare class Config {
5
12
  private readonly data;
6
13
  get db(): {
@@ -10,29 +17,47 @@ export declare class Config {
10
17
  password: string;
11
18
  database: string;
12
19
  };
13
- get ollama(): {
14
- host: string;
20
+ /**
21
+ * Embedding config. `apiKey` is returned **raw** (possibly an `env:VAR` ref) —
22
+ * callers resolve it lazily via `resolveEnvRef` at the moment they build a
23
+ * request, so an unset referenced variable fails only when the key is actually
24
+ * needed (during embed/search) rather than crashing container construction and
25
+ * locking the user out of `rag-web`/`rag-mcp`.
26
+ */
27
+ get embedding(): {
28
+ provider: "openai-compatible" | "voyage" | "infinity";
29
+ baseURL: string;
30
+ apiKey: string;
31
+ model: string;
32
+ dimensions: number | null;
33
+ batchSize: number;
34
+ };
35
+ /** Reranker config. `apiKey` is raw — resolve lazily with `resolveEnvRef`. */
36
+ get reranker(): {
37
+ enabled: boolean;
38
+ provider: "openai-compatible";
39
+ baseURL: string;
40
+ apiKey: string;
15
41
  model: string;
16
- rerankerModel: string;
17
- rerankerUrl: string;
42
+ url: string;
43
+ overfetch: number;
44
+ candidateFloor: 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";
60
+ searchMetrics: boolean;
36
61
  dir?: string | undefined;
37
62
  };
38
63
  get workspaceRoot(): string;