codesearch-mcp 0.0.18-alpha → 0.2.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 +47 -1
- package/README.md +194 -23
- package/dist/app.js +38 -2
- package/dist/src/commands/IndexCommand.js +55 -1
- package/dist/src/commands/McpCommand.js +6 -2
- package/dist/src/config-error.d.ts +7 -0
- package/dist/src/config-error.js +14 -0
- package/dist/src/config-store.d.ts +73 -17
- package/dist/src/config-store.js +142 -27
- package/dist/src/config.d.ts +36 -11
- package/dist/src/config.js +89 -27
- package/dist/src/constants.d.ts +42 -2
- package/dist/src/constants.js +61 -7
- package/dist/src/container.js +5 -3
- 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/help.js +6 -5
- package/dist/src/interfaces/IAstParser.d.ts +14 -0
- package/dist/src/interfaces/IChunker.d.ts +24 -0
- package/dist/src/interfaces/IChunker.js +2 -0
- package/dist/src/interfaces/IDatabase.d.ts +31 -0
- package/dist/src/interfaces/IEmbedder.d.ts +14 -2
- package/dist/src/interfaces/ILogger.d.ts +7 -0
- package/dist/src/interfaces/ISearcher.d.ts +1 -0
- package/dist/src/interfaces/index.d.ts +2 -1
- package/dist/src/owner-path.d.ts +10 -0
- package/dist/src/owner-path.js +27 -0
- package/dist/src/services/AstChunker.d.ts +42 -0
- package/dist/src/services/AstChunker.js +158 -0
- package/dist/src/services/AstParserService.d.ts +6 -0
- package/dist/src/services/AstParserService.js +31 -2
- package/dist/src/services/DatabaseService.d.ts +17 -1
- package/dist/src/services/DatabaseService.js +116 -25
- package/dist/src/services/EmbedderFactory.d.ts +9 -0
- package/dist/src/services/EmbedderFactory.js +26 -0
- package/dist/src/services/IndexerService.d.ts +21 -5
- package/dist/src/services/IndexerService.js +127 -44
- package/dist/src/services/InfinityEmbedder.d.ts +38 -0
- package/dist/src/services/InfinityEmbedder.js +88 -0
- package/dist/src/services/LoggerService.d.ts +3 -1
- package/dist/src/services/LoggerService.js +17 -3
- package/dist/src/services/OpenAICompatibleEmbedder.d.ts +25 -0
- package/dist/src/services/OpenAICompatibleEmbedder.js +75 -0
- package/dist/src/services/SearcherService.d.ts +21 -1
- package/dist/src/services/SearcherService.js +129 -28
- package/dist/src/services/VoyageEmbedder.d.ts +22 -0
- package/dist/src/services/VoyageEmbedder.js +72 -0
- package/dist/src/services/WebServerService.d.ts +57 -14
- package/dist/src/services/WebServerService.js +134 -26
- package/dist/src/services/http-retry.d.ts +19 -0
- package/dist/src/services/http-retry.js +54 -0
- package/dist/src/services/infinity-provisioning.d.ts +115 -0
- package/dist/src/services/infinity-provisioning.js +516 -0
- package/dist/src/services/legacy-ollama-index.d.ts +24 -0
- package/dist/src/services/legacy-ollama-index.js +61 -0
- package/dist/src/services/reranker.d.ts +15 -0
- package/dist/src/services/reranker.js +44 -0
- package/dist/src/setup.d.ts +43 -0
- package/dist/src/setup.js +227 -55
- package/dist/src/tools/SearchCodebaseTool.js +1 -1
- package/dist/src/tools/format-helpers.js +2 -1
- package/dist/src/web/dashboard.js +5 -0
- package/docker/docker-compose.infinity-gpu.yml +19 -0
- package/docker/docker-compose.yml +20 -5
- package/docs/web-dashboard.md +36 -17
- package/package.json +18 -3
- package/dist/src/ast.d.ts +0 -2
- package/dist/src/ast.js +0 -110
- package/dist/src/commands/SetupCommand.d.ts +0 -7
- 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/env-loader.js +0 -41
- 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/src/services/OllamaEmbedder.d.ts +0 -14
- package/dist/src/services/OllamaEmbedder.js +0 -63
- package/dist/vitest.config.d.ts +0 -2
- package/dist/vitest.config.js +0 -9
|
@@ -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();
|
|
@@ -180,13 +236,12 @@ class IndexerService {
|
|
|
180
236
|
const files = preCollectedFiles ?? (0, file_utils_1.collectFiles)(dir, (0, file_utils_1.loadIgnore)(dir), dir);
|
|
181
237
|
this.logger.debug('IndexerService: scanAndIndex files collected', { serviceName, count: files.length, force });
|
|
182
238
|
const collectedRelPaths = new Set(files.map(f => path.relative(this.config.workspaceRoot, f)));
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
console.log(` Removed stale: ${path.basename(filePath)}`);
|
|
239
|
+
const knownFiles = await this.db.listKnownFiles(serviceName);
|
|
240
|
+
const staleFiles = knownFiles.filter(f => !collectedRelPaths.has(f));
|
|
241
|
+
if (staleFiles.length > 0) {
|
|
242
|
+
await this.db.deleteFiles(serviceName, staleFiles);
|
|
243
|
+
for (const f of staleFiles) {
|
|
244
|
+
console.log(` Removed stale: ${path.basename(f)}`);
|
|
190
245
|
}
|
|
191
246
|
}
|
|
192
247
|
const bar = new cli_progress_1.SingleBar({
|
|
@@ -290,8 +345,26 @@ class IndexerService {
|
|
|
290
345
|
});
|
|
291
346
|
return stats;
|
|
292
347
|
}
|
|
293
|
-
|
|
294
|
-
|
|
348
|
+
async pruneProjects(keep) {
|
|
349
|
+
if (keep.length === 0)
|
|
350
|
+
return { removed: [], failed: [] };
|
|
351
|
+
const indexed = await this.db.listIndexedProjects();
|
|
352
|
+
const keepSet = new Set(keep);
|
|
353
|
+
const toRemove = indexed.filter(name => !keepSet.has(name));
|
|
354
|
+
const removed = [];
|
|
355
|
+
const failed = [];
|
|
356
|
+
for (const name of toRemove) {
|
|
357
|
+
try {
|
|
358
|
+
await this.db.deleteProject(name);
|
|
359
|
+
removed.push(name);
|
|
360
|
+
}
|
|
361
|
+
catch (err) {
|
|
362
|
+
console.error(`Failed to prune project ${name}: ${err.message}`);
|
|
363
|
+
this.logger.error('IndexerService: pruneProject failed', { name, reason: err.message });
|
|
364
|
+
failed.push(name);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return { removed, failed };
|
|
295
368
|
}
|
|
296
369
|
static inferRole(relPath) {
|
|
297
370
|
const p = relPath.toLowerCase();
|
|
@@ -317,9 +390,9 @@ class IndexerService {
|
|
|
317
390
|
return 'Utility';
|
|
318
391
|
return null;
|
|
319
392
|
}
|
|
320
|
-
getEnrichedPrompt(
|
|
321
|
-
const chunkStart =
|
|
322
|
-
const chunkEnd =
|
|
393
|
+
getEnrichedPrompt(piece, relPath, serviceName, _meta, edges, symbols) {
|
|
394
|
+
const chunkStart = piece.startLine;
|
|
395
|
+
const chunkEnd = piece.endLine;
|
|
323
396
|
const lines = [];
|
|
324
397
|
lines.push(`Project: ${serviceName} | File: ${relPath}`);
|
|
325
398
|
const role = IndexerService.inferRole(relPath);
|
|
@@ -335,12 +408,9 @@ class IndexerService {
|
|
|
335
408
|
const depNames = [...new Set(edges.map(e => path.basename(e.toFile, path.extname(e.toFile)).split('.')[0]))];
|
|
336
409
|
lines.push(`Dependencies: ${depNames.join(', ')}`);
|
|
337
410
|
}
|
|
338
|
-
lines.push(`Code: ${
|
|
411
|
+
lines.push(`Code: ${piece.content}`);
|
|
339
412
|
return lines.join('\n');
|
|
340
413
|
}
|
|
341
|
-
async embedText(prompt) {
|
|
342
|
-
return this.embedder.embed(prompt);
|
|
343
|
-
}
|
|
344
414
|
static hashContent(content) {
|
|
345
415
|
return (0, crypto_1.createHash)('sha256').update(content).digest('hex');
|
|
346
416
|
}
|
|
@@ -364,7 +434,7 @@ class IndexerService {
|
|
|
364
434
|
return { status: 'skipped', chunks: 0, pendingMtimeRefresh: relFilePath };
|
|
365
435
|
}
|
|
366
436
|
const tAst = Date.now();
|
|
367
|
-
const { edges: rawEdges, symbols: rawSymbols, calls: rawCalls } = analysis ?? { edges: [], symbols: [], calls: [] };
|
|
437
|
+
const { edges: rawEdges, symbols: rawSymbols, calls: rawCalls, ownerRanges: rawOwnerRanges = [] } = analysis ?? { edges: [], symbols: [], calls: [] };
|
|
368
438
|
// astMs is near-zero — actual AST time is spent upfront in analyzeProject()
|
|
369
439
|
// Normalize edges to workspace-relative paths
|
|
370
440
|
const edges = rawEdges
|
|
@@ -390,25 +460,38 @@ class IndexerService {
|
|
|
390
460
|
// Normalize symbols: replace absolute filePath with workspace-relative, strip endLine (not stored)
|
|
391
461
|
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 }));
|
|
392
462
|
const astMs = Date.now() - tAst;
|
|
393
|
-
const chunks = await this.
|
|
463
|
+
const chunks = await this.chunker.chunk(code, {
|
|
464
|
+
strategy: this.config.indexer.chunkStrategy,
|
|
465
|
+
isSchemaFile: SCHEMA_EXTENSIONS.test(filePath),
|
|
466
|
+
symbols: rawSymbols,
|
|
467
|
+
});
|
|
394
468
|
const relPath = path.relative(projectRoot, filePath);
|
|
469
|
+
// Owner-path ranges: top-level symbols (container: null) + method ranges.
|
|
470
|
+
// Deepest range containing a chunk's start line wins → Class.method.
|
|
471
|
+
const ownerRanges = [
|
|
472
|
+
...rawSymbols
|
|
473
|
+
.filter(s => s.line != null)
|
|
474
|
+
.map(s => ({ name: s.name, container: null, line: s.line, endLine: s.endLine ?? s.line })),
|
|
475
|
+
...rawOwnerRanges,
|
|
476
|
+
];
|
|
395
477
|
const t2 = Date.now();
|
|
396
478
|
const records = [];
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
479
|
+
const embedBatchSize = this.config.embedding.batchSize;
|
|
480
|
+
for (let i = 0; i < chunks.length; i += embedBatchSize) {
|
|
481
|
+
const batch = chunks.slice(i, i + embedBatchSize);
|
|
482
|
+
const prompts = batch.map(piece => this.getEnrichedPrompt(piece, relPath, serviceName, meta ?? 'missing', edges, rawSymbols));
|
|
483
|
+
const vectors = await this.embedder.embedBatch(prompts, 'document');
|
|
484
|
+
for (let j = 0; j < batch.length; j++) {
|
|
485
|
+
const piece = batch[j];
|
|
486
|
+
records.push({
|
|
487
|
+
serviceName, filePath: relFilePath, startLine: piece.startLine,
|
|
488
|
+
content: piece.content,
|
|
489
|
+
embedding: `[${vectors[j]}]`,
|
|
407
490
|
lastModified: mtimeMs,
|
|
408
491
|
contentHash: hash,
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
492
|
+
ownerPath: (0, owner_path_1.resolveOwnerPath)(piece.startLine, ownerRanges),
|
|
493
|
+
});
|
|
494
|
+
}
|
|
412
495
|
}
|
|
413
496
|
const embedMs = Date.now() - t2;
|
|
414
497
|
const t3 = Date.now();
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Config } from '../config';
|
|
2
|
+
import type { ILogger } from '../interfaces/ILogger';
|
|
3
|
+
import { OpenAICompatibleEmbedder, type EmbedderDeps } from './OpenAICompatibleEmbedder';
|
|
4
|
+
import { serverRoot, probeInfinityHealth, listInfinityModels, type ProvisioningDeps } from './infinity-provisioning';
|
|
5
|
+
export { serverRoot, probeInfinityHealth, listInfinityModels };
|
|
6
|
+
export type { CommandResult } from './infinity-provisioning';
|
|
7
|
+
/** Extra injection seams for `InfinityEmbedder` beyond the shared `EmbedderDeps`. */
|
|
8
|
+
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
|
+
composePath?: string;
|
|
15
|
+
}
|
|
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
|
+
export declare class InfinityEmbedder extends OpenAICompatibleEmbedder {
|
|
25
|
+
private readonly base;
|
|
26
|
+
private readonly composePath;
|
|
27
|
+
private readonly provisioningDeps;
|
|
28
|
+
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
|
+
ensureModelAvailable(): Promise<void>;
|
|
37
|
+
listModels(): Promise<string[]>;
|
|
38
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
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 = exports.listInfinityModels = exports.probeInfinityHealth = exports.serverRoot = void 0;
|
|
37
|
+
const LoggerService_1 = require("./LoggerService");
|
|
38
|
+
const config_store_1 = require("../config-store");
|
|
39
|
+
const OpenAICompatibleEmbedder_1 = require("./OpenAICompatibleEmbedder");
|
|
40
|
+
const infinity_provisioning_1 = require("./infinity-provisioning");
|
|
41
|
+
Object.defineProperty(exports, "serverRoot", { enumerable: true, get: function () { return infinity_provisioning_1.serverRoot; } });
|
|
42
|
+
Object.defineProperty(exports, "probeInfinityHealth", { enumerable: true, get: function () { return infinity_provisioning_1.probeInfinityHealth; } });
|
|
43
|
+
Object.defineProperty(exports, "listInfinityModels", { enumerable: true, get: function () { return infinity_provisioning_1.listInfinityModels; } });
|
|
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
|
+
class InfinityEmbedder extends OpenAICompatibleEmbedder_1.OpenAICompatibleEmbedder {
|
|
54
|
+
base;
|
|
55
|
+
composePath;
|
|
56
|
+
provisioningDeps;
|
|
57
|
+
constructor(config, logger = LoggerService_1.LoggerService.silent(), deps = {}) {
|
|
58
|
+
super(config, logger, deps);
|
|
59
|
+
this.base = (0, infinity_provisioning_1.serverRoot)(config.embedding.baseURL);
|
|
60
|
+
this.composePath = deps.composePath ?? path.join(config_store_1.CONFIG_DIR, 'docker-compose.yml');
|
|
61
|
+
this.provisioningDeps = {
|
|
62
|
+
fetchImpl: deps.fetchImpl,
|
|
63
|
+
runCommand: deps.runCommand,
|
|
64
|
+
pollIntervalMs: deps.pollIntervalMs,
|
|
65
|
+
readinessTimeoutMs: deps.readinessTimeoutMs,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
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
|
+
async ensureModelAvailable() {
|
|
76
|
+
await (0, infinity_provisioning_1.ensureBackendReady)({
|
|
77
|
+
baseURL: this.config.embedding.baseURL,
|
|
78
|
+
requiredModels: [this.config.embedding.model],
|
|
79
|
+
modelSetToWrite: (0, infinity_provisioning_1.requiredModelSet)(this.config),
|
|
80
|
+
composePath: this.composePath,
|
|
81
|
+
deps: this.provisioningDeps,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
async listModels() {
|
|
85
|
+
return (0, infinity_provisioning_1.listInfinityModels)(this.base, this.provisioningDeps.fetchImpl ?? fetch, 2000);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
exports.InfinityEmbedder = InfinityEmbedder;
|
|
@@ -3,7 +3,8 @@ 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
|
-
|
|
6
|
+
private readonly metricsLogger;
|
|
7
|
+
constructor(logger: pino.Logger, metricsLogger?: pino.Logger | null);
|
|
7
8
|
static silent(): LoggerService;
|
|
8
9
|
static fromConfig(cfg: {
|
|
9
10
|
level: string;
|
|
@@ -14,4 +15,5 @@ export declare class LoggerService implements ILogger {
|
|
|
14
15
|
info(msg: string, data?: Record<string, unknown>): void;
|
|
15
16
|
warn(msg: string, data?: Record<string, unknown>): void;
|
|
16
17
|
error(msg: string, data?: Record<string, unknown>): void;
|
|
18
|
+
metric(msg: string, data?: Record<string, unknown>): void;
|
|
17
19
|
}
|
|
@@ -44,8 +44,13 @@ const fs = __importStar(require("fs"));
|
|
|
44
44
|
const DEFAULT_LOG_DIR = path.join(os.homedir(), '.config', 'codesearch-mcp', 'logs');
|
|
45
45
|
class LoggerService {
|
|
46
46
|
logger;
|
|
47
|
-
|
|
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) {
|
|
48
52
|
this.logger = logger;
|
|
53
|
+
this.metricsLogger = metricsLogger;
|
|
49
54
|
}
|
|
50
55
|
static silent() {
|
|
51
56
|
return new LoggerService((0, pino_1.default)({ level: 'silent' }));
|
|
@@ -66,6 +71,10 @@ class LoggerService {
|
|
|
66
71
|
static buildFromPath(level, filePath) {
|
|
67
72
|
if (filePath)
|
|
68
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';
|
|
69
78
|
const transport = filePath
|
|
70
79
|
? pino_1.default.transport({
|
|
71
80
|
targets: [{
|
|
@@ -79,18 +88,23 @@ class LoggerService {
|
|
|
79
88
|
limit: { count: 30 },
|
|
80
89
|
size: '50m',
|
|
81
90
|
},
|
|
82
|
-
level,
|
|
91
|
+
level: targetLevel,
|
|
83
92
|
}],
|
|
84
93
|
})
|
|
85
94
|
: undefined;
|
|
86
95
|
const logger = transport
|
|
87
96
|
? (0, pino_1.default)({ level }, transport)
|
|
88
97
|
: (0, pino_1.default)({ level: 'silent' });
|
|
89
|
-
|
|
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);
|
|
90
103
|
}
|
|
91
104
|
debug(msg, data) { this.logger.debug(data ?? {}, msg); }
|
|
92
105
|
info(msg, data) { this.logger.info(data ?? {}, msg); }
|
|
93
106
|
warn(msg, data) { this.logger.warn(data ?? {}, msg); }
|
|
94
107
|
error(msg, data) { this.logger.error(data ?? {}, msg); }
|
|
108
|
+
metric(msg, data) { this.metricsLogger?.info(data ?? {}, msg); }
|
|
95
109
|
}
|
|
96
110
|
exports.LoggerService = LoggerService;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Config } from '../config';
|
|
2
|
+
import type { IEmbedder, InputType } from '../interfaces/IEmbedder';
|
|
3
|
+
import type { ILogger } from '../interfaces/ILogger';
|
|
4
|
+
export interface EmbedderDeps {
|
|
5
|
+
fetchImpl?: typeof fetch;
|
|
6
|
+
baseDelayMs?: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Embedder for any server exposing an OpenAI-style `/embeddings` endpoint:
|
|
10
|
+
* OpenAI cloud, Infinity, TEI, vLLM, LM Studio, LocalAI. One request carries all
|
|
11
|
+
* inputs (`{ input: string[], model, dimensions? }`) and returns
|
|
12
|
+
* `{ data: [{ embedding, index }] }`, re-ordered to input order.
|
|
13
|
+
*/
|
|
14
|
+
export declare class OpenAICompatibleEmbedder implements IEmbedder {
|
|
15
|
+
protected config: Config;
|
|
16
|
+
private logger;
|
|
17
|
+
readonly dimensions: number;
|
|
18
|
+
rerank?: (query: string, documents: string[], topN: number) => Promise<number[]>;
|
|
19
|
+
private readonly url;
|
|
20
|
+
private readonly doFetch;
|
|
21
|
+
private readonly baseDelayMs?;
|
|
22
|
+
constructor(config: Config, logger?: ILogger, deps?: EmbedderDeps);
|
|
23
|
+
embed(text: string, inputType?: InputType): Promise<number[]>;
|
|
24
|
+
embedBatch(texts: string[], _inputType?: InputType): Promise<number[][]>;
|
|
25
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OpenAICompatibleEmbedder = void 0;
|
|
4
|
+
const LoggerService_1 = require("./LoggerService");
|
|
5
|
+
const config_1 = require("../config");
|
|
6
|
+
const reranker_1 = require("./reranker");
|
|
7
|
+
const http_retry_1 = require("./http-retry");
|
|
8
|
+
const constants_1 = require("../constants");
|
|
9
|
+
/**
|
|
10
|
+
* Embedder for any server exposing an OpenAI-style `/embeddings` endpoint:
|
|
11
|
+
* OpenAI cloud, Infinity, TEI, vLLM, LM Studio, LocalAI. One request carries all
|
|
12
|
+
* inputs (`{ input: string[], model, dimensions? }`) and returns
|
|
13
|
+
* `{ data: [{ embedding, index }] }`, re-ordered to input order.
|
|
14
|
+
*/
|
|
15
|
+
class OpenAICompatibleEmbedder {
|
|
16
|
+
config;
|
|
17
|
+
logger;
|
|
18
|
+
dimensions;
|
|
19
|
+
rerank;
|
|
20
|
+
url;
|
|
21
|
+
doFetch;
|
|
22
|
+
baseDelayMs;
|
|
23
|
+
constructor(config, logger = LoggerService_1.LoggerService.silent(), deps = {}) {
|
|
24
|
+
this.config = config;
|
|
25
|
+
this.logger = logger;
|
|
26
|
+
const base = config.embedding.baseURL.replace(/\/+$/, '');
|
|
27
|
+
// Allow a baseURL that already includes the path (…/v1) or not.
|
|
28
|
+
this.url = /\/embeddings$/.test(base) ? base : `${base}/embeddings`;
|
|
29
|
+
this.doFetch = deps.fetchImpl ?? fetch;
|
|
30
|
+
this.baseDelayMs = deps.baseDelayMs;
|
|
31
|
+
this.dimensions = config.embedding.dimensions ?? (0, constants_1.registryDimension)(config.embedding.model) ?? 0;
|
|
32
|
+
this.rerank = (0, reranker_1.makeReranker)(config, logger, deps);
|
|
33
|
+
}
|
|
34
|
+
async embed(text, inputType) {
|
|
35
|
+
const [v] = await this.embedBatch([text], inputType);
|
|
36
|
+
return v;
|
|
37
|
+
}
|
|
38
|
+
async embedBatch(texts, _inputType) {
|
|
39
|
+
if (texts.length === 0)
|
|
40
|
+
return [];
|
|
41
|
+
return (0, http_retry_1.withRetry)(async () => {
|
|
42
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
43
|
+
// Resolve the key lazily — an unset `env:` ref throws here (request time),
|
|
44
|
+
// not at construction, so `rag-web`/`rag-mcp` still start.
|
|
45
|
+
const apiKey = (0, config_1.resolveEnvRef)(this.config.embedding.apiKey);
|
|
46
|
+
if (apiKey)
|
|
47
|
+
headers['Authorization'] = `Bearer ${apiKey}`;
|
|
48
|
+
const body = { model: this.config.embedding.model, input: texts };
|
|
49
|
+
if (this.config.embedding.dimensions != null)
|
|
50
|
+
body.dimensions = this.config.embedding.dimensions;
|
|
51
|
+
const res = await this.doFetch(this.url, {
|
|
52
|
+
method: 'POST',
|
|
53
|
+
headers,
|
|
54
|
+
signal: AbortSignal.timeout(120_000),
|
|
55
|
+
body: JSON.stringify(body),
|
|
56
|
+
});
|
|
57
|
+
if (!res.ok) {
|
|
58
|
+
const errBody = (await res.text().catch(() => res.statusText)).slice(0, 200);
|
|
59
|
+
throw new http_retry_1.HttpError(res.status, `embeddings request failed: ${res.status} ${errBody}`);
|
|
60
|
+
}
|
|
61
|
+
const data = await res.json();
|
|
62
|
+
// Guard against a partial/mismatched response — a missing vector would
|
|
63
|
+
// otherwise be inserted as "[undefined]" and fail the whole file, or (with
|
|
64
|
+
// reordering + no index) silently misalign vectors to the wrong chunks.
|
|
65
|
+
if (!Array.isArray(data.data) || data.data.length !== texts.length) {
|
|
66
|
+
throw new Error(`embeddings response returned ${data.data?.length ?? 0} vectors for ${texts.length} inputs`);
|
|
67
|
+
}
|
|
68
|
+
// Sort by `index` so vectors line up with input order regardless of server ordering.
|
|
69
|
+
return [...data.data]
|
|
70
|
+
.sort((a, b) => (a.index ?? 0) - (b.index ?? 0))
|
|
71
|
+
.map(d => d.embedding);
|
|
72
|
+
}, { logger: this.logger, baseDelayMs: this.baseDelayMs, label: 'openai-embed' });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
exports.OpenAICompatibleEmbedder = OpenAICompatibleEmbedder;
|
|
@@ -9,6 +9,7 @@ export interface ChunkRow {
|
|
|
9
9
|
filePath: string;
|
|
10
10
|
startLine: number;
|
|
11
11
|
content: string;
|
|
12
|
+
ownerPath?: string | null;
|
|
12
13
|
}
|
|
13
14
|
export interface SearchResult extends ChunkRow {
|
|
14
15
|
score: number;
|
|
@@ -102,8 +103,28 @@ export declare class SearcherService {
|
|
|
102
103
|
query: string;
|
|
103
104
|
translated: boolean;
|
|
104
105
|
}>;
|
|
106
|
+
/**
|
|
107
|
+
* Refuse when the configured embedding model disagrees with the one that built
|
|
108
|
+
* the index — embedding a query into a foreign vector space returns noise. A
|
|
109
|
+
* missing `index_meta` row with an empty (or altogether absent) `code_chunk`
|
|
110
|
+
* does not block — but a *populated* `code_chunk` with no `index_meta` row is a
|
|
111
|
+
* pre-`index_meta` install, treated as the legacy `ollama`/`bge-m3` index (same
|
|
112
|
+
* interpretation as the `rag-index` legacy-Ollama guard), read-only — this check
|
|
113
|
+
* never writes `index_meta`.
|
|
114
|
+
*/
|
|
115
|
+
assertIndexMatches(): Promise<void>;
|
|
116
|
+
private compareIndexed;
|
|
105
117
|
listProjects(): Promise<ProjectRow[]>;
|
|
106
118
|
searchChunks(query: string, limit: number, project: string | null, threshold?: number): Promise<SearchResult[]>;
|
|
119
|
+
/**
|
|
120
|
+
* Emit one structured search-tuning line (via `logger.metric`, at `info`
|
|
121
|
+
* regardless of `logging.level`). `candidates` are in cosine order, so a
|
|
122
|
+
* candidate's array index is its cosine rank — `maxCosineRankSurvived` is the
|
|
123
|
+
* deepest cosine position that survived reranking into the returned set. Read
|
|
124
|
+
* it alongside `poolSaturated` to tune `candidateFloor`/`overfetch`/threshold
|
|
125
|
+
* per hardware. Never logs chunk content, embeddings, or credentials.
|
|
126
|
+
*/
|
|
127
|
+
private emitSearchMetrics;
|
|
107
128
|
fetchNeighbors(results: SearchResult[]): Promise<NeighborResult[]>;
|
|
108
129
|
fetchGraphContext(results: SearchResult[]): Promise<Map<string, GraphContext>>;
|
|
109
130
|
getDependencies(filePath: string, project: string, symbol?: string | null, transitive?: boolean): Promise<DependencyRow[]>;
|
|
@@ -125,7 +146,6 @@ export declare class SearcherService {
|
|
|
125
146
|
};
|
|
126
147
|
readFileAbsolute(absPath: string, maxLines: number): Promise<FileRangeResult>;
|
|
127
148
|
readFileRange(filePath: string, startLine: number, endLine: number): Promise<FileRangeResult>;
|
|
128
|
-
private isNonEnglish;
|
|
129
149
|
private walkDir;
|
|
130
150
|
private buildFindSymbolPattern;
|
|
131
151
|
private inferKind;
|