codesearch-mcp 0.2.2-alpha → 0.3.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 (158) hide show
  1. package/CHANGELOG.md +65 -1
  2. package/README.md +60 -33
  3. package/dist/app.js +46 -22
  4. package/dist/src/chunking/AstChunker.d.ts +12 -0
  5. package/dist/src/chunking/AstChunker.js +30 -0
  6. package/dist/src/chunking/CharacterChunkStrategy.d.ts +8 -0
  7. package/dist/src/chunking/CharacterChunkStrategy.js +15 -0
  8. package/dist/src/chunking/DeclarationChunkStrategy.d.ts +15 -0
  9. package/dist/src/chunking/DeclarationChunkStrategy.js +87 -0
  10. package/dist/src/chunking/FragmentMerger.d.ts +11 -0
  11. package/dist/src/chunking/FragmentMerger.js +61 -0
  12. package/dist/src/chunking/ImportOnlyFilter.d.ts +5 -0
  13. package/dist/src/chunking/ImportOnlyFilter.js +14 -0
  14. package/dist/src/chunking/LangchainTextSplitter.d.ts +21 -0
  15. package/dist/src/chunking/LangchainTextSplitter.js +133 -0
  16. package/dist/src/chunking/MinSizeSplitPolicy.d.ts +9 -0
  17. package/dist/src/chunking/MinSizeSplitPolicy.js +18 -0
  18. package/dist/src/chunking/RunPacker.d.ts +12 -0
  19. package/dist/src/chunking/RunPacker.js +84 -0
  20. package/dist/src/chunking/create-chunker.d.ts +3 -0
  21. package/dist/src/chunking/create-chunker.js +15 -0
  22. package/dist/src/chunking/interfaces.d.ts +27 -0
  23. package/dist/src/chunking/interfaces.js +2 -0
  24. package/dist/src/commands/ConfigCommand.js +1 -9
  25. package/dist/src/commands/EvalCommand.d.ts +11 -0
  26. package/dist/src/commands/EvalCommand.js +264 -0
  27. package/dist/src/commands/IndexCommand.js +70 -66
  28. package/dist/src/commands/McpCommand.js +2 -8
  29. package/dist/src/commands/WebCommand.js +2 -5
  30. package/dist/src/config-error.d.ts +0 -4
  31. package/dist/src/config-error.js +0 -4
  32. package/dist/src/config-store.d.ts +1 -54
  33. package/dist/src/config-store.js +8 -69
  34. package/dist/src/config.d.ts +8 -17
  35. package/dist/src/config.js +35 -34
  36. package/dist/src/constants.d.ts +2 -28
  37. package/dist/src/constants.js +6 -28
  38. package/dist/src/container.d.ts +6 -0
  39. package/dist/src/container.js +12 -3
  40. package/dist/src/db/index.d.ts +1 -1
  41. package/dist/src/db/index.js +1 -2
  42. package/dist/src/db/schema.d.ts +22 -29
  43. package/dist/src/db/schema.js +3 -14
  44. package/dist/src/declarations/DeclarationTree.d.ts +19 -0
  45. package/dist/src/declarations/DeclarationTree.js +66 -0
  46. package/dist/src/declarations/DeclarationTreeBuilder.d.ts +22 -0
  47. package/dist/src/declarations/DeclarationTreeBuilder.js +179 -0
  48. package/dist/src/declarations/SourceLines.d.ts +37 -0
  49. package/dist/src/declarations/SourceLines.js +415 -0
  50. package/dist/src/dependency-traversal.d.ts +10 -0
  51. package/dist/src/dependency-traversal.js +39 -0
  52. package/dist/src/enriched-prompt.d.ts +0 -5
  53. package/dist/src/enriched-prompt.js +0 -5
  54. package/dist/src/error-record.d.ts +12 -0
  55. package/dist/src/error-record.js +248 -0
  56. package/dist/src/eval-corpus.d.ts +16 -0
  57. package/dist/src/eval-corpus.js +169 -0
  58. package/dist/src/eval-results.d.ts +15 -0
  59. package/dist/src/eval-results.js +397 -0
  60. package/dist/src/file-utils.d.ts +10 -0
  61. package/dist/src/file-utils.js +19 -1
  62. package/dist/src/graph.d.ts +0 -25
  63. package/dist/src/graph.js +0 -28
  64. package/dist/src/help.js +4 -1
  65. package/dist/src/hnsw-settings.d.ts +3 -0
  66. package/dist/src/hnsw-settings.js +24 -0
  67. package/dist/src/interfaces/IAstParser.d.ts +5 -30
  68. package/dist/src/interfaces/IChunker.d.ts +3 -12
  69. package/dist/src/interfaces/ICorpusInspector.d.ts +7 -0
  70. package/dist/src/interfaces/ICorpusInspector.js +2 -0
  71. package/dist/src/interfaces/IDatabase.d.ts +2 -36
  72. package/dist/src/interfaces/IEmbedder.d.ts +0 -9
  73. package/dist/src/interfaces/ILogger.d.ts +1 -11
  74. package/dist/src/interfaces/IMcpToolHandler.d.ts +0 -5
  75. package/dist/src/interfaces/IMonorepoResolver.d.ts +0 -22
  76. package/dist/src/interfaces/IProjectDiscovery.d.ts +0 -17
  77. package/dist/src/interfaces/IRetrievalEvaluator.d.ts +216 -0
  78. package/dist/src/interfaces/IRetrievalEvaluator.js +2 -0
  79. package/dist/src/interfaces/ISearcher.d.ts +26 -20
  80. package/dist/src/interfaces/index.d.ts +1 -0
  81. package/dist/src/interrupt.js +0 -2
  82. package/dist/src/owner-path.d.ts +9 -10
  83. package/dist/src/owner-path.js +11 -23
  84. package/dist/src/project-scope.d.ts +7 -0
  85. package/dist/src/project-scope.js +37 -0
  86. package/dist/src/project-selector.d.ts +1 -0
  87. package/dist/src/project-selector.js +13 -6
  88. package/dist/src/retrieval-eval.d.ts +24 -0
  89. package/dist/src/retrieval-eval.js +500 -0
  90. package/dist/src/search/result-shaping.d.ts +4 -0
  91. package/dist/src/search/result-shaping.js +19 -0
  92. package/dist/src/services/AstParserService.d.ts +28 -23
  93. package/dist/src/services/AstParserService.js +391 -98
  94. package/dist/src/services/DatabaseService.d.ts +2 -19
  95. package/dist/src/services/DatabaseService.js +44 -81
  96. package/dist/src/services/EmbedderFactory.d.ts +0 -4
  97. package/dist/src/services/EmbedderFactory.js +0 -5
  98. package/dist/src/services/IndexerService.d.ts +2 -10
  99. package/dist/src/services/IndexerService.js +29 -45
  100. package/dist/src/services/InfinityEmbedder.d.ts +0 -21
  101. package/dist/src/services/InfinityEmbedder.js +0 -15
  102. package/dist/src/services/LoggerService.d.ts +7 -5
  103. package/dist/src/services/LoggerService.js +37 -41
  104. package/dist/src/services/McpServer.d.ts +25 -10
  105. package/dist/src/services/McpServer.js +101 -64
  106. package/dist/src/services/MonorepoAwareDiscoveryService.d.ts +1 -45
  107. package/dist/src/services/MonorepoAwareDiscoveryService.js +1 -51
  108. package/dist/src/services/NxMonorepoResolver.d.ts +0 -29
  109. package/dist/src/services/NxMonorepoResolver.js +5 -56
  110. package/dist/src/services/OpenAICompatibleEmbedder.d.ts +0 -6
  111. package/dist/src/services/OpenAICompatibleEmbedder.js +1 -14
  112. package/dist/src/services/ProjectDiscoveryService.d.ts +0 -4
  113. package/dist/src/services/ProjectDiscoveryService.js +0 -4
  114. package/dist/src/services/SearcherService.d.ts +127 -70
  115. package/dist/src/services/SearcherService.js +526 -305
  116. package/dist/src/services/VoyageEmbedder.d.ts +0 -6
  117. package/dist/src/services/VoyageEmbedder.js +0 -6
  118. package/dist/src/services/WebServerService.d.ts +1 -53
  119. package/dist/src/services/WebServerService.js +11 -57
  120. package/dist/src/services/http-retry.d.ts +0 -7
  121. package/dist/src/services/http-retry.js +1 -10
  122. package/dist/src/services/infinity-provisioning.d.ts +0 -74
  123. package/dist/src/services/infinity-provisioning.js +5 -88
  124. package/dist/src/services/legacy-ollama-index.d.ts +0 -16
  125. package/dist/src/services/legacy-ollama-index.js +0 -23
  126. package/dist/src/services/reranker.d.ts +0 -9
  127. package/dist/src/services/reranker.js +46 -12
  128. package/dist/src/setup.d.ts +11 -23
  129. package/dist/src/setup.js +98 -71
  130. package/dist/src/symbol-name.d.ts +0 -12
  131. package/dist/src/symbol-name.js +0 -13
  132. package/dist/src/symbol-rows.d.ts +2 -11
  133. package/dist/src/symbol-rows.js +5 -13
  134. package/dist/src/tool-call-log.d.ts +2 -0
  135. package/dist/src/tool-call-log.js +24 -0
  136. package/dist/src/tools/FindSymbolTool.js +14 -5
  137. package/dist/src/tools/GetCallersTool.d.ts +3 -1
  138. package/dist/src/tools/GetCallersTool.js +16 -6
  139. package/dist/src/tools/GetDependenciesTool.d.ts +3 -1
  140. package/dist/src/tools/GetDependenciesTool.js +15 -8
  141. package/dist/src/tools/GetDependentsTool.d.ts +3 -1
  142. package/dist/src/tools/GetDependentsTool.js +31 -16
  143. package/dist/src/tools/ListFilesTool.d.ts +3 -1
  144. package/dist/src/tools/ListFilesTool.js +14 -12
  145. package/dist/src/tools/ListProjectsTool.js +5 -1
  146. package/dist/src/tools/ReadFileRangeTool.d.ts +3 -1
  147. package/dist/src/tools/ReadFileRangeTool.js +12 -11
  148. package/dist/src/tools/ReadFileTool.d.ts +3 -1
  149. package/dist/src/tools/ReadFileTool.js +12 -10
  150. package/dist/src/tools/SearchCodebaseTool.js +7 -2
  151. package/dist/src/tools/format-helpers.d.ts +10 -1
  152. package/dist/src/tools/format-helpers.js +145 -8
  153. package/dist/src/web/dashboard.d.ts +0 -8
  154. package/dist/src/web/dashboard.js +7 -10
  155. package/package.json +10 -9
  156. package/dist/src/services/AstChunker.d.ts +0 -42
  157. package/dist/src/services/AstChunker.js +0 -158
  158. package/docs/web-dashboard.md +0 -206
@@ -6,7 +6,6 @@ import * as schema from '../db/schema';
6
6
  type Schema = typeof schema;
7
7
  export type DrizzleDb = NodePgDatabase<Schema>;
8
8
  export type DrizzleTx = NodePgTransaction<Schema, ExtractTablesWithRelations<Schema>>;
9
- export declare const CURRENT_SYMBOL_INDEX_VERSION = 2;
10
9
  export declare class DatabaseService implements IDatabase {
11
10
  private config;
12
11
  private pool;
@@ -14,17 +13,13 @@ export declare class DatabaseService implements IDatabase {
14
13
  constructor(config: Config);
15
14
  get orm(): DrizzleDb;
16
15
  query<T = any>(sql: string, params?: any[]): Promise<T>;
16
+ queryWithLocalSettings<T = any>(settings: Record<string, string | number>, sql: string, params?: any[]): Promise<T>;
17
+ pgvectorVersion(): Promise<string | null>;
17
18
  transaction<T>(fn: (tx: DrizzleTx) => Promise<T>): Promise<T>;
18
19
  destroy(): Promise<void>;
19
20
  initDb(): Promise<void>;
20
21
  initDbWithSchema(): Promise<void>;
21
- /**
22
- * Create `code_chunk` at `dimensions` if it does not exist, plus its HNSW and
23
- * (serviceName, filePath) indexes and the additive `contentHash`/`ownerPath`
24
- * columns for pre-existing tables. Idempotent — safe to call repeatedly.
25
- */
26
22
  ensureChunkTable(dimensions: number): Promise<void>;
27
- /** Drop and recreate `code_chunk` at `dimensions` — used on a dimension change. */
28
23
  recreateChunkTable(dimensions: number): Promise<void>;
29
24
  readIndexMeta(): Promise<IndexMeta | null>;
30
25
  writeIndexMeta(meta: IndexMeta): Promise<void>;
@@ -32,18 +27,6 @@ export declare class DatabaseService implements IDatabase {
32
27
  deleteFiles(project: string, filePaths: string[]): Promise<void>;
33
28
  listIndexedProjects(): Promise<string[]>;
34
29
  deleteProject(name: string): Promise<void>;
35
- listProjectsWithIndexRows(): Promise<string[]>;
36
- /**
37
- * Among `projectNames` (or every project with rows when null), those with at
38
- * least one row in `code_chunk`/`code_symbol` and a `symbol_index_version`
39
- * below `CURRENT_SYMBOL_INDEX_VERSION` (a missing row counts as version 1).
40
- * A missing `symbol_index_version` table (un-migrated DB, `rag-mcp`/`rag-web`
41
- * bootstrapped with `initDb`) counts as "no marker rows" — every project with
42
- * rows is then outdated. Any other database error propagates.
43
- */
44
- listOutdatedProjects(projectNames: string[] | null): Promise<string[]>;
45
- upsertSymbolIndexVersion(project: string): Promise<void>;
46
- /** Guard against SQL injection — the dimension is interpolated into DDL. */
47
30
  private assertDim;
48
31
  private assertMinPostgresVersion;
49
32
  private ensureDatabaseExists;
@@ -33,14 +33,15 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.DatabaseService = exports.CURRENT_SYMBOL_INDEX_VERSION = void 0;
36
+ exports.DatabaseService = void 0;
37
37
  const node_postgres_1 = require("drizzle-orm/node-postgres");
38
38
  const pg_1 = require("pg");
39
39
  const drizzle_orm_1 = require("drizzle-orm");
40
40
  const schema = __importStar(require("../db/schema"));
41
41
  const db_1 = require("../db");
42
42
  const MIN_PG_VERSION_NUM = 150000;
43
- exports.CURRENT_SYMBOL_INDEX_VERSION = 2;
43
+ const SETTING_NAME = /^[a-z_]+(\.[a-z_]+)*$/;
44
+ const SETTING_VALUE = /^[A-Za-z0-9_]+$/;
44
45
  class DatabaseService {
45
46
  config;
46
47
  pool;
@@ -53,6 +54,41 @@ class DatabaseService {
53
54
  const result = await this.pool.query(sql, params);
54
55
  return result.rows;
55
56
  }
57
+ async queryWithLocalSettings(settings, sql, params) {
58
+ const setStatements = Object.entries(settings).map(([name, value]) => {
59
+ if (!SETTING_NAME.test(name) || !SETTING_VALUE.test(String(value))) {
60
+ throw new Error(`DatabaseService: invalid local setting ${JSON.stringify(name)} = ${JSON.stringify(value)}`);
61
+ }
62
+ return `SET LOCAL ${name} = ${value}`;
63
+ });
64
+ const client = await this.pool.connect();
65
+ let began = false;
66
+ let broken;
67
+ const onError = (err) => { broken ??= err; };
68
+ client.on('error', onError);
69
+ try {
70
+ await client.query('BEGIN');
71
+ began = true;
72
+ for (const statement of setStatements)
73
+ await client.query(statement);
74
+ const result = await client.query(sql, params);
75
+ await client.query('COMMIT');
76
+ return result.rows;
77
+ }
78
+ catch (err) {
79
+ if (began)
80
+ await client.query('ROLLBACK').catch((e) => { broken ??= e; });
81
+ throw err;
82
+ }
83
+ finally {
84
+ client.removeListener('error', onError);
85
+ client.release(broken);
86
+ }
87
+ }
88
+ async pgvectorVersion() {
89
+ const rows = await this.query(`SELECT extversion FROM pg_extension WHERE extname = 'vector'`);
90
+ return rows[0]?.extversion ?? null;
91
+ }
56
92
  transaction(fn) {
57
93
  return this._db.transaction(fn);
58
94
  }
@@ -70,17 +106,8 @@ class DatabaseService {
70
106
  async initDbWithSchema() {
71
107
  await this.ensureDatabaseExists();
72
108
  await this.initDb();
73
- // Read as the very first statement on the application database, before any
74
- // CREATE TABLE / CREATE INDEX / ALTER TABLE / DROP — `NULLS NOT DISTINCT`
75
- // (used below) requires PostgreSQL 15+, and the DROP/CREATE pair must never
76
- // run against an older server (it would fail after the DROP and leave
77
- // code_symbol without a unique index).
78
109
  await this.assertMinPostgresVersion();
79
- // code_chunk table — created at the legacy default width (1024) so search
80
- // tools always have a table. IndexerService recreates it at the resolved
81
- // width before indexing when the active model's dimension differs.
82
110
  await this.ensureChunkTable(1024);
83
- // index_meta table — source of truth for the model that built the index.
84
111
  await this.query(`
85
112
  CREATE TABLE IF NOT EXISTS index_meta (
86
113
  id BOOLEAN PRIMARY KEY DEFAULT true,
@@ -90,7 +117,6 @@ class DatabaseService {
90
117
  "indexedAt" TIMESTAMPTZ NOT NULL DEFAULT now()
91
118
  )
92
119
  `);
93
- // code_dependency table
94
120
  const [{ existsDep }] = await this.query(`
95
121
  SELECT EXISTS (
96
122
  SELECT 1 FROM information_schema.tables
@@ -115,7 +141,6 @@ class DatabaseService {
115
141
  await this.query(`DROP INDEX IF EXISTS idx_dep_from`);
116
142
  await this.query(`DROP INDEX IF EXISTS idx_dep_to`);
117
143
  await this.query(`DROP INDEX IF EXISTS idx_dep_project`);
118
- // code_symbol table
119
144
  const [{ existsSym }] = await this.query(`
120
145
  SELECT EXISTS (
121
146
  SELECT 1 FROM information_schema.tables
@@ -140,23 +165,13 @@ class DatabaseService {
140
165
  await this.query(`CREATE INDEX IF NOT EXISTS idx_symbol_file ON code_symbol (project, "filePath")`);
141
166
  await this.query(`ALTER TABLE code_symbol ADD COLUMN IF NOT EXISTS container TEXT`);
142
167
  await this.query(`ALTER TABLE code_symbol ADD COLUMN IF NOT EXISTS "isStatic" BOOLEAN NOT NULL DEFAULT false`);
143
- // DDL is transactional in PostgreSQL — either both statements apply or
144
- // neither does, so a failure here never leaves code_symbol without a
145
- // unique index (the old idx_symbol_unique is dropped only once the new
146
- // one is confirmed to exist).
168
+ await this.query(`ALTER TABLE code_symbol ADD COLUMN IF NOT EXISTS "target" TEXT`);
147
169
  await this.transaction(async (tx) => {
148
170
  await tx.execute('DROP INDEX IF EXISTS idx_symbol_unique');
149
171
  await tx.execute('CREATE UNIQUE INDEX IF NOT EXISTS idx_symbol_unique_v2 ' +
150
172
  'ON code_symbol (project, "filePath", container, name, kind, line) NULLS NOT DISTINCT');
151
173
  });
152
174
  await this.query(`
153
- CREATE TABLE IF NOT EXISTS symbol_index_version (
154
- project TEXT PRIMARY KEY,
155
- version INT NOT NULL
156
- )
157
- `);
158
- // code_call table
159
- await this.query(`
160
175
  CREATE TABLE IF NOT EXISTS code_call (
161
176
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
162
177
  project TEXT NOT NULL,
@@ -173,11 +188,6 @@ class DatabaseService {
173
188
  await this.query(`CREATE INDEX IF NOT EXISTS idx_call_callee_file ON code_call (project, "calleeFile")`);
174
189
  await this.query(`CREATE INDEX IF NOT EXISTS idx_call_from ON code_call (project, "fromFile")`);
175
190
  }
176
- /**
177
- * Create `code_chunk` at `dimensions` if it does not exist, plus its HNSW and
178
- * (serviceName, filePath) indexes and the additive `contentHash`/`ownerPath`
179
- * columns for pre-existing tables. Idempotent — safe to call repeatedly.
180
- */
181
191
  async ensureChunkTable(dimensions) {
182
192
  const dim = this.assertDim(dimensions);
183
193
  await this.query(`
@@ -190,15 +200,16 @@ class DatabaseService {
190
200
  embedding vector(${dim}) NOT NULL,
191
201
  "lastModified" DOUBLE PRECISION,
192
202
  "contentHash" VARCHAR(64),
193
- "ownerPath" VARCHAR
203
+ "ownerPath" VARCHAR,
204
+ "endLine" INT
194
205
  )
195
206
  `);
196
207
  await this.query(`ALTER TABLE code_chunk ADD COLUMN IF NOT EXISTS "contentHash" varchar(64) NULL`);
197
208
  await this.query(`ALTER TABLE code_chunk ADD COLUMN IF NOT EXISTS "ownerPath" varchar NULL`);
209
+ await this.query(`ALTER TABLE code_chunk ADD COLUMN IF NOT EXISTS "endLine" integer NULL`);
198
210
  await this.query(`CREATE INDEX IF NOT EXISTS idx_code_chunk_embedding ON code_chunk USING hnsw (embedding vector_cosine_ops)`);
199
211
  await this.query(`CREATE INDEX IF NOT EXISTS idx_code_chunk_service_file ON code_chunk ("serviceName", "filePath")`);
200
212
  }
201
- /** Drop and recreate `code_chunk` at `dimensions` — used on a dimension change. */
202
213
  async recreateChunkTable(dimensions) {
203
214
  const dim = this.assertDim(dimensions);
204
215
  await this.query(`DROP TABLE IF EXISTS code_chunk`);
@@ -211,7 +222,6 @@ class DatabaseService {
211
222
  return row ? { provider: row.provider, model: row.model, dimensions: Number(row.dimensions) } : null;
212
223
  }
213
224
  catch {
214
- // Table absent (un-migrated DB) — treat as "no metadata".
215
225
  return null;
216
226
  }
217
227
  }
@@ -237,8 +247,6 @@ class DatabaseService {
237
247
  async deleteFiles(project, filePaths) {
238
248
  if (filePaths.length === 0)
239
249
  return;
240
- // PostgreSQL limits ~32 767 bind params per statement; OR queries bind the
241
- // array twice (fromFile + calleeFile), so cap each batch at 10 000 entries.
242
250
  const BATCH = 10_000;
243
251
  await this.transaction(async (tx) => {
244
252
  for (let i = 0; i < filePaths.length; i += BATCH) {
@@ -268,51 +276,6 @@ class DatabaseService {
268
276
  await tx.delete(db_1.codeDependency).where((0, drizzle_orm_1.eq)(db_1.codeDependency.project, name));
269
277
  });
270
278
  }
271
- async listProjectsWithIndexRows() {
272
- const rows = await this.query(`SELECT DISTINCT name FROM (
273
- SELECT "serviceName" AS name FROM code_chunk
274
- UNION
275
- SELECT project AS name FROM code_symbol
276
- ) t`);
277
- return rows.map(r => r.name);
278
- }
279
- /**
280
- * Among `projectNames` (or every project with rows when null), those with at
281
- * least one row in `code_chunk`/`code_symbol` and a `symbol_index_version`
282
- * below `CURRENT_SYMBOL_INDEX_VERSION` (a missing row counts as version 1).
283
- * A missing `symbol_index_version` table (un-migrated DB, `rag-mcp`/`rag-web`
284
- * bootstrapped with `initDb`) counts as "no marker rows" — every project with
285
- * rows is then outdated. Any other database error propagates.
286
- */
287
- async listOutdatedProjects(projectNames) {
288
- const rowsUnion = `
289
- SELECT "serviceName" AS name FROM code_chunk
290
- UNION
291
- SELECT project AS name FROM code_symbol
292
- `;
293
- const filterSql = projectNames ? 'AND t.name = ANY($1)' : '';
294
- const params = projectNames ? [projectNames] : [];
295
- try {
296
- const rows = await this.query(`SELECT DISTINCT t.name FROM (${rowsUnion}) t
297
- WHERE NOT EXISTS (
298
- SELECT 1 FROM symbol_index_version v
299
- WHERE v.project = t.name AND v.version >= ${exports.CURRENT_SYMBOL_INDEX_VERSION}
300
- )
301
- ${filterSql}`, params);
302
- return rows.map(r => r.name);
303
- }
304
- catch (err) {
305
- if (!/relation "symbol_index_version" does not exist/i.test(err.message))
306
- throw err;
307
- const rows = await this.query(`SELECT DISTINCT t.name FROM (${rowsUnion}) t ${projectNames ? 'WHERE t.name = ANY($1)' : ''}`, params);
308
- return rows.map(r => r.name);
309
- }
310
- }
311
- async upsertSymbolIndexVersion(project) {
312
- await this.query(`INSERT INTO symbol_index_version (project, version) VALUES ($1, $2)
313
- ON CONFLICT (project) DO UPDATE SET version = EXCLUDED.version`, [project, exports.CURRENT_SYMBOL_INDEX_VERSION]);
314
- }
315
- /** Guard against SQL injection — the dimension is interpolated into DDL. */
316
279
  assertDim(dimensions) {
317
280
  if (!Number.isInteger(dimensions) || dimensions <= 0 || dimensions > 2000) {
318
281
  throw new Error(`Invalid vector dimension: ${dimensions} (must be an integer 1–2000)`);
@@ -326,7 +289,7 @@ class DatabaseService {
326
289
  versionNum = Number(rows[0]?.v);
327
290
  }
328
291
  catch (err) {
329
- throw new Error(`Failed to read PostgreSQL server_version_num (requires PostgreSQL 15+): ${err.message}`);
292
+ throw new Error(`Failed to read PostgreSQL server_version_num (requires PostgreSQL 15+): ${err.message}`, { cause: err });
330
293
  }
331
294
  if (!Number.isFinite(versionNum) || versionNum < MIN_PG_VERSION_NUM) {
332
295
  throw new Error(`PostgreSQL 15+ is required (server_version_num=${versionNum}) — ` +
@@ -360,7 +323,7 @@ class DatabaseService {
360
323
  catch (err) {
361
324
  await client.end().catch(() => { });
362
325
  const msg = err instanceof Error ? err.message : String(err);
363
- throw new Error(`Failed to ensure database "${database}" exists: ${msg}`);
326
+ throw new Error(`Failed to ensure database "${database}" exists: ${msg}`, { cause: err });
364
327
  }
365
328
  }
366
329
  }
@@ -2,8 +2,4 @@ import type { Config } from '../config';
2
2
  import type { IEmbedder } from '../interfaces/IEmbedder';
3
3
  import type { ILogger } from '../interfaces/ILogger';
4
4
  import { type EmbedderDeps } from './OpenAICompatibleEmbedder';
5
- /**
6
- * Construct the `IEmbedder` implementation selected by `config.embedding.provider`.
7
- * The default (`infinity`) is the recommended local backend.
8
- */
9
5
  export declare function createEmbedder(config: Config, logger?: ILogger, deps?: EmbedderDeps): IEmbedder;
@@ -5,10 +5,6 @@ const LoggerService_1 = require("./LoggerService");
5
5
  const OpenAICompatibleEmbedder_1 = require("./OpenAICompatibleEmbedder");
6
6
  const InfinityEmbedder_1 = require("./InfinityEmbedder");
7
7
  const VoyageEmbedder_1 = require("./VoyageEmbedder");
8
- /**
9
- * Construct the `IEmbedder` implementation selected by `config.embedding.provider`.
10
- * The default (`infinity`) is the recommended local backend.
11
- */
12
8
  function createEmbedder(config, logger = LoggerService_1.LoggerService.silent(), deps = {}) {
13
9
  switch (config.embedding.provider) {
14
10
  case 'openai-compatible':
@@ -18,7 +14,6 @@ function createEmbedder(config, logger = LoggerService_1.LoggerService.silent(),
18
14
  case 'voyage':
19
15
  return new VoyageEmbedder_1.VoyageEmbedder(config, logger, deps);
20
16
  default: {
21
- // The Zod enum should already reject this; guard anyway for a clear message.
22
17
  const p = config.embedding.provider;
23
18
  throw new Error(`Unknown embedding provider: "${p}". Expected one of: openai-compatible, voyage, infinity.`);
24
19
  }
@@ -3,6 +3,7 @@ import type { IEmbedder } from '../interfaces/IEmbedder';
3
3
  import type { IAstParser } from '../interfaces/IAstParser';
4
4
  import type { IChunker } from '../interfaces/IChunker';
5
5
  import type { IDatabase } from '../interfaces/IDatabase';
6
+ import type { DiscoveredProject } from '../interfaces/IProjectDiscovery';
6
7
  import type { ILogger } from '../interfaces/ILogger';
7
8
  import type { IndexMeta } from '../interfaces/IDatabase';
8
9
  export interface IndexStats {
@@ -22,16 +23,6 @@ export declare class IndexerService {
22
23
  private config;
23
24
  private logger;
24
25
  constructor(db: IDatabase, embedder: IEmbedder, ast: IAstParser, chunker: IChunker, config: Config, logger?: ILogger);
25
- /**
26
- * Resolve the active embedding dimension (config override → registry → probe),
27
- * reconcile it against `index_meta`, and prepare `code_chunk` accordingly:
28
- * a dimension change drops+recreates the table; a same-dimension model/provider
29
- * change clears its rows (vector spaces are not comparable across models); an
30
- * unchanged model keeps the rows for incremental indexing. Back-compat: an
31
- * existing populated table with no `index_meta` row is treated as the legacy
32
- * `ollama`/`bge-m3`/1024 index. Writes `index_meta` to reflect the active model.
33
- * Call once per run, before the scan loop.
34
- */
35
26
  prepareEmbeddingSchema(): Promise<IndexMeta>;
36
27
  private reconcileIndex;
37
28
  private countChunks;
@@ -39,6 +30,7 @@ export declare class IndexerService {
39
30
  scanAndIndex(dir: string, serviceName: string, force?: boolean, signal?: AbortSignal, preCollectedFiles?: string[], opts?: {
40
31
  rootTsConfigPath?: string;
41
32
  }): Promise<IndexStats>;
33
+ scanProjects(targets: DiscoveredProject[], force: boolean, signal: AbortSignal, filesPerProject: string[][]): Promise<IndexStats[]>;
42
34
  pruneProjects(keep: string[]): Promise<{
43
35
  removed: string[];
44
36
  failed: string[];
@@ -40,13 +40,17 @@ const crypto_1 = require("crypto");
40
40
  const cli_progress_1 = require("cli-progress");
41
41
  const drizzle_orm_1 = require("drizzle-orm");
42
42
  const owner_path_1 = require("../owner-path");
43
+ const DeclarationTree_1 = require("../declarations/DeclarationTree");
44
+ const SourceLines_1 = require("../declarations/SourceLines");
43
45
  const enriched_prompt_1 = require("../enriched-prompt");
44
46
  const symbol_rows_1 = require("../symbol-rows");
45
47
  const db_1 = require("../db");
46
48
  const file_utils_1 = require("../file-utils");
47
49
  const LoggerService_1 = require("./LoggerService");
48
50
  const constants_1 = require("../constants");
51
+ const error_record_1 = require("../error-record");
49
52
  const SCHEMA_EXTENSIONS = /\.(prisma|graphql|gql)$/;
53
+ const JSX_EXTENSIONS = /\.(tsx|jsx|js)$/;
50
54
  class IndexerService {
51
55
  db;
52
56
  embedder;
@@ -62,16 +66,6 @@ class IndexerService {
62
66
  this.config = config;
63
67
  this.logger = logger;
64
68
  }
65
- /**
66
- * Resolve the active embedding dimension (config override → registry → probe),
67
- * reconcile it against `index_meta`, and prepare `code_chunk` accordingly:
68
- * a dimension change drops+recreates the table; a same-dimension model/provider
69
- * change clears its rows (vector spaces are not comparable across models); an
70
- * unchanged model keeps the rows for incremental indexing. Back-compat: an
71
- * existing populated table with no `index_meta` row is treated as the legacy
72
- * `ollama`/`bge-m3`/1024 index. Writes `index_meta` to reflect the active model.
73
- * Call once per run, before the scan loop.
74
- */
75
69
  async prepareEmbeddingSchema() {
76
70
  const { provider, model, dimensions: override } = this.config.embedding;
77
71
  const next = {
@@ -86,13 +80,11 @@ class IndexerService {
86
80
  if (!meta) {
87
81
  const chunkCount = await this.countChunks();
88
82
  if (chunkCount === 0) {
89
- // Fresh index — create the table at the resolved width and record it.
90
83
  await this.db.recreateChunkTable(next.dimensions);
91
84
  await this.db.writeIndexMeta(next);
92
85
  this.logger.debug('IndexerService: prepared fresh index', { ...next });
93
86
  return next;
94
87
  }
95
- // Legacy index (pre-index_meta): assume the historical default model.
96
88
  await this.reconcileIndex({ provider: 'ollama', model: 'bge-m3', dimensions: 1024 }, next);
97
89
  return next;
98
90
  }
@@ -118,9 +110,6 @@ class IndexerService {
118
110
  return Number(rows[0]?.n ?? 0);
119
111
  }
120
112
  catch (err) {
121
- // A missing table means a fresh install → 0. Any other (transient) error
122
- // must propagate — returning 0 here could wrongly recreate/drop a populated
123
- // table on the `!meta` path.
124
113
  if (/does not exist/i.test(err.message))
125
114
  return 0;
126
115
  throw err;
@@ -142,9 +131,6 @@ class IndexerService {
142
131
  const startTime = Date.now();
143
132
  this.logger.debug('IndexerService: scanAndIndex start', { serviceName, dir, force });
144
133
  if (force) {
145
- // Reset incremental state and clear the project's symbol/call rows in one
146
- // transaction: a file whose re-ingestion later fails must hold no rows at
147
- // all, never rows in the previous (pre-member-extraction) format.
148
134
  await this.db.transaction(async (tx) => {
149
135
  await tx.update(db_1.codeChunk)
150
136
  .set({ lastModified: null, contentHash: null })
@@ -250,16 +236,10 @@ class IndexerService {
250
236
  ]);
251
237
  }
252
238
  catch (err) {
253
- this.logger.error('IndexerService: mtime refresh batch failed', { reason: err.message });
239
+ this.logger.error('IndexerService: mtime refresh batch failed', { err });
254
240
  }
255
241
  }
256
242
  this.ast.clearCache(dir);
257
- // The marker is written exactly when the scan reaches here without having
258
- // been interrupted — never on the early-return abort path above, and never
259
- // when anything in this method throws (the error propagates instead).
260
- if (!signal?.aborted) {
261
- await this.db.upsertSymbolIndexVersion(serviceName);
262
- }
263
243
  stats.elapsedSec = Math.round((Date.now() - startTime) / 100) / 10;
264
244
  this.logger.debug('IndexerService: project indexed', {
265
245
  project: serviceName,
@@ -271,6 +251,16 @@ class IndexerService {
271
251
  });
272
252
  return stats;
273
253
  }
254
+ async scanProjects(targets, force, signal, filesPerProject) {
255
+ const allStats = [];
256
+ for (let i = 0; i < targets.length; i++) {
257
+ if (signal.aborted)
258
+ break;
259
+ const stats = await this.scanAndIndex(targets[i].path, targets[i].name, force, signal, filesPerProject[i], { rootTsConfigPath: targets[i].rootTsConfigPath ?? undefined });
260
+ allStats.push(stats);
261
+ }
262
+ return allStats;
263
+ }
274
264
  async pruneProjects(keep) {
275
265
  if (keep.length === 0)
276
266
  return { removed: [], failed: [] };
@@ -286,7 +276,7 @@ class IndexerService {
286
276
  }
287
277
  catch (err) {
288
278
  console.error(`Failed to prune project ${name}: ${err.message}`);
289
- this.logger.error('IndexerService: pruneProject failed', { name, reason: err.message });
279
+ this.logger.error('IndexerService: pruneProject failed', { name, err });
290
280
  failed.push(name);
291
281
  }
292
282
  }
@@ -303,21 +293,17 @@ class IndexerService {
303
293
  .from(db_1.codeChunk)
304
294
  .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(db_1.codeChunk.serviceName, serviceName), (0, drizzle_orm_1.eq)(db_1.codeChunk.filePath, relFilePath)))
305
295
  .limit(1);
306
- // Fast path: mtime unchanged — skip without reading file
307
296
  if (existing?.lastModified === mtimeMs) {
308
297
  return { status: 'skipped', chunks: 0 };
309
298
  }
310
299
  const t0 = Date.now();
311
300
  const code = fs.readFileSync(filePath, 'utf-8');
312
301
  const hash = IndexerService.hashContent(code);
313
- // Content unchanged (mtime drifted — DB transfer, git clone, touch)
314
302
  if (existing !== undefined && existing.contentHash !== null && existing.contentHash === hash) {
315
303
  return { status: 'skipped', chunks: 0, pendingMtimeRefresh: relFilePath };
316
304
  }
317
305
  const tAst = Date.now();
318
- const { edges: rawEdges, symbols: rawSymbols, members: rawMembers = [], calls: rawCalls, ownerRanges: rawOwnerRanges = [], } = analysis ?? { edges: [], symbols: [], calls: [] };
319
- // astMs is near-zero — actual AST time is spent upfront in analyzeProject()
320
- // Normalize edges to workspace-relative paths
306
+ const { edges: rawEdges, symbols: rawSymbols, members: rawMembers = [], calls: rawCalls, graphqlNames: rawGraphqlNames = [], } = analysis ?? { edges: [], symbols: [], calls: [] };
321
307
  const edges = rawEdges
322
308
  .filter(e => e.toFile.startsWith(this.config.workspaceRoot))
323
309
  .map(e => ({
@@ -325,7 +311,6 @@ class IndexerService {
325
311
  fromFile: path.relative(this.config.workspaceRoot, e.fromFile),
326
312
  toFile: path.relative(this.config.workspaceRoot, e.toFile),
327
313
  }));
328
- // Normalize call edges to workspace-relative paths; keep only resolved internal calls
329
314
  const calls = rawCalls
330
315
  .filter(c => c.calleeFile.startsWith(this.config.workspaceRoot))
331
316
  .map(c => ({
@@ -338,26 +323,25 @@ class IndexerService {
338
323
  line: c.line,
339
324
  col: c.col,
340
325
  }));
341
- // symbols ∪ members → code_symbol rows: overload sets collapse to their
342
- // representative row, then a final dedup by (container, name, kind, line).
343
- const symbolRows = (0, symbol_rows_1.buildSymbolRows)(rawSymbols, rawMembers).map(s => ({ ...s, filePath: relFilePath }));
326
+ const symbolRows = (0, symbol_rows_1.buildSymbolRows)(rawSymbols, rawMembers, rawGraphqlNames).map(s => ({ ...s, filePath: relFilePath }));
327
+ const promptEdges = edges.filter(e => e.depType !== 'import-side-effect');
344
328
  const astMs = Date.now() - tAst;
329
+ const jsx = JSX_EXTENSIONS.test(filePath);
345
330
  const chunks = await this.chunker.chunk(code, {
346
331
  strategy: this.config.indexer.chunkStrategy,
347
332
  isSchemaFile: SCHEMA_EXTENSIONS.test(filePath),
348
- symbols: rawSymbols,
333
+ declarations: analysis?.declarations,
334
+ jsx,
349
335
  });
350
336
  const relPath = path.relative(projectRoot, filePath);
351
- // Ownership ranges (decorator-inclusive, symbols ∪ members minus
352
- // method_signature) are built by AstParserService — the deepest range
353
- // containing a chunk's start line wins.
354
- const ownerRanges = rawOwnerRanges;
337
+ const sourceLines = new SourceLines_1.SourceLines(code, { jsx });
338
+ const ownerTree = analysis?.declarations ?? DeclarationTree_1.DeclarationTree.EMPTY;
355
339
  const t2 = Date.now();
356
340
  const records = [];
357
341
  const embedBatchSize = this.config.embedding.batchSize;
358
342
  for (let i = 0; i < chunks.length; i += embedBatchSize) {
359
343
  const batch = chunks.slice(i, i + embedBatchSize);
360
- const prompts = batch.map(piece => (0, enriched_prompt_1.getEnrichedPrompt)(piece, relPath, serviceName, edges, rawSymbols));
344
+ const prompts = batch.map(piece => (0, enriched_prompt_1.getEnrichedPrompt)(piece, relPath, serviceName, promptEdges, rawSymbols));
361
345
  const vectors = await this.embedder.embedBatch(prompts, 'document');
362
346
  for (let j = 0; j < batch.length; j++) {
363
347
  const piece = batch[j];
@@ -367,7 +351,8 @@ class IndexerService {
367
351
  embedding: `[${vectors[j]}]`,
368
352
  lastModified: mtimeMs,
369
353
  contentHash: hash,
370
- ownerPath: (0, owner_path_1.resolveOwnerPath)(piece.startLine, ownerRanges),
354
+ ownerPath: (0, owner_path_1.ownerOf)(piece, ownerTree, sourceLines),
355
+ endLine: piece.startLine + (piece.content.match(/\n/g)?.length ?? 0),
371
356
  });
372
357
  }
373
358
  }
@@ -398,9 +383,8 @@ class IndexerService {
398
383
  return { status: 'indexed', chunks: chunks.length };
399
384
  }
400
385
  catch (err) {
401
- const reason = err.message;
402
- console.error(`\n[indexer] Failed: ${filePath} — ${reason}`);
403
- this.logger.error('IndexerService: failed to index file', { file: filePath, reason });
386
+ console.error(`\n[indexer] Failed: ${filePath} — ${(0, error_record_1.rootCauseMessage)(err)}`);
387
+ this.logger.error('IndexerService: failed to index file', { file: filePath, err });
404
388
  return { status: 'failed', chunks: 0 };
405
389
  }
406
390
  }
@@ -4,35 +4,14 @@ import { OpenAICompatibleEmbedder, type EmbedderDeps } from './OpenAICompatibleE
4
4
  import { serverRoot, probeInfinityHealth, listInfinityModels, type ProvisioningDeps } from './infinity-provisioning';
5
5
  export { serverRoot, probeInfinityHealth, listInfinityModels };
6
6
  export type { CommandResult } from './infinity-provisioning';
7
- /** Extra injection seams for `InfinityEmbedder` beyond the shared `EmbedderDeps`. */
8
7
  export interface InfinityDeps extends EmbedderDeps, ProvisioningDeps {
9
- /**
10
- * Path to the managed docker-compose.yml. Defaults to the file `rag-setup`
11
- * writes into `~/.config/codesearch-mcp/`. A missing file (or one without an
12
- * `infinity` service) means "external backend" — the Docker path is skipped.
13
- */
14
8
  composePath?: string;
15
9
  }
16
- /**
17
- * Embedder for a local Infinity server. Inherits the OpenAI-style `/embeddings`
18
- * HTTP path, batching, retry, and reranker wiring from `OpenAICompatibleEmbedder`
19
- * unchanged — it adds ONLY container-readiness behavior (`ensureModelAvailable`,
20
- * `listModels`), delegating to the standalone `infinity-provisioning` primitive
21
- * because Infinity binds its model set at container startup and exposes no
22
- * runtime model-pull API.
23
- */
24
10
  export declare class InfinityEmbedder extends OpenAICompatibleEmbedder {
25
11
  private readonly base;
26
12
  private readonly composePath;
27
13
  private readonly provisioningDeps;
28
14
  constructor(config: Config, logger?: ILogger, deps?: InfinityDeps);
29
- /**
30
- * Index-time embed readiness (see `local-backend-provisioning`): verifies only
31
- * the embedding model — indexing does not use the reranker, so a missing
32
- * reranker model never blocks or restarts indexing. Whenever a recreate is
33
- * needed, the full configured set is written so an already-served reranker
34
- * model is never dropped. Called by `IndexCommand` before indexing.
35
- */
36
15
  ensureModelAvailable(): Promise<void>;
37
16
  listModels(): Promise<string[]>;
38
17
  }
@@ -42,14 +42,6 @@ Object.defineProperty(exports, "serverRoot", { enumerable: true, get: function (
42
42
  Object.defineProperty(exports, "probeInfinityHealth", { enumerable: true, get: function () { return infinity_provisioning_1.probeInfinityHealth; } });
43
43
  Object.defineProperty(exports, "listInfinityModels", { enumerable: true, get: function () { return infinity_provisioning_1.listInfinityModels; } });
44
44
  const path = __importStar(require("path"));
45
- /**
46
- * Embedder for a local Infinity server. Inherits the OpenAI-style `/embeddings`
47
- * HTTP path, batching, retry, and reranker wiring from `OpenAICompatibleEmbedder`
48
- * unchanged — it adds ONLY container-readiness behavior (`ensureModelAvailable`,
49
- * `listModels`), delegating to the standalone `infinity-provisioning` primitive
50
- * because Infinity binds its model set at container startup and exposes no
51
- * runtime model-pull API.
52
- */
53
45
  class InfinityEmbedder extends OpenAICompatibleEmbedder_1.OpenAICompatibleEmbedder {
54
46
  base;
55
47
  composePath;
@@ -65,13 +57,6 @@ class InfinityEmbedder extends OpenAICompatibleEmbedder_1.OpenAICompatibleEmbedd
65
57
  readinessTimeoutMs: deps.readinessTimeoutMs,
66
58
  };
67
59
  }
68
- /**
69
- * Index-time embed readiness (see `local-backend-provisioning`): verifies only
70
- * the embedding model — indexing does not use the reranker, so a missing
71
- * reranker model never blocks or restarts indexing. Whenever a recreate is
72
- * needed, the full configured set is written so an already-served reranker
73
- * model is never dropped. Called by `IndexCommand` before indexing.
74
- */
75
60
  async ensureModelAvailable() {
76
61
  await (0, infinity_provisioning_1.ensureBackendReady)({
77
62
  baseURL: this.config.embedding.baseURL,
@@ -3,17 +3,19 @@ import type { ILogger } from '../interfaces/ILogger';
3
3
  export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
4
4
  export declare class LoggerService implements ILogger {
5
5
  private readonly logger;
6
- private readonly metricsLogger;
7
- constructor(logger: pino.Logger, metricsLogger?: pino.Logger | null);
6
+ private readonly context;
7
+ private constructor();
8
8
  static silent(): LoggerService;
9
9
  static fromConfig(cfg: {
10
10
  level: string;
11
11
  dir?: string;
12
- }, name?: string): LoggerService;
13
- private static buildFromPath;
12
+ }, name?: string, bindings?: Record<string, unknown>): LoggerService;
13
+ static toStream(level: LogLevel, stream: pino.DestinationStream, bindings?: Record<string, unknown>): LoggerService;
14
+ private static fileTransport;
15
+ private static build;
14
16
  debug(msg: string, data?: Record<string, unknown>): void;
15
17
  info(msg: string, data?: Record<string, unknown>): void;
16
18
  warn(msg: string, data?: Record<string, unknown>): void;
17
19
  error(msg: string, data?: Record<string, unknown>): void;
18
- metric(msg: string, data?: Record<string, unknown>): void;
20
+ withContext<T>(context: Record<string, unknown>, fn: () => Promise<T>): Promise<T>;
19
21
  }