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,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createEmbedder = createEmbedder;
4
+ const LoggerService_1 = require("./LoggerService");
5
+ const OllamaEmbedder_1 = require("./OllamaEmbedder");
6
+ const OpenAICompatibleEmbedder_1 = require("./OpenAICompatibleEmbedder");
7
+ const InfinityEmbedder_1 = require("./InfinityEmbedder");
8
+ const VoyageEmbedder_1 = require("./VoyageEmbedder");
9
+ /**
10
+ * Construct the `IEmbedder` implementation selected by `config.embedding.provider`.
11
+ * The default (`ollama`) keeps existing installs working unchanged.
12
+ */
13
+ function createEmbedder(config, logger = LoggerService_1.LoggerService.silent(), deps = {}) {
14
+ switch (config.embedding.provider) {
15
+ case 'openai-compatible':
16
+ return new OpenAICompatibleEmbedder_1.OpenAICompatibleEmbedder(config, logger, deps);
17
+ case 'infinity':
18
+ return new InfinityEmbedder_1.InfinityEmbedder(config, logger, deps);
19
+ case 'voyage':
20
+ return new VoyageEmbedder_1.VoyageEmbedder(config, logger, deps);
21
+ case 'ollama':
22
+ return new OllamaEmbedder_1.OllamaEmbedder(config, logger, deps);
23
+ default: {
24
+ // The Zod enum should already reject this; guard anyway for a clear message.
25
+ const p = config.embedding.provider;
26
+ throw new Error(`Unknown embedding provider: "${p}". Expected one of: ollama, openai-compatible, voyage, infinity.`);
27
+ }
28
+ }
29
+ }
@@ -1,8 +1,10 @@
1
1
  import type { Config } from '../config';
2
2
  import type { IEmbedder } from '../interfaces/IEmbedder';
3
3
  import type { IAstParser } from '../interfaces/IAstParser';
4
- import type { DatabaseService } from './DatabaseService';
5
- import { LoggerService } from './LoggerService';
4
+ import type { IChunker } from '../interfaces/IChunker';
5
+ import type { IDatabase } from '../interfaces/IDatabase';
6
+ import type { ILogger } from '../interfaces/ILogger';
7
+ import type { IndexMeta } from '../interfaces/IDatabase';
6
8
  export interface IndexStats {
7
9
  serviceName: string;
8
10
  indexed: number;
@@ -16,11 +18,23 @@ export declare class IndexerService {
16
18
  private db;
17
19
  private embedder;
18
20
  private ast;
21
+ private chunker;
19
22
  private config;
20
23
  private logger;
21
- private readonly jsSplitter;
22
- private readonly schemaSplitter;
23
- constructor(db: DatabaseService, embedder: IEmbedder, ast: IAstParser, config: Config, logger?: LoggerService);
24
+ 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
+ prepareEmbeddingSchema(): Promise<IndexMeta>;
36
+ private reconcileIndex;
37
+ private countChunks;
24
38
  backfillSymbols(signal?: AbortSignal): Promise<{
25
39
  total: number;
26
40
  processed: number;
@@ -33,16 +47,12 @@ export declare class IndexerService {
33
47
  skipped: number;
34
48
  failed: number;
35
49
  }>;
36
- discoverProjects(rootDir: string): {
37
- name: string;
38
- path: string;
39
- }[];
40
50
  private writeDepEdges;
41
- scanAndIndex(dir: string, serviceName: string, force?: boolean, signal?: AbortSignal, preCollectedFiles?: string[]): Promise<IndexStats>;
42
- private getSplitter;
51
+ scanAndIndex(dir: string, serviceName: string, force?: boolean, signal?: AbortSignal, preCollectedFiles?: string[], opts?: {
52
+ rootTsConfigPath?: string;
53
+ }): Promise<IndexStats>;
43
54
  private static inferRole;
44
55
  private getEnrichedPrompt;
45
- private embedText;
46
56
  private static hashContent;
47
57
  private ingestFile;
48
58
  }
@@ -37,36 +37,92 @@ exports.IndexerService = void 0;
37
37
  const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
39
  const crypto_1 = require("crypto");
40
- const textsplitters_1 = require("@langchain/textsplitters");
41
40
  const cli_progress_1 = require("cli-progress");
42
41
  const drizzle_orm_1 = require("drizzle-orm");
42
+ const owner_path_1 = require("../owner-path");
43
43
  const db_1 = require("../db");
44
44
  const file_utils_1 = require("../file-utils");
45
45
  const LoggerService_1 = require("./LoggerService");
46
+ const constants_1 = require("../constants");
46
47
  const SCHEMA_EXTENSIONS = /\.(prisma|graphql|gql)$/;
47
48
  class IndexerService {
48
49
  db;
49
50
  embedder;
50
51
  ast;
52
+ chunker;
51
53
  config;
52
54
  logger;
53
- jsSplitter;
54
- schemaSplitter;
55
- constructor(db, embedder, ast, config, logger = LoggerService_1.LoggerService.silent()) {
55
+ constructor(db, embedder, ast, chunker, config, logger = LoggerService_1.LoggerService.silent()) {
56
56
  this.db = db;
57
57
  this.embedder = embedder;
58
58
  this.ast = ast;
59
+ this.chunker = chunker;
59
60
  this.config = config;
60
61
  this.logger = logger;
61
- this.jsSplitter = textsplitters_1.RecursiveCharacterTextSplitter.fromLanguage('js', {
62
- chunkSize: config.indexer.chunkSize,
63
- chunkOverlap: config.indexer.chunkOverlap,
64
- });
65
- this.schemaSplitter = new textsplitters_1.RecursiveCharacterTextSplitter({
66
- chunkSize: config.indexer.chunkSize,
67
- chunkOverlap: config.indexer.chunkOverlap,
68
- separators: ['\n\n', '\n', ' '],
69
- });
62
+ }
63
+ /**
64
+ * Resolve the active embedding dimension (config override → registry → probe),
65
+ * reconcile it against `index_meta`, and prepare `code_chunk` accordingly:
66
+ * a dimension change drops+recreates the table; a same-dimension model/provider
67
+ * change clears its rows (vector spaces are not comparable across models); an
68
+ * unchanged model keeps the rows for incremental indexing. Back-compat: an
69
+ * existing populated table with no `index_meta` row is treated as the legacy
70
+ * `ollama`/`bge-m3`/1024 index. Writes `index_meta` to reflect the active model.
71
+ * Call once per run, before the scan loop.
72
+ */
73
+ async prepareEmbeddingSchema() {
74
+ const { provider, model, dimensions: override } = this.config.embedding;
75
+ const next = {
76
+ provider,
77
+ model,
78
+ dimensions: await (0, constants_1.resolveDimension)(override, model, async () => {
79
+ const probe = await this.embedder.embed('probe', 'document');
80
+ return probe.length;
81
+ }),
82
+ };
83
+ const meta = await this.db.readIndexMeta();
84
+ if (!meta) {
85
+ const chunkCount = await this.countChunks();
86
+ if (chunkCount === 0) {
87
+ // Fresh index — create the table at the resolved width and record it.
88
+ await this.db.recreateChunkTable(next.dimensions);
89
+ await this.db.writeIndexMeta(next);
90
+ this.logger.debug('IndexerService: prepared fresh index', { ...next });
91
+ return next;
92
+ }
93
+ // Legacy index (pre-index_meta): assume the historical default model.
94
+ await this.reconcileIndex({ provider: 'ollama', model: 'bge-m3', dimensions: 1024 }, next);
95
+ return next;
96
+ }
97
+ await this.reconcileIndex(meta, next);
98
+ return next;
99
+ }
100
+ async reconcileIndex(prev, next) {
101
+ if (prev.dimensions !== next.dimensions) {
102
+ console.log(`Embedding dimension changed (${prev.dimensions} → ${next.dimensions}) — recreating code_chunk.`);
103
+ this.logger.info('IndexerService: dimension change, recreating code_chunk', { prev, next });
104
+ await this.db.recreateChunkTable(next.dimensions);
105
+ }
106
+ else if (prev.provider !== next.provider || prev.model !== next.model) {
107
+ console.log(`Embedding model changed (${prev.provider}/${prev.model} → ${next.provider}/${next.model}) — clearing index.`);
108
+ this.logger.info('IndexerService: model change, clearing code_chunk', { prev, next });
109
+ await this.db.query(`DELETE FROM code_chunk`);
110
+ }
111
+ await this.db.writeIndexMeta(next);
112
+ }
113
+ async countChunks() {
114
+ try {
115
+ const rows = await this.db.query(`SELECT COUNT(*)::int AS n FROM code_chunk`);
116
+ return Number(rows[0]?.n ?? 0);
117
+ }
118
+ catch (err) {
119
+ // A missing table means a fresh install → 0. Any other (transient) error
120
+ // must propagate — returning 0 here could wrongly recreate/drop a populated
121
+ // table on the `!meta` path.
122
+ if (/does not exist/i.test(err.message))
123
+ return 0;
124
+ throw err;
125
+ }
70
126
  }
71
127
  async backfillSymbols(signal) {
72
128
  const t0 = Date.now();
@@ -159,12 +215,6 @@ class IndexerService {
159
215
  this.logger.debug('IndexerService: backfillCalls done', { total: rows.length, processed, skipped, failed, ms: Date.now() - t0 });
160
216
  return { total: rows.length, processed, skipped, failed };
161
217
  }
162
- discoverProjects(rootDir) {
163
- return fs.readdirSync(rootDir, { withFileTypes: true })
164
- .filter(d => d.isDirectory() && !file_utils_1.IGNORE_DIRS.has(d.name))
165
- .map(d => ({ name: d.name, path: path.join(rootDir, d.name) }))
166
- .filter(p => fs.existsSync(path.join(p.path, 'package.json')));
167
- }
168
218
  async writeDepEdges(serviceName, relFilePath, edges, tx) {
169
219
  const run = async (t) => {
170
220
  await t.delete(db_1.codeDependency).where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(db_1.codeDependency.project, serviceName), (0, drizzle_orm_1.eq)(db_1.codeDependency.fromFile, relFilePath)));
@@ -177,7 +227,7 @@ class IndexerService {
177
227
  else
178
228
  await this.db.transaction(run);
179
229
  }
180
- async scanAndIndex(dir, serviceName, force = false, signal, preCollectedFiles) {
230
+ async scanAndIndex(dir, serviceName, force = false, signal, preCollectedFiles, opts) {
181
231
  const startTime = Date.now();
182
232
  this.logger.debug('IndexerService: scanAndIndex start', { serviceName, dir, force });
183
233
  if (force) {
@@ -237,7 +287,10 @@ class IndexerService {
237
287
  if (signal?.aborted)
238
288
  break;
239
289
  const batch = files.slice(batchStart, batchStart + this.config.indexer.astBatchSize);
240
- const analysisMap = await this.ast.analyzeProject(batch, serviceName, dir, { skipDependencyResolution: isLargeProject });
290
+ const analysisMap = await this.ast.analyzeProject(batch, serviceName, dir, {
291
+ skipDependencyResolution: isLargeProject,
292
+ rootTsConfigPath: opts?.rootTsConfigPath,
293
+ });
241
294
  for (const filePath of batch) {
242
295
  if (signal?.aborted) {
243
296
  bar.stop();
@@ -293,9 +346,6 @@ class IndexerService {
293
346
  });
294
347
  return stats;
295
348
  }
296
- getSplitter(filePath) {
297
- return SCHEMA_EXTENSIONS.test(filePath) ? this.schemaSplitter : this.jsSplitter;
298
- }
299
349
  static inferRole(relPath) {
300
350
  const p = relPath.toLowerCase();
301
351
  if (p.includes('/services/') || p.endsWith('.service.ts'))
@@ -320,9 +370,9 @@ class IndexerService {
320
370
  return 'Utility';
321
371
  return null;
322
372
  }
323
- getEnrichedPrompt(chunk, relPath, serviceName, _meta, edges, symbols) {
324
- const chunkStart = (chunk.metadata?.loc?.lines?.from ?? 1);
325
- const chunkEnd = (chunk.metadata?.loc?.lines?.to ?? chunkStart);
373
+ getEnrichedPrompt(piece, relPath, serviceName, _meta, edges, symbols) {
374
+ const chunkStart = piece.startLine;
375
+ const chunkEnd = piece.endLine;
326
376
  const lines = [];
327
377
  lines.push(`Project: ${serviceName} | File: ${relPath}`);
328
378
  const role = IndexerService.inferRole(relPath);
@@ -338,12 +388,9 @@ class IndexerService {
338
388
  const depNames = [...new Set(edges.map(e => path.basename(e.toFile, path.extname(e.toFile)).split('.')[0]))];
339
389
  lines.push(`Dependencies: ${depNames.join(', ')}`);
340
390
  }
341
- lines.push(`Code: ${chunk.pageContent}`);
391
+ lines.push(`Code: ${piece.content}`);
342
392
  return lines.join('\n');
343
393
  }
344
- async embedText(prompt) {
345
- return this.embedder.embed(prompt);
346
- }
347
394
  static hashContent(content) {
348
395
  return (0, crypto_1.createHash)('sha256').update(content).digest('hex');
349
396
  }
@@ -367,7 +414,7 @@ class IndexerService {
367
414
  return { status: 'skipped', chunks: 0, pendingMtimeRefresh: relFilePath };
368
415
  }
369
416
  const tAst = Date.now();
370
- const { edges: rawEdges, symbols: rawSymbols, calls: rawCalls } = analysis ?? { edges: [], symbols: [], calls: [] };
417
+ const { edges: rawEdges, symbols: rawSymbols, calls: rawCalls, ownerRanges: rawOwnerRanges = [] } = analysis ?? { edges: [], symbols: [], calls: [] };
371
418
  // astMs is near-zero — actual AST time is spent upfront in analyzeProject()
372
419
  // Normalize edges to workspace-relative paths
373
420
  const edges = rawEdges
@@ -393,25 +440,38 @@ class IndexerService {
393
440
  // Normalize symbols: replace absolute filePath with workspace-relative, strip endLine (not stored)
394
441
  const symbols = [...new Map(rawSymbols.map(s => ({ ...s, filePath: relFilePath })).map(s => [`${s.name}:${s.kind}`, s])).values()].map(({ project, filePath: fp, name, kind, signature, line, col }) => ({ project, filePath: fp, name, kind, signature, line, col }));
395
442
  const astMs = Date.now() - tAst;
396
- const chunks = await this.getSplitter(filePath).createDocuments([code]);
443
+ const chunks = await this.chunker.chunk(code, {
444
+ strategy: this.config.indexer.chunkStrategy,
445
+ isSchemaFile: SCHEMA_EXTENSIONS.test(filePath),
446
+ symbols: rawSymbols,
447
+ });
397
448
  const relPath = path.relative(projectRoot, filePath);
449
+ // Owner-path ranges: top-level symbols (container: null) + method ranges.
450
+ // Deepest range containing a chunk's start line wins → Class.method.
451
+ const ownerRanges = [
452
+ ...rawSymbols
453
+ .filter(s => s.line != null)
454
+ .map(s => ({ name: s.name, container: null, line: s.line, endLine: s.endLine ?? s.line })),
455
+ ...rawOwnerRanges,
456
+ ];
398
457
  const t2 = Date.now();
399
458
  const records = [];
400
- for (let i = 0; i < chunks.length; i += this.config.indexer.batchSize) {
401
- const batch = chunks.slice(i, i + this.config.indexer.batchSize);
402
- const embedded = await Promise.all(batch.map(async (chunk) => {
403
- const startLine = (chunk.metadata?.loc?.lines?.from ?? 1);
404
- const prompt = this.getEnrichedPrompt(chunk, relPath, serviceName, meta ?? 'missing', edges, rawSymbols);
405
- const embedding = await this.embedText(prompt);
406
- return {
407
- serviceName, filePath: relFilePath, startLine,
408
- content: chunk.pageContent,
409
- embedding: `[${embedding}]`,
459
+ const embedBatchSize = this.config.embedding.batchSize;
460
+ for (let i = 0; i < chunks.length; i += embedBatchSize) {
461
+ const batch = chunks.slice(i, i + embedBatchSize);
462
+ const prompts = batch.map(piece => this.getEnrichedPrompt(piece, relPath, serviceName, meta ?? 'missing', edges, rawSymbols));
463
+ const vectors = await this.embedder.embedBatch(prompts, 'document');
464
+ for (let j = 0; j < batch.length; j++) {
465
+ const piece = batch[j];
466
+ records.push({
467
+ serviceName, filePath: relFilePath, startLine: piece.startLine,
468
+ content: piece.content,
469
+ embedding: `[${vectors[j]}]`,
410
470
  lastModified: mtimeMs,
411
471
  contentHash: hash,
412
- };
413
- }));
414
- records.push(...embedded);
472
+ ownerPath: (0, owner_path_1.resolveOwnerPath)(piece.startLine, ownerRanges),
473
+ });
474
+ }
415
475
  }
416
476
  const embedMs = Date.now() - t2;
417
477
  const t3 = Date.now();
@@ -0,0 +1,79 @@
1
+ import type { Config } from '../config';
2
+ import type { ILogger } from '../interfaces/ILogger';
3
+ import { OpenAICompatibleEmbedder, type EmbedderDeps } from './OpenAICompatibleEmbedder';
4
+ /** Result of a shell command run, mirroring the parts of `spawnSync` we use. */
5
+ export interface CommandResult {
6
+ status: number | null;
7
+ stdout?: string;
8
+ }
9
+ /**
10
+ * Extra injection seams for `InfinityEmbedder` beyond the shared `EmbedderDeps`.
11
+ * All optional and defaulted to the real implementations; tests inject fakes so
12
+ * `ensureModelAvailable()` runs without a live Infinity server or Docker.
13
+ */
14
+ export interface InfinityDeps extends EmbedderDeps {
15
+ /** Runs a shell command (docker), optionally with extra env. Defaults to `spawnSync`. */
16
+ runCommand?: (cmd: string, args: string[], env?: Record<string, string>) => CommandResult;
17
+ /**
18
+ * Path to the managed docker-compose.yml. Defaults to the file `rag-setup`
19
+ * writes into `~/.config/codesearch-mcp/`. A missing file (or one without an
20
+ * `infinity` service) means "external backend" — the Docker path is skipped.
21
+ */
22
+ composePath?: string;
23
+ /** Sleep between readiness polls (ms). Defaults to 2000; tests pass 0. */
24
+ pollIntervalMs?: number;
25
+ }
26
+ /**
27
+ * Strip trailing slashes and a trailing `/embeddings` or `/v1` to get the server
28
+ * root — Infinity serves `/health`, `/models`, `/rerank`, `/embeddings` from the
29
+ * root, so a baseURL written with an OpenAI-style suffix must be normalized here
30
+ * to keep the probe/models URLs aligned with the embeddings URL.
31
+ */
32
+ export declare function serverRoot(baseURL: string): string;
33
+ /**
34
+ * Probe an Infinity-style `/health` endpoint. Resolves `true` when the server
35
+ * answers with a 2xx, `false` on any error or non-2xx. Reused by setup's
36
+ * `waitForInfinity`.
37
+ */
38
+ export declare function probeInfinityHealth(baseURL: string, fetchImpl?: typeof fetch, timeoutMs?: number): Promise<boolean>;
39
+ /**
40
+ * List the model ids an Infinity server currently serves via its OpenAI-style
41
+ * `/models` listing (`{ data: [{ id }] }`). Returns `[]` on any error.
42
+ */
43
+ export declare function listInfinityModels(baseURL: string, fetchImpl?: typeof fetch, timeoutMs?: number): Promise<string[]>;
44
+ /**
45
+ * Embedder for a local Infinity server. Inherits the OpenAI-style `/embeddings`
46
+ * HTTP path, batching, retry, and reranker wiring from `OpenAICompatibleEmbedder`
47
+ * unchanged — it adds ONLY container-readiness behavior (`ensureModelAvailable`),
48
+ * because Infinity binds its model set at container startup and exposes no
49
+ * runtime model-pull API.
50
+ */
51
+ export declare class InfinityEmbedder extends OpenAICompatibleEmbedder {
52
+ private readonly base;
53
+ private readonly doFetchImpl;
54
+ private readonly runCommand;
55
+ private readonly composePath;
56
+ private readonly pollIntervalMs;
57
+ private readonly log;
58
+ constructor(config: Config, logger?: ILogger, deps?: InfinityDeps);
59
+ private isHealthy;
60
+ private servedModels;
61
+ /** True when a managed compose file exists AND declares an `infinity` service. */
62
+ private hasManagedService;
63
+ /**
64
+ * Bring the Infinity backend to a serving state per the D4 decision tree:
65
+ * healthy + model served → OK
66
+ * healthy + model NOT served → throw (restart container with correct --model-id)
67
+ * unreachable + managed → `docker compose up -d infinity`, wait /health
68
+ * unreachable + external → print manual instructions (no Docker touched)
69
+ *
70
+ * Called by `IndexCommand` before `prepareEmbeddingSchema()`.
71
+ */
72
+ ensureModelAvailable(): Promise<void>;
73
+ /**
74
+ * Poll `/health` until ready or the timeout elapses. Generous default (30 min)
75
+ * because a CPU first boot downloads the model set and runs a lengthy per-model
76
+ * warmup before `/health` flips to 200.
77
+ */
78
+ private waitForHealthy;
79
+ }
@@ -0,0 +1,199 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.InfinityEmbedder = void 0;
37
+ exports.serverRoot = serverRoot;
38
+ exports.probeInfinityHealth = probeInfinityHealth;
39
+ exports.listInfinityModels = listInfinityModels;
40
+ const fs = __importStar(require("fs"));
41
+ const path = __importStar(require("path"));
42
+ const child_process_1 = require("child_process");
43
+ const LoggerService_1 = require("./LoggerService");
44
+ const config_store_1 = require("../config-store");
45
+ const OpenAICompatibleEmbedder_1 = require("./OpenAICompatibleEmbedder");
46
+ /**
47
+ * Strip trailing slashes and a trailing `/embeddings` or `/v1` to get the server
48
+ * root — Infinity serves `/health`, `/models`, `/rerank`, `/embeddings` from the
49
+ * root, so a baseURL written with an OpenAI-style suffix must be normalized here
50
+ * to keep the probe/models URLs aligned with the embeddings URL.
51
+ */
52
+ function serverRoot(baseURL) {
53
+ return baseURL.replace(/\/+$/, '').replace(/\/(embeddings|v1)$/, '');
54
+ }
55
+ /**
56
+ * Probe an Infinity-style `/health` endpoint. Resolves `true` when the server
57
+ * answers with a 2xx, `false` on any error or non-2xx. Reused by setup's
58
+ * `waitForInfinity`.
59
+ */
60
+ async function probeInfinityHealth(baseURL, fetchImpl = fetch, timeoutMs = 2000) {
61
+ try {
62
+ const res = await fetchImpl(`${serverRoot(baseURL)}/health`, {
63
+ method: 'GET',
64
+ signal: AbortSignal.timeout(timeoutMs),
65
+ });
66
+ return res.ok;
67
+ }
68
+ catch {
69
+ return false;
70
+ }
71
+ }
72
+ /**
73
+ * List the model ids an Infinity server currently serves via its OpenAI-style
74
+ * `/models` listing (`{ data: [{ id }] }`). Returns `[]` on any error.
75
+ */
76
+ async function listInfinityModels(baseURL, fetchImpl = fetch, timeoutMs = 5000) {
77
+ try {
78
+ const res = await fetchImpl(`${serverRoot(baseURL)}/models`, {
79
+ method: 'GET',
80
+ signal: AbortSignal.timeout(timeoutMs),
81
+ });
82
+ if (!res.ok)
83
+ return [];
84
+ const data = await res.json();
85
+ return (data.data ?? []).map(m => m.id).filter((id) => Boolean(id));
86
+ }
87
+ catch {
88
+ return [];
89
+ }
90
+ }
91
+ /**
92
+ * Embedder for a local Infinity server. Inherits the OpenAI-style `/embeddings`
93
+ * HTTP path, batching, retry, and reranker wiring from `OpenAICompatibleEmbedder`
94
+ * unchanged — it adds ONLY container-readiness behavior (`ensureModelAvailable`),
95
+ * because Infinity binds its model set at container startup and exposes no
96
+ * runtime model-pull API.
97
+ */
98
+ class InfinityEmbedder extends OpenAICompatibleEmbedder_1.OpenAICompatibleEmbedder {
99
+ base;
100
+ doFetchImpl;
101
+ runCommand;
102
+ composePath;
103
+ pollIntervalMs;
104
+ log;
105
+ constructor(config, logger = LoggerService_1.LoggerService.silent(), deps = {}) {
106
+ super(config, logger, deps);
107
+ this.base = serverRoot(config.embedding.baseURL);
108
+ this.doFetchImpl = deps.fetchImpl ?? fetch;
109
+ this.runCommand = deps.runCommand ?? ((cmd, args, env) => {
110
+ const r = (0, child_process_1.spawnSync)(cmd, args, { stdio: 'inherit', env: env ? { ...process.env, ...env } : process.env });
111
+ return { status: r.status, stdout: r.stdout?.toString() };
112
+ });
113
+ this.composePath = deps.composePath ?? path.join(config_store_1.CONFIG_DIR, 'docker-compose.yml');
114
+ this.pollIntervalMs = deps.pollIntervalMs ?? 2000;
115
+ this.log = logger;
116
+ }
117
+ isHealthy() {
118
+ return probeInfinityHealth(this.base, this.doFetchImpl);
119
+ }
120
+ servedModels() {
121
+ return listInfinityModels(this.base, this.doFetchImpl);
122
+ }
123
+ /** True when a managed compose file exists AND declares an `infinity` service. */
124
+ hasManagedService() {
125
+ try {
126
+ const content = fs.readFileSync(this.composePath, 'utf-8');
127
+ return /^\s*infinity:\s*$/m.test(content);
128
+ }
129
+ catch {
130
+ return false;
131
+ }
132
+ }
133
+ /**
134
+ * Bring the Infinity backend to a serving state per the D4 decision tree:
135
+ * healthy + model served → OK
136
+ * healthy + model NOT served → throw (restart container with correct --model-id)
137
+ * unreachable + managed → `docker compose up -d infinity`, wait /health
138
+ * unreachable + external → print manual instructions (no Docker touched)
139
+ *
140
+ * Called by `IndexCommand` before `prepareEmbeddingSchema()`.
141
+ */
142
+ async ensureModelAvailable() {
143
+ const model = this.config.embedding.model;
144
+ if (await this.isHealthy()) {
145
+ const served = await this.servedModels();
146
+ // A server that lists no models (older Infinity, or listing disabled) is
147
+ // trusted — the embed request will surface a real error if the model is
148
+ // truly absent.
149
+ if (served.length === 0 || served.includes(model))
150
+ return;
151
+ throw new Error(`Infinity at ${this.base} serves [${served.join(', ')}] but embedding.model is "${model}". ` +
152
+ `Restart the container with the correct --model-id (e.g. recreate the managed container ` +
153
+ `via rag-setup, or fix embedding.model).`);
154
+ }
155
+ // Unreachable.
156
+ if (this.hasManagedService()) {
157
+ console.log(`Infinity not reachable at ${this.base} — starting the managed container...`);
158
+ this.log.info('InfinityEmbedder: starting managed container', { composePath: this.composePath });
159
+ // Bind the SAME model set the config expects (embed + reranker when enabled),
160
+ // so the auto-started container matches the config — otherwise compose would
161
+ // (re)create it with the compose default and silently drop the reranker /
162
+ // serve the wrong embed model. `--force-recreate` picks up a changed set.
163
+ const modelIds = [
164
+ this.config.embedding.model,
165
+ ...(this.config.reranker.enabled && this.config.reranker.model ? [this.config.reranker.model] : []),
166
+ ].join(';');
167
+ const r = this.runCommand('docker', ['compose', '-f', this.composePath, 'up', '-d', '--force-recreate', 'infinity'], { INFINITY_MODEL_ID: modelIds });
168
+ if (r.status !== 0) {
169
+ throw new Error(`Failed to start the managed Infinity container (docker compose exited ${r.status}). ` +
170
+ `Ensure Docker is running, or start Infinity manually.`);
171
+ }
172
+ await this.waitForHealthy();
173
+ return;
174
+ }
175
+ throw new Error(`Infinity is not reachable at ${this.base} and no managed container is configured. ` +
176
+ `Start it manually, e.g.:\n` +
177
+ ` docker run -it --gpus all -p 7997:7997 michaelf34/infinity:latest \\\n` +
178
+ ` v2 --model-id ${model} --port 7997\n` +
179
+ `(drop --gpus all to run on CPU), or re-run rag-setup to provision it.`);
180
+ }
181
+ /**
182
+ * Poll `/health` until ready or the timeout elapses. Generous default (30 min)
183
+ * because a CPU first boot downloads the model set and runs a lengthy per-model
184
+ * warmup before `/health` flips to 200.
185
+ */
186
+ async waitForHealthy(maxWaitSec = 1800) {
187
+ console.log('Waiting for Infinity to be ready (first boot downloads + warms up the model set, this can take several minutes on CPU)...');
188
+ const deadline = Date.now() + maxWaitSec * 1000;
189
+ while (Date.now() < deadline) {
190
+ if (await this.isHealthy()) {
191
+ console.log(' ✓ Infinity ready.');
192
+ return;
193
+ }
194
+ await new Promise(r => setTimeout(r, this.pollIntervalMs));
195
+ }
196
+ throw new Error(`Infinity did not become ready within ${maxWaitSec}s at ${this.base}.`);
197
+ }
198
+ }
199
+ exports.InfinityEmbedder = InfinityEmbedder;
@@ -1,6 +1,7 @@
1
1
  import pino from 'pino';
2
+ import type { ILogger } from '../interfaces/ILogger';
2
3
  export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
3
- export declare class LoggerService {
4
+ export declare class LoggerService implements ILogger {
4
5
  private readonly logger;
5
6
  constructor(logger: pino.Logger);
6
7
  static silent(): LoggerService;
@@ -1,14 +1,23 @@
1
1
  import type { Config } from '../config';
2
- import { SearcherService } from './SearcherService';
3
- import { LoggerService } from './LoggerService';
2
+ import type { ISearcher } from '../interfaces/ISearcher';
3
+ import type { ILogger } from '../interfaces/ILogger';
4
+ /**
5
+ * McpServer — thin orchestration layer that delegates tool handling
6
+ * to individual IMcpToolHandler implementations (Open/Closed Principle).
7
+ *
8
+ * To add a new tool: implement IMcpToolHandler and register it in createTools().
9
+ */
4
10
  export declare class McpServer {
5
11
  private searcher;
6
12
  private config;
7
13
  private logger;
8
- constructor(searcher: SearcherService, config: Config, logger?: LoggerService);
14
+ private readonly version;
15
+ private readonly tools;
16
+ constructor(searcher: ISearcher, config: Config, logger: ILogger);
9
17
  start(startupError?: Error): Promise<void>;
10
- private formatGraphContext;
11
- private formatChunk;
12
- private formatDependencies;
13
- private formatDependents;
18
+ /**
19
+ * Factory method: creates and registers all tool handlers.
20
+ * New tools are added here without modifying the dispatch logic above.
21
+ */
22
+ private createTools;
14
23
  }