@softerist/heuristic-mcp 2.1.47 → 3.0.0
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/.agent/workflows/code-review.md +60 -0
- package/.prettierrc +7 -0
- package/ARCHITECTURE.md +105 -170
- package/CONTRIBUTING.md +32 -113
- package/GEMINI.md +73 -0
- package/LICENSE +21 -21
- package/README.md +161 -54
- package/config.json +876 -75
- package/debug-pids.js +27 -0
- package/eslint.config.js +36 -0
- package/features/ann-config.js +37 -26
- package/features/clear-cache.js +28 -19
- package/features/find-similar-code.js +142 -66
- package/features/hybrid-search.js +253 -93
- package/features/index-codebase.js +1455 -394
- package/features/lifecycle.js +813 -180
- package/features/register.js +58 -52
- package/index.js +450 -306
- package/lib/cache-ops.js +22 -0
- package/lib/cache-utils.js +68 -0
- package/lib/cache.js +1392 -587
- package/lib/call-graph.js +165 -50
- package/lib/cli.js +154 -0
- package/lib/config.js +462 -121
- package/lib/embedding-process.js +77 -0
- package/lib/embedding-worker.js +545 -30
- package/lib/ignore-patterns.js +61 -59
- package/lib/json-worker.js +14 -0
- package/lib/json-writer.js +344 -0
- package/lib/logging.js +88 -0
- package/lib/memory-logger.js +13 -0
- package/lib/project-detector.js +13 -17
- package/lib/server-lifecycle.js +38 -0
- package/lib/settings-editor.js +645 -0
- package/lib/tokenizer.js +207 -104
- package/lib/utils.js +273 -198
- package/lib/vector-store-binary.js +592 -0
- package/mcp_config.example.json +13 -0
- package/package.json +13 -2
- package/scripts/clear-cache.js +6 -17
- package/scripts/download-model.js +14 -9
- package/scripts/postinstall.js +5 -5
- package/search-configs.js +36 -0
- package/test/ann-config.test.js +179 -0
- package/test/ann-fallback.test.js +6 -6
- package/test/binary-store.test.js +69 -0
- package/test/cache-branches.test.js +120 -0
- package/test/cache-errors.test.js +264 -0
- package/test/cache-extra.test.js +300 -0
- package/test/cache-helpers.test.js +205 -0
- package/test/cache-hnsw-failure.test.js +40 -0
- package/test/cache-json-worker.test.js +190 -0
- package/test/cache-worker.test.js +102 -0
- package/test/cache.test.js +443 -0
- package/test/call-graph.test.js +103 -4
- package/test/clear-cache.test.js +69 -68
- package/test/code-review-workflow.test.js +50 -0
- package/test/config.test.js +418 -0
- package/test/coverage-gap.test.js +497 -0
- package/test/coverage-maximizer.test.js +236 -0
- package/test/debug-analysis.js +107 -0
- package/test/embedding-model.test.js +173 -103
- package/test/embedding-worker-extra.test.js +272 -0
- package/test/embedding-worker.test.js +158 -0
- package/test/features.test.js +139 -0
- package/test/final-boost.test.js +271 -0
- package/test/final-polish.test.js +183 -0
- package/test/final.test.js +95 -0
- package/test/find-similar-code.test.js +191 -0
- package/test/helpers.js +92 -11
- package/test/helpers.test.js +46 -0
- package/test/hybrid-search-basic.test.js +62 -0
- package/test/hybrid-search-branch.test.js +202 -0
- package/test/hybrid-search-callgraph.test.js +229 -0
- package/test/hybrid-search-extra.test.js +81 -0
- package/test/hybrid-search.test.js +484 -71
- package/test/index-cli.test.js +520 -0
- package/test/index-codebase-batch.test.js +119 -0
- package/test/index-codebase-branches.test.js +585 -0
- package/test/index-codebase-core.test.js +1032 -0
- package/test/index-codebase-edge-cases.test.js +254 -0
- package/test/index-codebase-errors.test.js +132 -0
- package/test/index-codebase-gap.test.js +239 -0
- package/test/index-codebase-lines.test.js +151 -0
- package/test/index-codebase-watcher.test.js +259 -0
- package/test/index-codebase-zone.test.js +259 -0
- package/test/index-codebase.test.js +371 -69
- package/test/index-memory.test.js +220 -0
- package/test/indexer-detailed.test.js +176 -0
- package/test/integration.test.js +148 -92
- package/test/json-worker.test.js +50 -0
- package/test/lifecycle.test.js +541 -0
- package/test/master.test.js +198 -0
- package/test/perfection.test.js +349 -0
- package/test/project-detector.test.js +65 -0
- package/test/register.test.js +262 -0
- package/test/tokenizer.test.js +55 -93
- package/test/ultra-maximizer.test.js +116 -0
- package/test/utils-branches.test.js +161 -0
- package/test/utils-extra.test.js +116 -0
- package/test/utils.test.js +131 -0
- package/test/verify_fixes.js +76 -0
- package/test/worker-errors.test.js +96 -0
- package/test/worker-init.test.js +102 -0
- package/test/worker_throttling.test.js +93 -0
- package/tools/scripts/benchmark-search.js +95 -0
- package/tools/scripts/cache-stats.js +71 -0
- package/tools/scripts/manual-search.js +34 -0
- package/vitest.config.js +19 -9
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import path from
|
|
2
|
-
import fs from
|
|
3
|
-
import { dotSimilarity } from
|
|
4
|
-
import { extractSymbolsFromContent } from
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import { dotSimilarity } from '../lib/utils.js';
|
|
4
|
+
import { extractSymbolsFromContent } from '../lib/call-graph.js';
|
|
5
5
|
|
|
6
6
|
export class HybridSearch {
|
|
7
7
|
constructor(embedder, cache, config) {
|
|
@@ -11,6 +11,14 @@ export class HybridSearch {
|
|
|
11
11
|
this.fileModTimes = new Map(); // Cache for file modification times
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
async getChunkContent(chunkOrIndex) {
|
|
15
|
+
return await this.cache.getChunkContent(chunkOrIndex);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
getChunkVector(chunk) {
|
|
19
|
+
return this.cache.getChunkVector(chunk);
|
|
20
|
+
}
|
|
21
|
+
|
|
14
22
|
getAnnCandidateCount(maxResults, totalChunks) {
|
|
15
23
|
const minCandidates = this.config.annMinCandidates ?? 0;
|
|
16
24
|
const maxCandidates = this.config.annMaxCandidates ?? totalChunks;
|
|
@@ -26,7 +34,13 @@ export class HybridSearch {
|
|
|
26
34
|
|
|
27
35
|
for (const file of uniqueFiles) {
|
|
28
36
|
if (!this.fileModTimes.has(file)) {
|
|
29
|
-
|
|
37
|
+
// Try to get from cache metadata first (fast)
|
|
38
|
+
const meta = this.cache.getFileMeta(file);
|
|
39
|
+
if (meta && typeof meta.mtimeMs === 'number') {
|
|
40
|
+
this.fileModTimes.set(file, meta.mtimeMs);
|
|
41
|
+
} else {
|
|
42
|
+
missing.push(file);
|
|
43
|
+
}
|
|
30
44
|
}
|
|
31
45
|
}
|
|
32
46
|
|
|
@@ -34,17 +48,31 @@ export class HybridSearch {
|
|
|
34
48
|
return;
|
|
35
49
|
}
|
|
36
50
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
51
|
+
// Concurrency-limited execution to avoid EMFILE
|
|
52
|
+
const CONCURRENCY = 50;
|
|
53
|
+
let index = 0;
|
|
54
|
+
|
|
55
|
+
const worker = async () => {
|
|
56
|
+
while (index < missing.length) {
|
|
57
|
+
const file = missing[index++];
|
|
58
|
+
if (!file) break; // Safety check
|
|
41
59
|
try {
|
|
42
60
|
const stats = await fs.stat(file);
|
|
43
61
|
this.fileModTimes.set(file, stats.mtimeMs);
|
|
44
62
|
} catch {
|
|
45
63
|
this.fileModTimes.set(file, null);
|
|
46
64
|
}
|
|
47
|
-
}
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
await Promise.all(Array.from({ length: Math.min(CONCURRENCY, missing.length) }, worker));
|
|
69
|
+
|
|
70
|
+
// Prevent unbounded growth (simple eviction)
|
|
71
|
+
if (this.fileModTimes.size > 5000) {
|
|
72
|
+
for (const [key] of this.fileModTimes) {
|
|
73
|
+
this.fileModTimes.delete(key);
|
|
74
|
+
if (this.fileModTimes.size <= 4000) break;
|
|
75
|
+
}
|
|
48
76
|
}
|
|
49
77
|
}
|
|
50
78
|
|
|
@@ -54,82 +82,190 @@ export class HybridSearch {
|
|
|
54
82
|
}
|
|
55
83
|
|
|
56
84
|
async search(query, maxResults) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
85
|
+
try {
|
|
86
|
+
if (typeof this.cache.ensureLoaded === 'function') {
|
|
87
|
+
await this.cache.ensureLoaded();
|
|
88
|
+
}
|
|
89
|
+
this.cache.startRead();
|
|
90
|
+
|
|
91
|
+
const storeSize = this.cache.getStoreSize();
|
|
92
|
+
|
|
93
|
+
if (storeSize === 0) {
|
|
94
|
+
return {
|
|
95
|
+
results: [],
|
|
96
|
+
message: 'No code has been indexed yet. Please wait for initial indexing to complete.',
|
|
97
|
+
};
|
|
98
|
+
}
|
|
65
99
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
100
|
+
// Generate query embedding
|
|
101
|
+
console.info(`[Search] Query: "${query}"`);
|
|
102
|
+
const queryEmbed = await this.embedder(query, {
|
|
103
|
+
pooling: 'mean',
|
|
104
|
+
normalize: true,
|
|
105
|
+
});
|
|
106
|
+
const queryVector = queryEmbed.data; // Keep as Float32Array for performance
|
|
107
|
+
const queryVectorTyped = queryVector;
|
|
70
108
|
|
|
71
|
-
|
|
109
|
+
let candidateIndices = null; // null implies full scan of all chunks
|
|
72
110
|
let usedAnn = false;
|
|
111
|
+
|
|
73
112
|
if (this.config.annEnabled) {
|
|
74
|
-
const candidateCount = this.getAnnCandidateCount(maxResults,
|
|
113
|
+
const candidateCount = this.getAnnCandidateCount(maxResults, storeSize);
|
|
75
114
|
const annLabels = await this.cache.queryAnn(queryVectorTyped, candidateCount);
|
|
76
115
|
if (annLabels && annLabels.length >= maxResults) {
|
|
77
116
|
usedAnn = true;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
.map((index) => {
|
|
81
|
-
if (seen.has(index)) return null;
|
|
82
|
-
seen.add(index);
|
|
83
|
-
return vectorStore[index];
|
|
84
|
-
})
|
|
85
|
-
.filter(Boolean);
|
|
117
|
+
console.info(`[Search] Using ANN index (${annLabels.length} candidates)`);
|
|
118
|
+
candidateIndices = Array.from(new Set(annLabels)); // dedupe
|
|
86
119
|
}
|
|
87
120
|
}
|
|
88
121
|
|
|
89
|
-
if (usedAnn
|
|
90
|
-
|
|
91
|
-
usedAnn = false;
|
|
122
|
+
if (!usedAnn) {
|
|
123
|
+
console.info(`[Search] Using full scan (${storeSize} chunks)`);
|
|
92
124
|
}
|
|
93
125
|
|
|
94
|
-
if (
|
|
95
|
-
|
|
126
|
+
if (usedAnn && candidateIndices && candidateIndices.length < maxResults) {
|
|
127
|
+
console.info(`[Search] ANN returned fewer results (${candidateIndices.length}) than requested (${maxResults}), augmenting with full scan...`);
|
|
128
|
+
candidateIndices = null; // Fallback to full scan to ensure we don't miss anything relevant
|
|
129
|
+
usedAnn = false;
|
|
96
130
|
}
|
|
97
131
|
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
132
|
+
const lowerQuery = query.toLowerCase();
|
|
133
|
+
const queryWords =
|
|
134
|
+
lowerQuery.length > 1 ? lowerQuery.split(/\s+/).filter((word) => word.length > 2) : [];
|
|
135
|
+
const queryWordCount = queryWords.length;
|
|
136
|
+
|
|
137
|
+
if (usedAnn && candidateIndices && lowerQuery.length > 1) {
|
|
138
|
+
let exactMatchCount = 0;
|
|
139
|
+
for (const index of candidateIndices) {
|
|
140
|
+
const content = await this.getChunkContent(index);
|
|
141
|
+
if (content && content.toLowerCase().includes(lowerQuery)) {
|
|
142
|
+
exactMatchCount++;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
102
145
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
146
|
+
if (exactMatchCount < maxResults) {
|
|
147
|
+
// Fallback to full scan if keyword constraint isn't met in candidates
|
|
148
|
+
// Note: This is expensive as it iterates everything.
|
|
149
|
+
// Optimization: Only do this for small-ish codebases to avoid UI freeze
|
|
150
|
+
const MAX_FULL_SCAN_SIZE = 2000;
|
|
151
|
+
|
|
152
|
+
if (storeSize <= MAX_FULL_SCAN_SIZE) {
|
|
153
|
+
const seen = new Set(candidateIndices);
|
|
154
|
+
|
|
155
|
+
// Full scan logic for keyword augmentation
|
|
156
|
+
// Iterate by index with yielding
|
|
157
|
+
const FALLBACK_BATCH = 100;
|
|
158
|
+
for (let i = 0; i < storeSize; i += FALLBACK_BATCH) {
|
|
159
|
+
if (i > 0) await new Promise(r => setTimeout(r, 0)); // Yield
|
|
160
|
+
|
|
161
|
+
const limit = Math.min(storeSize, i + FALLBACK_BATCH);
|
|
162
|
+
for (let j = i; j < limit; j++) {
|
|
163
|
+
if (seen.has(j)) continue;
|
|
164
|
+
|
|
165
|
+
// Lazy load content only if needed (this might be slow for huge repo)
|
|
166
|
+
// But `getChunkContent` should use cache.
|
|
167
|
+
const content = await this.getChunkContent(j);
|
|
168
|
+
if (content && content.toLowerCase().includes(lowerQuery)) {
|
|
169
|
+
seen.add(j);
|
|
170
|
+
candidateIndices.push(j);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
console.info(`[Search] Skipping full scan fallback (store size ${storeSize} > ${MAX_FULL_SCAN_SIZE})`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
106
179
|
|
|
107
|
-
|
|
108
|
-
|
|
180
|
+
// Recency pre-processing
|
|
181
|
+
let recencyBoostEnabled = this.config.recencyBoost > 0;
|
|
182
|
+
let now = Date.now();
|
|
183
|
+
let recencyDecayMs = (this.config.recencyDecayDays || 30) * 24 * 60 * 60 * 1000;
|
|
184
|
+
let semanticWeight = this.config.semanticWeight;
|
|
185
|
+
let exactMatchBoost = this.config.exactMatchBoost;
|
|
186
|
+
let recencyBoost = this.config.recencyBoost;
|
|
187
|
+
|
|
188
|
+
if (recencyBoostEnabled) {
|
|
189
|
+
const candidates = candidateIndices
|
|
190
|
+
? candidateIndices.map((idx) => this.cache.getChunk(idx)).filter(Boolean)
|
|
191
|
+
: Array.from({ length: storeSize }, (_, i) => this.cache.getChunk(i)).filter(Boolean);
|
|
192
|
+
// optimization: avoid IO storm during full scan fallbacks
|
|
193
|
+
// For large candidate sets, we strictly rely on cached metadata
|
|
194
|
+
// For small sets, we allow best-effort fs.stat
|
|
195
|
+
if (candidates.length <= 1000) {
|
|
196
|
+
await this.populateFileModTimes(candidates.map((chunk) => chunk.file));
|
|
109
197
|
} else {
|
|
110
|
-
//
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
198
|
+
// Bulk pre-populate from cache only (no syscalls)
|
|
199
|
+
for (const chunk of candidates) {
|
|
200
|
+
if (!this.fileModTimes.has(chunk.file)) {
|
|
201
|
+
const meta = this.cache.getFileMeta(chunk.file);
|
|
202
|
+
if (meta && typeof meta.mtimeMs === 'number') {
|
|
203
|
+
this.fileModTimes.set(chunk.file, meta.mtimeMs);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
116
207
|
}
|
|
208
|
+
}
|
|
117
209
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (typeof mtime === "number") {
|
|
122
|
-
const daysSinceModified = (Date.now() - mtime) / (1000 * 60 * 60 * 24);
|
|
123
|
-
const decayDays = this.config.recencyDecayDays || 30;
|
|
210
|
+
// Score all chunks (batched to prevent blocking event loop)
|
|
211
|
+
const BATCH_SIZE = 500;
|
|
212
|
+
const scoredChunks = [];
|
|
124
213
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
214
|
+
// Process in batches
|
|
215
|
+
// Candidates is now implicitly range 0..storeSize OR candidateIndices
|
|
216
|
+
const totalCandidates = candidateIndices ? candidateIndices.length : storeSize;
|
|
217
|
+
|
|
218
|
+
for (let i = 0; i < totalCandidates; i += BATCH_SIZE) {
|
|
219
|
+
// Allow event loop to tick between batches
|
|
220
|
+
if (i > 0) {
|
|
221
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
129
222
|
}
|
|
130
223
|
|
|
131
|
-
|
|
132
|
-
|
|
224
|
+
const limit = Math.min(totalCandidates, i + BATCH_SIZE);
|
|
225
|
+
|
|
226
|
+
for (let j = i; j < limit; j++) {
|
|
227
|
+
const idx = candidateIndices ? candidateIndices[j] : j;
|
|
228
|
+
|
|
229
|
+
// Lazy load keys
|
|
230
|
+
const vector = this.cache.getVector(idx);
|
|
231
|
+
if (!vector) continue;
|
|
232
|
+
|
|
233
|
+
// Ensure vector compatibility (dotSimilarity now checks length too)
|
|
234
|
+
let score = dotSimilarity(queryVector, vector) * semanticWeight;
|
|
235
|
+
|
|
236
|
+
// Exact match boost
|
|
237
|
+
const content = await this.getChunkContent(idx);
|
|
238
|
+
const lowerContent = content ? content.toLowerCase() : '';
|
|
239
|
+
|
|
240
|
+
if (lowerContent && lowerContent.includes(lowerQuery)) {
|
|
241
|
+
score += exactMatchBoost;
|
|
242
|
+
} else if (lowerContent && queryWordCount > 0) {
|
|
243
|
+
// Partial word matching (optimized)
|
|
244
|
+
let matchedWords = 0;
|
|
245
|
+
for (let k = 0; k < queryWordCount; k++) {
|
|
246
|
+
if (lowerContent.includes(queryWords[k])) matchedWords++;
|
|
247
|
+
}
|
|
248
|
+
score += (matchedWords / queryWordCount) * 0.3;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Needs chunk info for result
|
|
252
|
+
const chunkInfo = this.cache.getChunk(idx);
|
|
253
|
+
|
|
254
|
+
// Recency boost
|
|
255
|
+
if (recencyBoostEnabled && chunkInfo) {
|
|
256
|
+
const mtime = this.fileModTimes.get(chunkInfo.file);
|
|
257
|
+
if (typeof mtime === 'number') {
|
|
258
|
+
const ageMs = now - mtime;
|
|
259
|
+
const recencyFactor = Math.max(0, 1 - ageMs / recencyDecayMs);
|
|
260
|
+
score += recencyFactor * recencyBoost;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (chunkInfo) {
|
|
265
|
+
scoredChunks.push({ ...chunkInfo, score, content });
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
133
269
|
|
|
134
270
|
// Sort by initial score
|
|
135
271
|
scoredChunks.sort((a, b) => b.score - a.score);
|
|
@@ -140,7 +276,8 @@ export class HybridSearch {
|
|
|
140
276
|
const topN = Math.min(5, scoredChunks.length);
|
|
141
277
|
const symbolsFromTop = new Set();
|
|
142
278
|
for (let i = 0; i < topN; i++) {
|
|
143
|
-
const
|
|
279
|
+
const content = await this.getChunkContent(scoredChunks[i]);
|
|
280
|
+
const symbols = extractSymbolsFromContent(content || '');
|
|
144
281
|
for (const sym of symbols) {
|
|
145
282
|
symbolsFromTop.add(sym);
|
|
146
283
|
}
|
|
@@ -157,62 +294,85 @@ export class HybridSearch {
|
|
|
157
294
|
chunk.score += proximity * this.config.callGraphBoost;
|
|
158
295
|
}
|
|
159
296
|
}
|
|
160
|
-
|
|
161
297
|
// Re-sort after applying call graph boost
|
|
162
298
|
scoredChunks.sort((a, b) => b.score - a.score);
|
|
163
299
|
}
|
|
164
300
|
}
|
|
165
301
|
|
|
166
302
|
// Get top results
|
|
167
|
-
const results = scoredChunks.slice(0, maxResults)
|
|
303
|
+
const results = await Promise.all(scoredChunks.slice(0, maxResults).map(async (chunk) => {
|
|
304
|
+
if (chunk.content === undefined || chunk.content === null) {
|
|
305
|
+
return { ...chunk, content: await this.getChunkContent(chunk) };
|
|
306
|
+
}
|
|
307
|
+
return chunk;
|
|
308
|
+
}));
|
|
309
|
+
|
|
310
|
+
if (results.length > 0) {
|
|
311
|
+
console.info(`[Search] Found ${results.length} results. Top score: ${results[0].score.toFixed(4)}`);
|
|
312
|
+
} else {
|
|
313
|
+
console.info('[Search] No results found.');
|
|
314
|
+
}
|
|
168
315
|
|
|
169
316
|
return { results, message: null };
|
|
317
|
+
} finally {
|
|
318
|
+
this.cache.endRead();
|
|
319
|
+
}
|
|
170
320
|
}
|
|
171
321
|
|
|
172
|
-
formatResults(results) {
|
|
322
|
+
async formatResults(results) {
|
|
173
323
|
if (results.length === 0) {
|
|
174
|
-
return
|
|
324
|
+
return 'No matching code found for your query.';
|
|
175
325
|
}
|
|
176
326
|
|
|
177
|
-
|
|
327
|
+
const formatted = await Promise.all(results.map(async (r, idx) => {
|
|
178
328
|
const relPath = path.relative(this.config.searchDirectory, r.file);
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
329
|
+
const content = r.content ?? await this.getChunkContent(r);
|
|
330
|
+
return (
|
|
331
|
+
`## Result ${idx + 1} (Relevance: ${(r.score * 100).toFixed(1)}%)\n` +
|
|
332
|
+
`**File:** \`${relPath}\`\n` +
|
|
333
|
+
`**Lines:** ${r.startLine}-${r.endLine}\n\n` +
|
|
334
|
+
'```' +
|
|
335
|
+
path.extname(r.file).slice(1) +
|
|
336
|
+
'\n' +
|
|
337
|
+
content +
|
|
338
|
+
'\n' +
|
|
339
|
+
'```\n'
|
|
340
|
+
);
|
|
341
|
+
}));
|
|
342
|
+
|
|
343
|
+
return formatted.join('\n');
|
|
186
344
|
}
|
|
187
345
|
}
|
|
188
346
|
|
|
189
347
|
// MCP Tool definition for this feature
|
|
190
348
|
export function getToolDefinition(config) {
|
|
191
349
|
return {
|
|
192
|
-
name:
|
|
193
|
-
description:
|
|
350
|
+
name: 'a_semantic_search',
|
|
351
|
+
description:
|
|
352
|
+
"Performs intelligent hybrid code search combining semantic understanding with exact text matching. Ideal for finding code by meaning (e.g., 'authentication logic', 'database queries') even with typos or variations. Returns the most relevant code snippets with file locations and line numbers.",
|
|
194
353
|
inputSchema: {
|
|
195
|
-
type:
|
|
354
|
+
type: 'object',
|
|
196
355
|
properties: {
|
|
197
356
|
query: {
|
|
198
|
-
type:
|
|
199
|
-
description:
|
|
357
|
+
type: 'string',
|
|
358
|
+
description:
|
|
359
|
+
"Search query - can be natural language (e.g., 'where do we handle user login') or specific terms",
|
|
200
360
|
},
|
|
201
361
|
maxResults: {
|
|
202
|
-
type:
|
|
203
|
-
description:
|
|
204
|
-
default: config.maxResults
|
|
205
|
-
}
|
|
362
|
+
type: 'number',
|
|
363
|
+
description: 'Maximum number of results to return (default: from config)',
|
|
364
|
+
default: config.maxResults,
|
|
365
|
+
},
|
|
206
366
|
},
|
|
207
|
-
required: [
|
|
367
|
+
required: ['query'],
|
|
208
368
|
},
|
|
209
369
|
annotations: {
|
|
210
|
-
title:
|
|
370
|
+
title: 'Semantic Code Search',
|
|
211
371
|
readOnlyHint: true,
|
|
212
372
|
destructiveHint: false,
|
|
213
373
|
idempotentHint: true,
|
|
214
|
-
openWorldHint: false
|
|
215
|
-
}
|
|
374
|
+
openWorldHint: false,
|
|
375
|
+
},
|
|
216
376
|
};
|
|
217
377
|
}
|
|
218
378
|
|
|
@@ -225,13 +385,13 @@ export async function handleToolCall(request, hybridSearch) {
|
|
|
225
385
|
|
|
226
386
|
if (message) {
|
|
227
387
|
return {
|
|
228
|
-
content: [{ type:
|
|
388
|
+
content: [{ type: 'text', text: message }],
|
|
229
389
|
};
|
|
230
390
|
}
|
|
231
391
|
|
|
232
|
-
const formattedText = hybridSearch.formatResults(results);
|
|
392
|
+
const formattedText = await hybridSearch.formatResults(results);
|
|
233
393
|
|
|
234
394
|
return {
|
|
235
|
-
content: [{ type:
|
|
395
|
+
content: [{ type: 'text', text: formattedText }],
|
|
236
396
|
};
|
|
237
397
|
}
|