codesearch-mcp 0.0.17-alpha.1 → 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 (134) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +95 -6
  3. package/dist/app.js +70 -10
  4. package/dist/src/commands/ConfigCommand.d.ts +1 -0
  5. package/dist/src/commands/ConfigCommand.js +84 -4
  6. package/dist/src/commands/IndexCommand.js +35 -7
  7. package/dist/src/commands/McpCommand.js +9 -4
  8. package/dist/src/commands/{SetupCommand.d.ts → WebCommand.d.ts} +3 -2
  9. package/dist/src/commands/WebCommand.js +56 -0
  10. package/dist/src/config-store.d.ts +85 -4
  11. package/dist/src/config-store.js +157 -32
  12. package/dist/src/config.d.ts +32 -8
  13. package/dist/src/config.js +93 -30
  14. package/dist/src/constants.d.ts +35 -0
  15. package/dist/src/constants.js +57 -4
  16. package/dist/src/container.d.ts +10 -6
  17. package/dist/src/container.js +10 -5
  18. package/dist/src/db/index.d.ts +1 -1
  19. package/dist/src/db/index.js +2 -1
  20. package/dist/src/db/schema.d.ts +111 -0
  21. package/dist/src/db/schema.js +15 -1
  22. package/dist/src/graph.d.ts +60 -0
  23. package/dist/src/graph.js +239 -120
  24. package/dist/src/help.js +20 -5
  25. package/dist/src/interfaces/IAstParser.d.ts +20 -3
  26. package/dist/src/interfaces/IChunker.d.ts +24 -0
  27. package/dist/src/interfaces/IChunker.js +2 -0
  28. package/dist/src/interfaces/IDatabase.d.ts +27 -0
  29. package/dist/src/interfaces/IDatabase.js +2 -0
  30. package/dist/src/interfaces/IEmbedder.d.ts +15 -2
  31. package/dist/src/interfaces/ILogger.d.ts +10 -0
  32. package/dist/src/interfaces/ILogger.js +2 -0
  33. package/dist/src/interfaces/IMcpToolHandler.d.ts +22 -0
  34. package/dist/src/interfaces/IMcpToolHandler.js +2 -0
  35. package/dist/src/interfaces/IMonorepoResolver.d.ts +33 -0
  36. package/dist/src/interfaces/IMonorepoResolver.js +2 -0
  37. package/dist/src/interfaces/IProjectDiscovery.d.ts +28 -0
  38. package/dist/src/interfaces/IProjectDiscovery.js +2 -0
  39. package/dist/src/interfaces/ISearcher.d.ts +37 -0
  40. package/dist/src/interfaces/ISearcher.js +2 -0
  41. package/dist/src/interfaces/index.d.ts +8 -0
  42. package/dist/src/interfaces/index.js +2 -0
  43. package/dist/src/owner-path.d.ts +10 -0
  44. package/dist/src/owner-path.js +27 -0
  45. package/dist/src/project-selector.d.ts +1 -1
  46. package/dist/src/services/AstChunker.d.ts +31 -0
  47. package/dist/src/services/AstChunker.js +114 -0
  48. package/dist/src/services/AstParserService.d.ts +10 -6
  49. package/dist/src/services/AstParserService.js +60 -15
  50. package/dist/src/services/DatabaseService.d.ts +14 -1
  51. package/dist/src/services/DatabaseService.js +70 -25
  52. package/dist/src/services/EmbedderFactory.d.ts +9 -0
  53. package/dist/src/services/EmbedderFactory.js +29 -0
  54. package/dist/src/services/IndexerService.d.ts +22 -12
  55. package/dist/src/services/IndexerService.js +106 -46
  56. package/dist/src/services/InfinityEmbedder.d.ts +79 -0
  57. package/dist/src/services/InfinityEmbedder.js +199 -0
  58. package/dist/src/services/LoggerService.d.ts +2 -1
  59. package/dist/src/services/McpServer.d.ts +16 -7
  60. package/dist/src/services/McpServer.js +54 -367
  61. package/dist/src/services/MonorepoAwareDiscoveryService.d.ts +62 -0
  62. package/dist/src/services/MonorepoAwareDiscoveryService.js +125 -0
  63. package/dist/src/services/NxMonorepoResolver.d.ts +45 -0
  64. package/dist/src/services/NxMonorepoResolver.js +308 -0
  65. package/dist/src/services/OllamaEmbedder.d.ts +19 -5
  66. package/dist/src/services/OllamaEmbedder.js +91 -36
  67. package/dist/src/services/OpenAICompatibleEmbedder.d.ts +25 -0
  68. package/dist/src/services/OpenAICompatibleEmbedder.js +75 -0
  69. package/dist/src/services/ProjectDiscoveryService.d.ts +8 -0
  70. package/dist/src/{env-loader.js → services/ProjectDiscoveryService.js} +16 -5
  71. package/dist/src/services/SearcherService.d.ts +46 -7
  72. package/dist/src/services/SearcherService.js +183 -53
  73. package/dist/src/services/VoyageEmbedder.d.ts +22 -0
  74. package/dist/src/services/VoyageEmbedder.js +72 -0
  75. package/dist/src/services/WebServerService.d.ts +77 -0
  76. package/dist/src/services/WebServerService.js +267 -0
  77. package/dist/src/services/http-retry.d.ts +19 -0
  78. package/dist/src/services/http-retry.js +54 -0
  79. package/dist/src/services/reranker.d.ts +15 -0
  80. package/dist/src/services/reranker.js +44 -0
  81. package/dist/src/setup.d.ts +6 -0
  82. package/dist/src/setup.js +249 -31
  83. package/dist/src/tools/FindSymbolTool.d.ts +8 -0
  84. package/dist/src/tools/FindSymbolTool.js +45 -0
  85. package/dist/src/tools/GetCallersTool.d.ts +8 -0
  86. package/dist/src/tools/GetCallersTool.js +42 -0
  87. package/dist/src/tools/GetDependenciesTool.d.ts +8 -0
  88. package/dist/src/tools/GetDependenciesTool.js +38 -0
  89. package/dist/src/tools/GetDependentsTool.d.ts +8 -0
  90. package/dist/src/tools/GetDependentsTool.js +57 -0
  91. package/dist/src/tools/ListFilesTool.d.ts +8 -0
  92. package/dist/src/tools/ListFilesTool.js +39 -0
  93. package/dist/src/tools/ListProjectsTool.d.ts +8 -0
  94. package/dist/src/tools/ListProjectsTool.js +44 -0
  95. package/dist/src/tools/ReadFileRangeTool.d.ts +8 -0
  96. package/dist/src/tools/ReadFileRangeTool.js +44 -0
  97. package/dist/src/tools/ReadFileTool.d.ts +8 -0
  98. package/dist/src/tools/ReadFileTool.js +42 -0
  99. package/dist/src/tools/SearchCodebaseTool.d.ts +13 -0
  100. package/dist/src/tools/SearchCodebaseTool.js +81 -0
  101. package/dist/src/tools/format-helpers.d.ts +8 -0
  102. package/dist/src/tools/format-helpers.js +58 -0
  103. package/dist/src/tools/index.d.ts +9 -0
  104. package/dist/src/tools/index.js +21 -0
  105. package/dist/src/web/dashboard.d.ts +9 -0
  106. package/dist/src/web/dashboard.js +342 -0
  107. package/docker/docker-compose.infinity-gpu.yml +19 -0
  108. package/docker/docker-compose.yml +25 -0
  109. package/docs/web-dashboard.md +187 -0
  110. package/package.json +25 -3
  111. package/dist/src/ast.d.ts +0 -2
  112. package/dist/src/ast.js +0 -110
  113. package/dist/src/commands/SetupCommand.js +0 -14
  114. package/dist/src/db/data-source.d.ts +0 -2
  115. package/dist/src/db/data-source.js +0 -13
  116. package/dist/src/db/entities/code-chunk.entity.d.ts +0 -9
  117. package/dist/src/db/entities/code-chunk.entity.js +0 -54
  118. package/dist/src/db/entities/code-dependency.entity.d.ts +0 -9
  119. package/dist/src/db/entities/code-dependency.entity.js +0 -54
  120. package/dist/src/db/entities/code-symbol.entity.d.ts +0 -10
  121. package/dist/src/db/entities/code-symbol.entity.js +0 -59
  122. package/dist/src/db/init.d.ts +0 -2
  123. package/dist/src/db/init.js +0 -62
  124. package/dist/src/env-loader.d.ts +0 -1
  125. package/dist/src/indexer.d.ts +0 -17
  126. package/dist/src/indexer.js +0 -242
  127. package/dist/src/mcp.d.ts +0 -1
  128. package/dist/src/mcp.js +0 -403
  129. package/dist/src/ollama.d.ts +0 -2
  130. package/dist/src/ollama.js +0 -6
  131. package/dist/src/searcher.d.ts +0 -74
  132. package/dist/src/searcher.js +0 -446
  133. package/dist/vitest.config.d.ts +0 -2
  134. package/dist/vitest.config.js +0 -9
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WebCommand = void 0;
4
+ const WebServerService_1 = require("../services/WebServerService");
5
+ const DEFAULT_PORT = 4319;
6
+ class WebCommand {
7
+ container;
8
+ port;
9
+ constructor(container, args = []) {
10
+ this.container = container;
11
+ this.port = parsePort(args) ?? DEFAULT_PORT;
12
+ }
13
+ async execute() {
14
+ const { db, searcher, config, logger, embedder } = this.container;
15
+ try {
16
+ await db.initDb();
17
+ }
18
+ catch (err) {
19
+ const reason = err instanceof Error ? err.message : String(err);
20
+ logger.error('WebCommand: database connection failed', { reason });
21
+ console.error(`Database connection failed: ${reason}`);
22
+ // Continue: config editing still works; project/graph tabs will error in-page.
23
+ }
24
+ const server = new WebServerService_1.WebServer(searcher, config, logger, embedder);
25
+ const host = '127.0.0.1';
26
+ try {
27
+ await server.start(this.port, host);
28
+ }
29
+ catch (err) {
30
+ const reason = err instanceof Error ? err.message : String(err);
31
+ logger.error('WebCommand: server failed to start', { reason });
32
+ console.error(`Failed to start web server on ${host}:${this.port}: ${reason}`);
33
+ return;
34
+ }
35
+ console.log(`RAG dashboard: http://${host}:${this.port} (Ctrl+C to stop)`);
36
+ // Keep execute() pending until a shutdown signal, otherwise app.ts tears
37
+ // down the DB and exits immediately (mirrors McpCommand's keep-alive).
38
+ await new Promise(resolve => {
39
+ const shutdown = () => { void server.stop().then(resolve); };
40
+ process.once('SIGINT', shutdown);
41
+ process.once('SIGTERM', shutdown);
42
+ });
43
+ }
44
+ }
45
+ exports.WebCommand = WebCommand;
46
+ function parsePort(args) {
47
+ const i = args.indexOf('--port');
48
+ if (i === -1)
49
+ return undefined;
50
+ const raw = args[i + 1];
51
+ const n = Number(raw);
52
+ if (!raw || !Number.isInteger(n) || n < 1 || n > 65535) {
53
+ throw new Error(`Invalid --port value: ${raw ?? '(missing)'}`);
54
+ }
55
+ return n;
56
+ }
@@ -1,6 +1,8 @@
1
1
  import { z } from 'zod';
2
2
  export declare const CONFIG_DIR: string;
3
3
  export declare const CONFIG_FILE: string;
4
+ export declare const SCHEMA_FILE: string;
5
+ export declare const ENV_FILE: string;
4
6
  declare const jsonSchema: z.ZodObject<{
5
7
  db: z.ZodOptional<z.ZodObject<{
6
8
  host: z.ZodOptional<z.ZodString>;
@@ -9,25 +11,41 @@ declare const jsonSchema: z.ZodObject<{
9
11
  password: z.ZodOptional<z.ZodString>;
10
12
  name: z.ZodOptional<z.ZodString>;
11
13
  }, z.core.$strip>>;
12
- ollama: z.ZodOptional<z.ZodObject<{
13
- 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>;
14
23
  model: z.ZodOptional<z.ZodString>;
24
+ dimensions: z.ZodOptional<z.ZodNumber>;
25
+ batchSize: z.ZodOptional<z.ZodNumber>;
15
26
  }, z.core.$strip>>;
16
27
  indexing: z.ZodOptional<z.ZodObject<{
17
28
  chunkSize: z.ZodOptional<z.ZodNumber>;
18
29
  chunkOverlap: z.ZodOptional<z.ZodNumber>;
19
30
  embedBatchSize: z.ZodOptional<z.ZodNumber>;
20
31
  astBatchSize: z.ZodOptional<z.ZodNumber>;
32
+ chunkStrategy: z.ZodOptional<z.ZodEnum<{
33
+ ast: "ast";
34
+ character: "character";
35
+ }>>;
21
36
  }, z.core.$strip>>;
22
37
  search: z.ZodOptional<z.ZodObject<{
23
38
  similarityThreshold: z.ZodOptional<z.ZodNumber>;
24
39
  defaultLimit: z.ZodOptional<z.ZodNumber>;
25
40
  maxLimit: z.ZodOptional<z.ZodNumber>;
26
- autoTranslate: z.ZodOptional<z.ZodBoolean>;
27
- translateModel: z.ZodOptional<z.ZodString>;
28
41
  }, z.core.$strip>>;
29
42
  reranker: z.ZodOptional<z.ZodObject<{
30
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>;
31
49
  model: z.ZodOptional<z.ZodString>;
32
50
  url: z.ZodOptional<z.ZodString>;
33
51
  overfetch: z.ZodOptional<z.ZodNumber>;
@@ -45,8 +63,71 @@ declare const jsonSchema: z.ZodObject<{
45
63
  }, z.core.$strip>;
46
64
  export type JsonConfig = z.infer<typeof jsonSchema>;
47
65
  export declare const ALL_KEYS: string[];
66
+ /**
67
+ * Single source of truth for built-in defaults, keyed by raw config.json dotted
68
+ * key. Consumed both by the web dashboard (`GET /api/defaults`) and by
69
+ * `resolvedSchema` in `src/config.ts`, whose `.default(...)` calls reference
70
+ * these values — so the runtime default and the dashboard-displayed default
71
+ * cannot diverge. `as const` keeps the literal types so those `.default()`
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(...)`.
77
+ */
78
+ export declare const CONFIG_DEFAULTS: {
79
+ readonly 'db.port': 5433;
80
+ readonly 'embedding.provider': "ollama";
81
+ readonly 'embedding.apiKey': "";
82
+ readonly 'embedding.model': "bge-m3";
83
+ readonly 'embedding.batchSize': 32;
84
+ readonly 'indexing.chunkSize': 1500;
85
+ readonly 'indexing.chunkOverlap': 200;
86
+ readonly 'indexing.embedBatchSize': 5;
87
+ readonly 'indexing.astBatchSize': 3000;
88
+ readonly 'indexing.chunkStrategy': "ast";
89
+ readonly 'search.similarityThreshold': 0.6;
90
+ readonly 'search.defaultLimit': 5;
91
+ readonly 'search.maxLimit': 20;
92
+ readonly 'reranker.enabled': false;
93
+ readonly 'reranker.provider': "openai-compatible";
94
+ readonly 'reranker.apiKey': "";
95
+ readonly 'reranker.model': "";
96
+ readonly 'reranker.overfetch': 3;
97
+ readonly 'logging.level': "error";
98
+ };
48
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;
116
+ /**
117
+ * Validate a parsed config object against the Zod schema. Unknown keys (e.g.
118
+ * the "$schema" editor reference) are stripped. Throws with a caller-friendly
119
+ * message on validation failure. Pure — no file I/O.
120
+ */
121
+ export declare function parseConfig(raw: unknown, source?: string): JsonConfig;
122
+ /** Build the JSON Schema object generated from the Zod config schema. Pure. */
123
+ export declare function buildJsonSchema(): Record<string, unknown>;
49
124
  export declare function writeConfigJson(data: JsonConfig): void;
125
+ /**
126
+ * Generate a JSON Schema from the Zod config schema and write it next to
127
+ * config.json. Editors resolve it via the "$schema" reference and provide
128
+ * key autocomplete, type hints, and inline validation while editing.
129
+ */
130
+ export declare function writeSchemaFile(): void;
50
131
  export declare function getNestedValue(cfg: JsonConfig, key: string): unknown;
51
132
  export declare function setNestedValue(cfg: JsonConfig, key: string, raw: string): JsonConfig;
52
133
  export declare function deleteNestedValue(cfg: JsonConfig, key: string): JsonConfig;
@@ -33,9 +33,13 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.ALL_KEYS = 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;
39
+ exports.parseConfig = parseConfig;
40
+ exports.buildJsonSchema = buildJsonSchema;
38
41
  exports.writeConfigJson = writeConfigJson;
42
+ exports.writeSchemaFile = writeSchemaFile;
39
43
  exports.getNestedValue = getNestedValue;
40
44
  exports.setNestedValue = setNestedValue;
41
45
  exports.deleteNestedValue = deleteNestedValue;
@@ -43,45 +47,58 @@ const fs = __importStar(require("fs"));
43
47
  const path = __importStar(require("path"));
44
48
  const os = __importStar(require("os"));
45
49
  const zod_1 = require("zod");
50
+ const constants_1 = require("./constants");
46
51
  exports.CONFIG_DIR = path.join(os.homedir(), '.config', 'codesearch-mcp');
47
52
  exports.CONFIG_FILE = path.join(exports.CONFIG_DIR, 'config.json');
53
+ exports.SCHEMA_FILE = path.join(exports.CONFIG_DIR, 'config.schema.json');
54
+ exports.ENV_FILE = path.join(exports.CONFIG_DIR, '.env');
55
+ // Relative $schema reference written into config.json so editors resolve the
56
+ // sibling schema file for autocomplete / inline validation.
57
+ const SCHEMA_REF = './config.schema.json';
48
58
  // All fields optional — defaults live in config.ts. The file stays minimal.
59
+ // Descriptions surface as tooltips in schema-aware editors (VS Code, JetBrains).
49
60
  const jsonSchema = zod_1.z.object({
50
61
  db: zod_1.z.object({
51
- host: zod_1.z.string().optional(),
52
- port: zod_1.z.number().optional(),
53
- user: zod_1.z.string().optional(),
54
- password: zod_1.z.string().optional(),
55
- name: zod_1.z.string().optional(),
62
+ host: zod_1.z.string().describe('PostgreSQL host').optional(),
63
+ port: zod_1.z.number().int().describe('PostgreSQL port (default 5433)').optional(),
64
+ user: zod_1.z.string().describe('PostgreSQL user').optional(),
65
+ password: zod_1.z.string().describe('PostgreSQL password').optional(),
66
+ name: zod_1.z.string().describe('Database name; defaults to rag_<os-username>, auto-created on first run').optional(),
56
67
  }).optional(),
57
- ollama: zod_1.z.object({
58
- host: zod_1.z.string().optional(),
59
- model: zod_1.z.string().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(),
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(),
60
75
  }).optional(),
61
76
  indexing: zod_1.z.object({
62
- chunkSize: zod_1.z.number().optional(),
63
- chunkOverlap: zod_1.z.number().optional(),
64
- embedBatchSize: zod_1.z.number().optional(),
65
- astBatchSize: zod_1.z.number().optional(),
77
+ chunkSize: zod_1.z.number().int().describe('Max chars per chunk (default 1500)').optional(),
78
+ chunkOverlap: zod_1.z.number().int().describe('Overlap between chunks (default 200)').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(),
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(),
66
82
  }).optional(),
67
83
  search: zod_1.z.object({
68
- similarityThreshold: zod_1.z.number().optional(),
69
- defaultLimit: zod_1.z.number().optional(),
70
- maxLimit: zod_1.z.number().optional(),
71
- autoTranslate: zod_1.z.boolean().optional(),
72
- translateModel: zod_1.z.string().optional(),
84
+ similarityThreshold: zod_1.z.number().min(0).max(1).describe('Max cosine distance 0–1; lower = stricter (default 0.6)').optional(),
85
+ defaultLimit: zod_1.z.number().int().describe('Default result count (default 5)').optional(),
86
+ maxLimit: zod_1.z.number().int().describe('Hard cap on results (default 20)').optional(),
73
87
  }).optional(),
74
88
  reranker: zod_1.z.object({
75
- enabled: zod_1.z.boolean().optional(),
76
- model: zod_1.z.string().optional(),
77
- url: zod_1.z.string().optional(),
78
- overfetch: zod_1.z.number().int().min(1).max(20).optional(),
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(),
93
+ model: zod_1.z.string().describe('Reranker model name (e.g. qllama/bge-reranker-v2-m3)').optional(),
94
+ url: zod_1.z.string().describe('Full URL of the rerank endpoint (default derived from provider + baseURL)').optional(),
95
+ overfetch: zod_1.z.number().int().min(1).max(20).describe('Candidate multiplier before reranking (limit × N, default 3)').optional(),
79
96
  }).optional(),
80
97
  logging: zod_1.z.object({
81
- level: zod_1.z.enum(['debug', 'info', 'warn', 'error']).optional(),
82
- dir: zod_1.z.string().optional(),
98
+ level: zod_1.z.enum(['debug', 'info', 'warn', 'error']).describe('File log level (default error)').optional(),
99
+ dir: zod_1.z.string().describe('Base log directory; "false" to disable file logging').optional(),
83
100
  }).optional(),
84
- workspaceRoot: zod_1.z.string().optional(),
101
+ workspaceRoot: zod_1.z.string().describe('Full path to the directory containing all your projects').optional(),
85
102
  });
86
103
  const FIELD_TYPES = {
87
104
  'db.host': 'string',
@@ -89,18 +106,24 @@ const FIELD_TYPES = {
89
106
  'db.user': 'string',
90
107
  'db.password': 'string',
91
108
  'db.name': 'string',
92
- 'ollama.host': 'string',
93
- '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',
94
115
  'indexing.chunkSize': 'number',
95
116
  'indexing.chunkOverlap': 'number',
96
117
  'indexing.embedBatchSize': 'number',
97
118
  'indexing.astBatchSize': 'number',
119
+ 'indexing.chunkStrategy': 'string',
98
120
  'search.similarityThreshold': 'number',
99
121
  'search.defaultLimit': 'number',
100
122
  'search.maxLimit': 'number',
101
- 'search.autoTranslate': 'boolean',
102
- 'search.translateModel': 'string',
103
123
  'reranker.enabled': 'boolean',
124
+ 'reranker.provider': 'string',
125
+ 'reranker.baseURL': 'string',
126
+ 'reranker.apiKey': 'string',
104
127
  'reranker.model': 'string',
105
128
  'reranker.url': 'string',
106
129
  'reranker.overfetch': 'number',
@@ -109,6 +132,39 @@ const FIELD_TYPES = {
109
132
  'workspaceRoot': 'string',
110
133
  };
111
134
  exports.ALL_KEYS = Object.keys(FIELD_TYPES);
135
+ /**
136
+ * Single source of truth for built-in defaults, keyed by raw config.json dotted
137
+ * key. Consumed both by the web dashboard (`GET /api/defaults`) and by
138
+ * `resolvedSchema` in `src/config.ts`, whose `.default(...)` calls reference
139
+ * these values — so the runtime default and the dashboard-displayed default
140
+ * cannot diverge. `as const` keeps the literal types so those `.default()`
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(...)`.
146
+ */
147
+ exports.CONFIG_DEFAULTS = {
148
+ 'db.port': 5433,
149
+ 'embedding.provider': 'ollama',
150
+ 'embedding.apiKey': '',
151
+ 'embedding.model': constants_1.DEFAULT_EMBED_MODEL,
152
+ 'embedding.batchSize': 32,
153
+ 'indexing.chunkSize': 1500,
154
+ 'indexing.chunkOverlap': 200,
155
+ 'indexing.embedBatchSize': 5,
156
+ 'indexing.astBatchSize': 3000,
157
+ 'indexing.chunkStrategy': 'ast',
158
+ 'search.similarityThreshold': 0.6,
159
+ 'search.defaultLimit': 5,
160
+ 'search.maxLimit': 20,
161
+ 'reranker.enabled': false,
162
+ 'reranker.provider': 'openai-compatible',
163
+ 'reranker.apiKey': '',
164
+ 'reranker.model': '',
165
+ 'reranker.overfetch': 3,
166
+ 'logging.level': 'error',
167
+ };
112
168
  function readConfigJson() {
113
169
  if (!fs.existsSync(exports.CONFIG_FILE))
114
170
  return null;
@@ -127,17 +183,86 @@ function readConfigJson() {
127
183
  throw new Error(`Invalid JSON in ${exports.CONFIG_FILE}: ${e.message}\n` +
128
184
  `Fix the file manually or delete it and run: rag-setup`);
129
185
  }
130
- const result = jsonSchema.safeParse(parsed);
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;
228
+ }
229
+ /**
230
+ * Validate a parsed config object against the Zod schema. Unknown keys (e.g.
231
+ * the "$schema" editor reference) are stripped. Throws with a caller-friendly
232
+ * message on validation failure. Pure — no file I/O.
233
+ */
234
+ function parseConfig(raw, source = exports.CONFIG_FILE) {
235
+ const result = jsonSchema.safeParse(raw);
131
236
  if (!result.success) {
132
237
  const issues = result.error.issues.map(i => ` ${i.path.join('.')}: ${i.message}`).join('\n');
133
- throw new Error(`Invalid config in ${exports.CONFIG_FILE}:\n${issues}\n` +
238
+ throw new Error(`Invalid config in ${source}:\n${issues}\n` +
134
239
  `Fix with: rag-config set <key> <value>`);
135
240
  }
136
241
  return result.data;
137
242
  }
243
+ /** Build the JSON Schema object generated from the Zod config schema. Pure. */
244
+ function buildJsonSchema() {
245
+ return {
246
+ $id: 'https://github.com/oleksandr-antoniuk/RAG/config.schema.json',
247
+ title: 'codesearch-mcp config',
248
+ ...zod_1.z.toJSONSchema(jsonSchema),
249
+ };
250
+ }
138
251
  function writeConfigJson(data) {
139
252
  fs.mkdirSync(exports.CONFIG_DIR, { recursive: true });
140
- fs.writeFileSync(exports.CONFIG_FILE, JSON.stringify(data, null, 2) + '\n', { mode: 0o600 });
253
+ writeSchemaFile();
254
+ // $schema first so editors pick it up; readConfigJson strips it on validation.
255
+ const withSchema = { $schema: SCHEMA_REF, ...data };
256
+ fs.writeFileSync(exports.CONFIG_FILE, JSON.stringify(withSchema, null, 2) + '\n', { mode: 0o600 });
257
+ }
258
+ /**
259
+ * Generate a JSON Schema from the Zod config schema and write it next to
260
+ * config.json. Editors resolve it via the "$schema" reference and provide
261
+ * key autocomplete, type hints, and inline validation while editing.
262
+ */
263
+ function writeSchemaFile() {
264
+ fs.mkdirSync(exports.CONFIG_DIR, { recursive: true });
265
+ fs.writeFileSync(exports.SCHEMA_FILE, JSON.stringify(buildJsonSchema(), null, 2) + '\n');
141
266
  }
142
267
  function getNestedValue(cfg, key) {
143
268
  const parts = key.split('.');
@@ -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";