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.
- package/CHANGELOG.md +48 -0
- package/README.md +95 -6
- package/dist/app.js +70 -10
- package/dist/src/commands/ConfigCommand.d.ts +1 -0
- package/dist/src/commands/ConfigCommand.js +84 -4
- package/dist/src/commands/IndexCommand.js +35 -7
- package/dist/src/commands/McpCommand.js +9 -4
- package/dist/src/commands/{SetupCommand.d.ts → WebCommand.d.ts} +3 -2
- package/dist/src/commands/WebCommand.js +56 -0
- package/dist/src/config-store.d.ts +85 -4
- package/dist/src/config-store.js +157 -32
- package/dist/src/config.d.ts +32 -8
- package/dist/src/config.js +93 -30
- package/dist/src/constants.d.ts +35 -0
- package/dist/src/constants.js +57 -4
- package/dist/src/container.d.ts +10 -6
- package/dist/src/container.js +10 -5
- package/dist/src/db/index.d.ts +1 -1
- package/dist/src/db/index.js +2 -1
- package/dist/src/db/schema.d.ts +111 -0
- package/dist/src/db/schema.js +15 -1
- package/dist/src/graph.d.ts +60 -0
- package/dist/src/graph.js +239 -120
- package/dist/src/help.js +20 -5
- package/dist/src/interfaces/IAstParser.d.ts +20 -3
- package/dist/src/interfaces/IChunker.d.ts +24 -0
- package/dist/src/interfaces/IChunker.js +2 -0
- package/dist/src/interfaces/IDatabase.d.ts +27 -0
- package/dist/src/interfaces/IDatabase.js +2 -0
- package/dist/src/interfaces/IEmbedder.d.ts +15 -2
- package/dist/src/interfaces/ILogger.d.ts +10 -0
- package/dist/src/interfaces/ILogger.js +2 -0
- package/dist/src/interfaces/IMcpToolHandler.d.ts +22 -0
- package/dist/src/interfaces/IMcpToolHandler.js +2 -0
- package/dist/src/interfaces/IMonorepoResolver.d.ts +33 -0
- package/dist/src/interfaces/IMonorepoResolver.js +2 -0
- package/dist/src/interfaces/IProjectDiscovery.d.ts +28 -0
- package/dist/src/interfaces/IProjectDiscovery.js +2 -0
- package/dist/src/interfaces/ISearcher.d.ts +37 -0
- package/dist/src/interfaces/ISearcher.js +2 -0
- package/dist/src/interfaces/index.d.ts +8 -0
- package/dist/src/interfaces/index.js +2 -0
- package/dist/src/owner-path.d.ts +10 -0
- package/dist/src/owner-path.js +27 -0
- package/dist/src/project-selector.d.ts +1 -1
- package/dist/src/services/AstChunker.d.ts +31 -0
- package/dist/src/services/AstChunker.js +114 -0
- package/dist/src/services/AstParserService.d.ts +10 -6
- package/dist/src/services/AstParserService.js +60 -15
- package/dist/src/services/DatabaseService.d.ts +14 -1
- package/dist/src/services/DatabaseService.js +70 -25
- package/dist/src/services/EmbedderFactory.d.ts +9 -0
- package/dist/src/services/EmbedderFactory.js +29 -0
- package/dist/src/services/IndexerService.d.ts +22 -12
- package/dist/src/services/IndexerService.js +106 -46
- package/dist/src/services/InfinityEmbedder.d.ts +79 -0
- package/dist/src/services/InfinityEmbedder.js +199 -0
- package/dist/src/services/LoggerService.d.ts +2 -1
- package/dist/src/services/McpServer.d.ts +16 -7
- package/dist/src/services/McpServer.js +54 -367
- package/dist/src/services/MonorepoAwareDiscoveryService.d.ts +62 -0
- package/dist/src/services/MonorepoAwareDiscoveryService.js +125 -0
- package/dist/src/services/NxMonorepoResolver.d.ts +45 -0
- package/dist/src/services/NxMonorepoResolver.js +308 -0
- package/dist/src/services/OllamaEmbedder.d.ts +19 -5
- package/dist/src/services/OllamaEmbedder.js +91 -36
- package/dist/src/services/OpenAICompatibleEmbedder.d.ts +25 -0
- package/dist/src/services/OpenAICompatibleEmbedder.js +75 -0
- package/dist/src/services/ProjectDiscoveryService.d.ts +8 -0
- package/dist/src/{env-loader.js → services/ProjectDiscoveryService.js} +16 -5
- package/dist/src/services/SearcherService.d.ts +46 -7
- package/dist/src/services/SearcherService.js +183 -53
- package/dist/src/services/VoyageEmbedder.d.ts +22 -0
- package/dist/src/services/VoyageEmbedder.js +72 -0
- package/dist/src/services/WebServerService.d.ts +77 -0
- package/dist/src/services/WebServerService.js +267 -0
- package/dist/src/services/http-retry.d.ts +19 -0
- package/dist/src/services/http-retry.js +54 -0
- package/dist/src/services/reranker.d.ts +15 -0
- package/dist/src/services/reranker.js +44 -0
- package/dist/src/setup.d.ts +6 -0
- package/dist/src/setup.js +249 -31
- package/dist/src/tools/FindSymbolTool.d.ts +8 -0
- package/dist/src/tools/FindSymbolTool.js +45 -0
- package/dist/src/tools/GetCallersTool.d.ts +8 -0
- package/dist/src/tools/GetCallersTool.js +42 -0
- package/dist/src/tools/GetDependenciesTool.d.ts +8 -0
- package/dist/src/tools/GetDependenciesTool.js +38 -0
- package/dist/src/tools/GetDependentsTool.d.ts +8 -0
- package/dist/src/tools/GetDependentsTool.js +57 -0
- package/dist/src/tools/ListFilesTool.d.ts +8 -0
- package/dist/src/tools/ListFilesTool.js +39 -0
- package/dist/src/tools/ListProjectsTool.d.ts +8 -0
- package/dist/src/tools/ListProjectsTool.js +44 -0
- package/dist/src/tools/ReadFileRangeTool.d.ts +8 -0
- package/dist/src/tools/ReadFileRangeTool.js +44 -0
- package/dist/src/tools/ReadFileTool.d.ts +8 -0
- package/dist/src/tools/ReadFileTool.js +42 -0
- package/dist/src/tools/SearchCodebaseTool.d.ts +13 -0
- package/dist/src/tools/SearchCodebaseTool.js +81 -0
- package/dist/src/tools/format-helpers.d.ts +8 -0
- package/dist/src/tools/format-helpers.js +58 -0
- package/dist/src/tools/index.d.ts +9 -0
- package/dist/src/tools/index.js +21 -0
- package/dist/src/web/dashboard.d.ts +9 -0
- package/dist/src/web/dashboard.js +342 -0
- package/docker/docker-compose.infinity-gpu.yml +19 -0
- package/docker/docker-compose.yml +25 -0
- package/docs/web-dashboard.md +187 -0
- package/package.json +25 -3
- package/dist/src/ast.d.ts +0 -2
- package/dist/src/ast.js +0 -110
- package/dist/src/commands/SetupCommand.js +0 -14
- package/dist/src/db/data-source.d.ts +0 -2
- package/dist/src/db/data-source.js +0 -13
- package/dist/src/db/entities/code-chunk.entity.d.ts +0 -9
- package/dist/src/db/entities/code-chunk.entity.js +0 -54
- package/dist/src/db/entities/code-dependency.entity.d.ts +0 -9
- package/dist/src/db/entities/code-dependency.entity.js +0 -54
- package/dist/src/db/entities/code-symbol.entity.d.ts +0 -10
- package/dist/src/db/entities/code-symbol.entity.js +0 -59
- package/dist/src/db/init.d.ts +0 -2
- package/dist/src/db/init.js +0 -62
- package/dist/src/env-loader.d.ts +0 -1
- package/dist/src/indexer.d.ts +0 -17
- package/dist/src/indexer.js +0 -242
- package/dist/src/mcp.d.ts +0 -1
- package/dist/src/mcp.js +0 -403
- package/dist/src/ollama.d.ts +0 -2
- package/dist/src/ollama.js +0 -6
- package/dist/src/searcher.d.ts +0 -74
- package/dist/src/searcher.js +0 -446
- package/dist/vitest.config.d.ts +0 -2
- 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 {
|
|
5
|
-
import {
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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[]
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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, {
|
|
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(
|
|
324
|
-
const chunkStart =
|
|
325
|
-
const chunkEnd =
|
|
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: ${
|
|
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.
|
|
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
|
-
|
|
401
|
-
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
-
|
|
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 {
|
|
3
|
-
import {
|
|
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
|
-
|
|
14
|
+
private readonly version;
|
|
15
|
+
private readonly tools;
|
|
16
|
+
constructor(searcher: ISearcher, config: Config, logger: ILogger);
|
|
9
17
|
start(startupError?: Error): Promise<void>;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
}
|