codesearch-mcp 0.2.3-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 (141) hide show
  1. package/CHANGELOG.md +31 -1
  2. package/README.md +46 -26
  3. package/dist/app.js +5 -19
  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 +4 -3
  26. package/dist/src/commands/EvalCommand.js +85 -52
  27. package/dist/src/commands/IndexCommand.js +0 -22
  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/schema.d.ts +17 -0
  41. package/dist/src/db/schema.js +1 -6
  42. package/dist/src/declarations/DeclarationTree.d.ts +19 -0
  43. package/dist/src/declarations/DeclarationTree.js +66 -0
  44. package/dist/src/declarations/DeclarationTreeBuilder.d.ts +22 -0
  45. package/dist/src/declarations/DeclarationTreeBuilder.js +179 -0
  46. package/dist/src/declarations/SourceLines.d.ts +37 -0
  47. package/dist/src/declarations/SourceLines.js +415 -0
  48. package/dist/src/dependency-traversal.d.ts +0 -6
  49. package/dist/src/dependency-traversal.js +0 -6
  50. package/dist/src/enriched-prompt.d.ts +0 -6
  51. package/dist/src/enriched-prompt.js +0 -6
  52. package/dist/src/error-record.d.ts +12 -0
  53. package/dist/src/error-record.js +248 -0
  54. package/dist/src/eval-corpus.d.ts +16 -0
  55. package/dist/src/eval-corpus.js +169 -0
  56. package/dist/src/eval-results.d.ts +6 -3
  57. package/dist/src/eval-results.js +309 -26
  58. package/dist/src/file-utils.d.ts +0 -5
  59. package/dist/src/file-utils.js +0 -5
  60. package/dist/src/graph.d.ts +0 -25
  61. package/dist/src/graph.js +0 -28
  62. package/dist/src/hnsw-settings.d.ts +3 -0
  63. package/dist/src/hnsw-settings.js +24 -0
  64. package/dist/src/interfaces/IAstParser.d.ts +2 -39
  65. package/dist/src/interfaces/IChunker.d.ts +3 -12
  66. package/dist/src/interfaces/ICorpusInspector.d.ts +7 -0
  67. package/dist/src/interfaces/ICorpusInspector.js +2 -0
  68. package/dist/src/interfaces/IDatabase.d.ts +2 -22
  69. package/dist/src/interfaces/IEmbedder.d.ts +0 -9
  70. package/dist/src/interfaces/ILogger.d.ts +1 -11
  71. package/dist/src/interfaces/IMcpToolHandler.d.ts +0 -5
  72. package/dist/src/interfaces/IMonorepoResolver.d.ts +0 -22
  73. package/dist/src/interfaces/IProjectDiscovery.d.ts +0 -17
  74. package/dist/src/interfaces/IRetrievalEvaluator.d.ts +134 -12
  75. package/dist/src/interfaces/ISearcher.d.ts +16 -5
  76. package/dist/src/interrupt.js +0 -2
  77. package/dist/src/owner-path.d.ts +9 -10
  78. package/dist/src/owner-path.js +11 -23
  79. package/dist/src/project-scope.d.ts +0 -12
  80. package/dist/src/project-scope.js +0 -12
  81. package/dist/src/project-selector.d.ts +0 -5
  82. package/dist/src/project-selector.js +0 -11
  83. package/dist/src/retrieval-eval.d.ts +14 -6
  84. package/dist/src/retrieval-eval.js +345 -59
  85. package/dist/src/search/result-shaping.d.ts +4 -0
  86. package/dist/src/search/result-shaping.js +19 -0
  87. package/dist/src/services/AstParserService.d.ts +2 -62
  88. package/dist/src/services/AstParserService.js +25 -137
  89. package/dist/src/services/DatabaseService.d.ts +2 -7
  90. package/dist/src/services/DatabaseService.js +42 -29
  91. package/dist/src/services/EmbedderFactory.d.ts +0 -4
  92. package/dist/src/services/EmbedderFactory.js +0 -5
  93. package/dist/src/services/IndexerService.d.ts +0 -16
  94. package/dist/src/services/IndexerService.js +16 -45
  95. package/dist/src/services/InfinityEmbedder.d.ts +0 -21
  96. package/dist/src/services/InfinityEmbedder.js +0 -15
  97. package/dist/src/services/LoggerService.d.ts +7 -5
  98. package/dist/src/services/LoggerService.js +37 -41
  99. package/dist/src/services/McpServer.d.ts +22 -14
  100. package/dist/src/services/McpServer.js +70 -37
  101. package/dist/src/services/MonorepoAwareDiscoveryService.d.ts +1 -45
  102. package/dist/src/services/MonorepoAwareDiscoveryService.js +1 -51
  103. package/dist/src/services/NxMonorepoResolver.d.ts +0 -29
  104. package/dist/src/services/NxMonorepoResolver.js +5 -56
  105. package/dist/src/services/OpenAICompatibleEmbedder.d.ts +0 -6
  106. package/dist/src/services/OpenAICompatibleEmbedder.js +0 -14
  107. package/dist/src/services/ProjectDiscoveryService.d.ts +0 -4
  108. package/dist/src/services/ProjectDiscoveryService.js +0 -4
  109. package/dist/src/services/SearcherService.d.ts +9 -116
  110. package/dist/src/services/SearcherService.js +95 -176
  111. package/dist/src/services/VoyageEmbedder.d.ts +0 -6
  112. package/dist/src/services/VoyageEmbedder.js +0 -6
  113. package/dist/src/services/WebServerService.d.ts +1 -53
  114. package/dist/src/services/WebServerService.js +11 -57
  115. package/dist/src/services/http-retry.d.ts +0 -7
  116. package/dist/src/services/http-retry.js +1 -10
  117. package/dist/src/services/infinity-provisioning.d.ts +0 -74
  118. package/dist/src/services/infinity-provisioning.js +5 -88
  119. package/dist/src/services/legacy-ollama-index.d.ts +0 -16
  120. package/dist/src/services/legacy-ollama-index.js +0 -23
  121. package/dist/src/services/reranker.d.ts +0 -9
  122. package/dist/src/services/reranker.js +46 -12
  123. package/dist/src/setup.d.ts +11 -23
  124. package/dist/src/setup.js +98 -71
  125. package/dist/src/symbol-name.d.ts +0 -12
  126. package/dist/src/symbol-name.js +0 -13
  127. package/dist/src/symbol-rows.d.ts +0 -14
  128. package/dist/src/symbol-rows.js +0 -14
  129. package/dist/src/tool-call-log.d.ts +2 -0
  130. package/dist/src/tool-call-log.js +24 -0
  131. package/dist/src/tools/GetDependenciesTool.js +1 -1
  132. package/dist/src/tools/GetDependentsTool.js +1 -1
  133. package/dist/src/tools/SearchCodebaseTool.js +6 -3
  134. package/dist/src/tools/format-helpers.d.ts +0 -6
  135. package/dist/src/tools/format-helpers.js +87 -14
  136. package/dist/src/web/dashboard.d.ts +0 -8
  137. package/dist/src/web/dashboard.js +7 -10
  138. package/package.json +2 -2
  139. package/dist/src/services/AstChunker.d.ts +0 -42
  140. package/dist/src/services/AstChunker.js +0 -158
  141. package/docs/web-dashboard.md +0 -206
@@ -40,6 +40,8 @@ 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
+ const SETTING_NAME = /^[a-z_]+(\.[a-z_]+)*$/;
44
+ const SETTING_VALUE = /^[A-Za-z0-9_]+$/;
43
45
  class DatabaseService {
44
46
  config;
45
47
  pool;
@@ -52,6 +54,41 @@ class DatabaseService {
52
54
  const result = await this.pool.query(sql, params);
53
55
  return result.rows;
54
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
+ }
55
92
  transaction(fn) {
56
93
  return this._db.transaction(fn);
57
94
  }
@@ -69,17 +106,8 @@ class DatabaseService {
69
106
  async initDbWithSchema() {
70
107
  await this.ensureDatabaseExists();
71
108
  await this.initDb();
72
- // Read as the very first statement on the application database, before any
73
- // CREATE TABLE / CREATE INDEX / ALTER TABLE / DROP — `NULLS NOT DISTINCT`
74
- // (used below) requires PostgreSQL 15+, and the DROP/CREATE pair must never
75
- // run against an older server (it would fail after the DROP and leave
76
- // code_symbol without a unique index).
77
109
  await this.assertMinPostgresVersion();
78
- // code_chunk table — created at the legacy default width (1024) so search
79
- // tools always have a table. IndexerService recreates it at the resolved
80
- // width before indexing when the active model's dimension differs.
81
110
  await this.ensureChunkTable(1024);
82
- // index_meta table — source of truth for the model that built the index.
83
111
  await this.query(`
84
112
  CREATE TABLE IF NOT EXISTS index_meta (
85
113
  id BOOLEAN PRIMARY KEY DEFAULT true,
@@ -89,7 +117,6 @@ class DatabaseService {
89
117
  "indexedAt" TIMESTAMPTZ NOT NULL DEFAULT now()
90
118
  )
91
119
  `);
92
- // code_dependency table
93
120
  const [{ existsDep }] = await this.query(`
94
121
  SELECT EXISTS (
95
122
  SELECT 1 FROM information_schema.tables
@@ -114,7 +141,6 @@ class DatabaseService {
114
141
  await this.query(`DROP INDEX IF EXISTS idx_dep_from`);
115
142
  await this.query(`DROP INDEX IF EXISTS idx_dep_to`);
116
143
  await this.query(`DROP INDEX IF EXISTS idx_dep_project`);
117
- // code_symbol table
118
144
  const [{ existsSym }] = await this.query(`
119
145
  SELECT EXISTS (
120
146
  SELECT 1 FROM information_schema.tables
@@ -140,16 +166,11 @@ class DatabaseService {
140
166
  await this.query(`ALTER TABLE code_symbol ADD COLUMN IF NOT EXISTS container TEXT`);
141
167
  await this.query(`ALTER TABLE code_symbol ADD COLUMN IF NOT EXISTS "isStatic" BOOLEAN NOT NULL DEFAULT false`);
142
168
  await this.query(`ALTER TABLE code_symbol ADD COLUMN IF NOT EXISTS "target" TEXT`);
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).
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
- // code_call table
153
174
  await this.query(`
154
175
  CREATE TABLE IF NOT EXISTS code_call (
155
176
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
@@ -167,11 +188,6 @@ class DatabaseService {
167
188
  await this.query(`CREATE INDEX IF NOT EXISTS idx_call_callee_file ON code_call (project, "calleeFile")`);
168
189
  await this.query(`CREATE INDEX IF NOT EXISTS idx_call_from ON code_call (project, "fromFile")`);
169
190
  }
170
- /**
171
- * Create `code_chunk` at `dimensions` if it does not exist, plus its HNSW and
172
- * (serviceName, filePath) indexes and the additive `contentHash`/`ownerPath`
173
- * columns for pre-existing tables. Idempotent — safe to call repeatedly.
174
- */
175
191
  async ensureChunkTable(dimensions) {
176
192
  const dim = this.assertDim(dimensions);
177
193
  await this.query(`
@@ -184,15 +200,16 @@ class DatabaseService {
184
200
  embedding vector(${dim}) NOT NULL,
185
201
  "lastModified" DOUBLE PRECISION,
186
202
  "contentHash" VARCHAR(64),
187
- "ownerPath" VARCHAR
203
+ "ownerPath" VARCHAR,
204
+ "endLine" INT
188
205
  )
189
206
  `);
190
207
  await this.query(`ALTER TABLE code_chunk ADD COLUMN IF NOT EXISTS "contentHash" varchar(64) NULL`);
191
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`);
192
210
  await this.query(`CREATE INDEX IF NOT EXISTS idx_code_chunk_embedding ON code_chunk USING hnsw (embedding vector_cosine_ops)`);
193
211
  await this.query(`CREATE INDEX IF NOT EXISTS idx_code_chunk_service_file ON code_chunk ("serviceName", "filePath")`);
194
212
  }
195
- /** Drop and recreate `code_chunk` at `dimensions` — used on a dimension change. */
196
213
  async recreateChunkTable(dimensions) {
197
214
  const dim = this.assertDim(dimensions);
198
215
  await this.query(`DROP TABLE IF EXISTS code_chunk`);
@@ -205,7 +222,6 @@ class DatabaseService {
205
222
  return row ? { provider: row.provider, model: row.model, dimensions: Number(row.dimensions) } : null;
206
223
  }
207
224
  catch {
208
- // Table absent (un-migrated DB) — treat as "no metadata".
209
225
  return null;
210
226
  }
211
227
  }
@@ -231,8 +247,6 @@ class DatabaseService {
231
247
  async deleteFiles(project, filePaths) {
232
248
  if (filePaths.length === 0)
233
249
  return;
234
- // PostgreSQL limits ~32 767 bind params per statement; OR queries bind the
235
- // array twice (fromFile + calleeFile), so cap each batch at 10 000 entries.
236
250
  const BATCH = 10_000;
237
251
  await this.transaction(async (tx) => {
238
252
  for (let i = 0; i < filePaths.length; i += BATCH) {
@@ -262,7 +276,6 @@ class DatabaseService {
262
276
  await tx.delete(db_1.codeDependency).where((0, drizzle_orm_1.eq)(db_1.codeDependency.project, name));
263
277
  });
264
278
  }
265
- /** Guard against SQL injection — the dimension is interpolated into DDL. */
266
279
  assertDim(dimensions) {
267
280
  if (!Number.isInteger(dimensions) || dimensions <= 0 || dimensions > 2000) {
268
281
  throw new Error(`Invalid vector dimension: ${dimensions} (must be an integer 1–2000)`);
@@ -276,7 +289,7 @@ class DatabaseService {
276
289
  versionNum = Number(rows[0]?.v);
277
290
  }
278
291
  catch (err) {
279
- 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 });
280
293
  }
281
294
  if (!Number.isFinite(versionNum) || versionNum < MIN_PG_VERSION_NUM) {
282
295
  throw new Error(`PostgreSQL 15+ is required (server_version_num=${versionNum}) — ` +
@@ -310,7 +323,7 @@ class DatabaseService {
310
323
  catch (err) {
311
324
  await client.end().catch(() => { });
312
325
  const msg = err instanceof Error ? err.message : String(err);
313
- throw new Error(`Failed to ensure database "${database}" exists: ${msg}`);
326
+ throw new Error(`Failed to ensure database "${database}" exists: ${msg}`, { cause: err });
314
327
  }
315
328
  }
316
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
  }
@@ -23,16 +23,6 @@ export declare class IndexerService {
23
23
  private config;
24
24
  private logger;
25
25
  constructor(db: IDatabase, embedder: IEmbedder, ast: IAstParser, chunker: IChunker, config: Config, logger?: ILogger);
26
- /**
27
- * Resolve the active embedding dimension (config override → registry → probe),
28
- * reconcile it against `index_meta`, and prepare `code_chunk` accordingly:
29
- * a dimension change drops+recreates the table; a same-dimension model/provider
30
- * change clears its rows (vector spaces are not comparable across models); an
31
- * unchanged model keeps the rows for incremental indexing. Back-compat: an
32
- * existing populated table with no `index_meta` row is treated as the legacy
33
- * `ollama`/`bge-m3`/1024 index. Writes `index_meta` to reflect the active model.
34
- * Call once per run, before the scan loop.
35
- */
36
26
  prepareEmbeddingSchema(): Promise<IndexMeta>;
37
27
  private reconcileIndex;
38
28
  private countChunks;
@@ -40,12 +30,6 @@ export declare class IndexerService {
40
30
  scanAndIndex(dir: string, serviceName: string, force?: boolean, signal?: AbortSignal, preCollectedFiles?: string[], opts?: {
41
31
  rootTsConfigPath?: string;
42
32
  }): Promise<IndexStats>;
43
- /**
44
- * Scan several leaf projects in one call, each with its own `rootTsConfigPath`
45
- * (a unit's children may belong to different monorepos). No pruning, no
46
- * console output — callers (CLI or a future hook) own reporting. A rejecting
47
- * `scanAndIndex` call stops the loop before the next target and propagates.
48
- */
49
33
  scanProjects(targets: DiscoveredProject[], force: boolean, signal: AbortSignal, filesPerProject: string[][]): Promise<IndexStats[]>;
50
34
  pruneProjects(keep: string[]): Promise<{
51
35
  removed: 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,7 +236,7 @@ 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);
@@ -265,12 +251,6 @@ class IndexerService {
265
251
  });
266
252
  return stats;
267
253
  }
268
- /**
269
- * Scan several leaf projects in one call, each with its own `rootTsConfigPath`
270
- * (a unit's children may belong to different monorepos). No pruning, no
271
- * console output — callers (CLI or a future hook) own reporting. A rejecting
272
- * `scanAndIndex` call stops the loop before the next target and propagates.
273
- */
274
254
  async scanProjects(targets, force, signal, filesPerProject) {
275
255
  const allStats = [];
276
256
  for (let i = 0; i < targets.length; i++) {
@@ -296,7 +276,7 @@ class IndexerService {
296
276
  }
297
277
  catch (err) {
298
278
  console.error(`Failed to prune project ${name}: ${err.message}`);
299
- this.logger.error('IndexerService: pruneProject failed', { name, reason: err.message });
279
+ this.logger.error('IndexerService: pruneProject failed', { name, err });
300
280
  failed.push(name);
301
281
  }
302
282
  }
@@ -313,21 +293,17 @@ class IndexerService {
313
293
  .from(db_1.codeChunk)
314
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)))
315
295
  .limit(1);
316
- // Fast path: mtime unchanged — skip without reading file
317
296
  if (existing?.lastModified === mtimeMs) {
318
297
  return { status: 'skipped', chunks: 0 };
319
298
  }
320
299
  const t0 = Date.now();
321
300
  const code = fs.readFileSync(filePath, 'utf-8');
322
301
  const hash = IndexerService.hashContent(code);
323
- // Content unchanged (mtime drifted — DB transfer, git clone, touch)
324
302
  if (existing !== undefined && existing.contentHash !== null && existing.contentHash === hash) {
325
303
  return { status: 'skipped', chunks: 0, pendingMtimeRefresh: relFilePath };
326
304
  }
327
305
  const tAst = Date.now();
328
- const { edges: rawEdges, symbols: rawSymbols, members: rawMembers = [], calls: rawCalls, ownerRanges: rawOwnerRanges = [], graphqlNames: rawGraphqlNames = [], } = analysis ?? { edges: [], symbols: [], calls: [] };
329
- // astMs is near-zero — actual AST time is spent upfront in analyzeProject()
330
- // Normalize edges to workspace-relative paths
306
+ const { edges: rawEdges, symbols: rawSymbols, members: rawMembers = [], calls: rawCalls, graphqlNames: rawGraphqlNames = [], } = analysis ?? { edges: [], symbols: [], calls: [] };
331
307
  const edges = rawEdges
332
308
  .filter(e => e.toFile.startsWith(this.config.workspaceRoot))
333
309
  .map(e => ({
@@ -335,7 +311,6 @@ class IndexerService {
335
311
  fromFile: path.relative(this.config.workspaceRoot, e.fromFile),
336
312
  toFile: path.relative(this.config.workspaceRoot, e.toFile),
337
313
  }));
338
- // Normalize call edges to workspace-relative paths; keep only resolved internal calls
339
314
  const calls = rawCalls
340
315
  .filter(c => c.calleeFile.startsWith(this.config.workspaceRoot))
341
316
  .map(c => ({
@@ -348,23 +323,19 @@ class IndexerService {
348
323
  line: c.line,
349
324
  col: c.col,
350
325
  }));
351
- // symbols ∪ members ∪ graphqlNames → code_symbol rows: overload sets
352
- // collapse to their representative row (graphqlNames bypass the
353
- // collapse), then a final dedup by (container, name, kind, line).
354
326
  const symbolRows = (0, symbol_rows_1.buildSymbolRows)(rawSymbols, rawMembers, rawGraphqlNames).map(s => ({ ...s, filePath: relFilePath }));
355
- // Side-effect imports bind no name, so they stay out of the Dependencies: line.
356
327
  const promptEdges = edges.filter(e => e.depType !== 'import-side-effect');
357
328
  const astMs = Date.now() - tAst;
329
+ const jsx = JSX_EXTENSIONS.test(filePath);
358
330
  const chunks = await this.chunker.chunk(code, {
359
331
  strategy: this.config.indexer.chunkStrategy,
360
332
  isSchemaFile: SCHEMA_EXTENSIONS.test(filePath),
361
- symbols: rawSymbols,
333
+ declarations: analysis?.declarations,
334
+ jsx,
362
335
  });
363
336
  const relPath = path.relative(projectRoot, filePath);
364
- // Ownership ranges (decorator-inclusive, symbols ∪ members minus
365
- // method_signature) are built by AstParserService — the deepest range
366
- // containing a chunk's start line wins.
367
- const ownerRanges = rawOwnerRanges;
337
+ const sourceLines = new SourceLines_1.SourceLines(code, { jsx });
338
+ const ownerTree = analysis?.declarations ?? DeclarationTree_1.DeclarationTree.EMPTY;
368
339
  const t2 = Date.now();
369
340
  const records = [];
370
341
  const embedBatchSize = this.config.embedding.batchSize;
@@ -380,7 +351,8 @@ class IndexerService {
380
351
  embedding: `[${vectors[j]}]`,
381
352
  lastModified: mtimeMs,
382
353
  contentHash: hash,
383
- 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),
384
356
  });
385
357
  }
386
358
  }
@@ -411,9 +383,8 @@ class IndexerService {
411
383
  return { status: 'indexed', chunks: chunks.length };
412
384
  }
413
385
  catch (err) {
414
- const reason = err.message;
415
- console.error(`\n[indexer] Failed: ${filePath} — ${reason}`);
416
- 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 });
417
388
  return { status: 'failed', chunks: 0 };
418
389
  }
419
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
  }
@@ -41,21 +41,20 @@ const pino_1 = __importDefault(require("pino"));
41
41
  const path = __importStar(require("path"));
42
42
  const os = __importStar(require("os"));
43
43
  const fs = __importStar(require("fs"));
44
+ const async_hooks_1 = require("async_hooks");
45
+ const error_record_1 = require("../error-record");
44
46
  const DEFAULT_LOG_DIR = path.join(os.homedir(), '.config', 'codesearch-mcp', 'logs');
45
47
  class LoggerService {
46
48
  logger;
47
- // Dedicated `info`-level channel for search telemetry, independent of the
48
- // main logger's configured level. Null when file logging is disabled so
49
- // metrics never reach stdout (owned by the MCP JSON transport).
50
- metricsLogger;
51
- constructor(logger, metricsLogger = null) {
49
+ context;
50
+ constructor(logger, context) {
52
51
  this.logger = logger;
53
- this.metricsLogger = metricsLogger;
52
+ this.context = context;
54
53
  }
55
54
  static silent() {
56
- return new LoggerService((0, pino_1.default)({ level: 'silent' }));
55
+ return LoggerService.build('debug', undefined);
57
56
  }
58
- static fromConfig(cfg, name) {
57
+ static fromConfig(cfg, name, bindings) {
59
58
  const level = cfg.level;
60
59
  const dirVal = cfg.dir;
61
60
  const enabled = dirVal !== 'false' && dirVal !== '0';
@@ -66,45 +65,42 @@ class LoggerService {
66
65
  ? path.join(logDir, name, `${name}.log`)
67
66
  : path.join(logDir, 'rag.log');
68
67
  }
69
- return LoggerService.buildFromPath(level, filePath);
68
+ return LoggerService.build(level, filePath ? LoggerService.fileTransport(level, filePath) : undefined, bindings);
70
69
  }
71
- static buildFromPath(level, filePath) {
72
- if (filePath)
73
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
74
- // The transport target must accept `info` so the metrics channel can emit
75
- // even when the main level is `warn`/`error`; the main logger still gates
76
- // normal calls at `level`, so nothing else leaks below it.
77
- const targetLevel = pino_1.default.levels.values[level] < pino_1.default.levels.values.info ? level : 'info';
78
- const transport = filePath
79
- ? pino_1.default.transport({
80
- targets: [{
81
- target: 'pino-roll',
82
- options: {
83
- file: filePath,
84
- frequency: 'daily',
85
- dateFormat: 'yyyy-MM-dd',
86
- mkdir: true,
87
- symlink: true,
88
- limit: { count: 30 },
89
- size: '50m',
90
- },
91
- level: targetLevel,
92
- }],
93
- })
94
- : undefined;
95
- const logger = transport
96
- ? (0, pino_1.default)({ level }, transport)
70
+ static toStream(level, stream, bindings) {
71
+ return LoggerService.build(level, stream, bindings);
72
+ }
73
+ static fileTransport(level, filePath) {
74
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
75
+ return pino_1.default.transport({
76
+ targets: [{
77
+ target: 'pino-roll',
78
+ options: {
79
+ file: filePath,
80
+ frequency: 'daily',
81
+ dateFormat: 'yyyy-MM-dd',
82
+ mkdir: true,
83
+ symlink: true,
84
+ limit: { count: 30 },
85
+ size: '50m',
86
+ },
87
+ level,
88
+ }],
89
+ });
90
+ }
91
+ static build(level, destination, bindings) {
92
+ const context = new async_hooks_1.AsyncLocalStorage();
93
+ const base = destination
94
+ ? (0, pino_1.default)({ level, serializers: error_record_1.LOG_SERIALIZERS, mixin: () => ({ ...context.getStore() }) }, destination)
97
95
  : (0, pino_1.default)({ level: 'silent' });
98
- // Child logger pinned at `info` regardless of the parent level; only used by
99
- // metric(). Null when there is no file transport (default stdout must stay
100
- // clean for the MCP transport).
101
- const metricsLogger = transport ? logger.child({}, { level: 'info' }) : null;
102
- return new LoggerService(logger, metricsLogger);
96
+ return new LoggerService(bindings ? base.child(bindings) : base, context);
103
97
  }
104
98
  debug(msg, data) { this.logger.debug(data ?? {}, msg); }
105
99
  info(msg, data) { this.logger.info(data ?? {}, msg); }
106
100
  warn(msg, data) { this.logger.warn(data ?? {}, msg); }
107
101
  error(msg, data) { this.logger.error(data ?? {}, msg); }
108
- metric(msg, data) { this.metricsLogger?.info(data ?? {}, msg); }
102
+ withContext(context, fn) {
103
+ return this.context.run(context, fn);
104
+ }
109
105
  }
110
106
  exports.LoggerService = LoggerService;