@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.
Files changed (109) hide show
  1. package/.agent/workflows/code-review.md +60 -0
  2. package/.prettierrc +7 -0
  3. package/ARCHITECTURE.md +105 -170
  4. package/CONTRIBUTING.md +32 -113
  5. package/GEMINI.md +73 -0
  6. package/LICENSE +21 -21
  7. package/README.md +161 -54
  8. package/config.json +876 -75
  9. package/debug-pids.js +27 -0
  10. package/eslint.config.js +36 -0
  11. package/features/ann-config.js +37 -26
  12. package/features/clear-cache.js +28 -19
  13. package/features/find-similar-code.js +142 -66
  14. package/features/hybrid-search.js +253 -93
  15. package/features/index-codebase.js +1455 -394
  16. package/features/lifecycle.js +813 -180
  17. package/features/register.js +58 -52
  18. package/index.js +450 -306
  19. package/lib/cache-ops.js +22 -0
  20. package/lib/cache-utils.js +68 -0
  21. package/lib/cache.js +1392 -587
  22. package/lib/call-graph.js +165 -50
  23. package/lib/cli.js +154 -0
  24. package/lib/config.js +462 -121
  25. package/lib/embedding-process.js +77 -0
  26. package/lib/embedding-worker.js +545 -30
  27. package/lib/ignore-patterns.js +61 -59
  28. package/lib/json-worker.js +14 -0
  29. package/lib/json-writer.js +344 -0
  30. package/lib/logging.js +88 -0
  31. package/lib/memory-logger.js +13 -0
  32. package/lib/project-detector.js +13 -17
  33. package/lib/server-lifecycle.js +38 -0
  34. package/lib/settings-editor.js +645 -0
  35. package/lib/tokenizer.js +207 -104
  36. package/lib/utils.js +273 -198
  37. package/lib/vector-store-binary.js +592 -0
  38. package/mcp_config.example.json +13 -0
  39. package/package.json +13 -2
  40. package/scripts/clear-cache.js +6 -17
  41. package/scripts/download-model.js +14 -9
  42. package/scripts/postinstall.js +5 -5
  43. package/search-configs.js +36 -0
  44. package/test/ann-config.test.js +179 -0
  45. package/test/ann-fallback.test.js +6 -6
  46. package/test/binary-store.test.js +69 -0
  47. package/test/cache-branches.test.js +120 -0
  48. package/test/cache-errors.test.js +264 -0
  49. package/test/cache-extra.test.js +300 -0
  50. package/test/cache-helpers.test.js +205 -0
  51. package/test/cache-hnsw-failure.test.js +40 -0
  52. package/test/cache-json-worker.test.js +190 -0
  53. package/test/cache-worker.test.js +102 -0
  54. package/test/cache.test.js +443 -0
  55. package/test/call-graph.test.js +103 -4
  56. package/test/clear-cache.test.js +69 -68
  57. package/test/code-review-workflow.test.js +50 -0
  58. package/test/config.test.js +418 -0
  59. package/test/coverage-gap.test.js +497 -0
  60. package/test/coverage-maximizer.test.js +236 -0
  61. package/test/debug-analysis.js +107 -0
  62. package/test/embedding-model.test.js +173 -103
  63. package/test/embedding-worker-extra.test.js +272 -0
  64. package/test/embedding-worker.test.js +158 -0
  65. package/test/features.test.js +139 -0
  66. package/test/final-boost.test.js +271 -0
  67. package/test/final-polish.test.js +183 -0
  68. package/test/final.test.js +95 -0
  69. package/test/find-similar-code.test.js +191 -0
  70. package/test/helpers.js +92 -11
  71. package/test/helpers.test.js +46 -0
  72. package/test/hybrid-search-basic.test.js +62 -0
  73. package/test/hybrid-search-branch.test.js +202 -0
  74. package/test/hybrid-search-callgraph.test.js +229 -0
  75. package/test/hybrid-search-extra.test.js +81 -0
  76. package/test/hybrid-search.test.js +484 -71
  77. package/test/index-cli.test.js +520 -0
  78. package/test/index-codebase-batch.test.js +119 -0
  79. package/test/index-codebase-branches.test.js +585 -0
  80. package/test/index-codebase-core.test.js +1032 -0
  81. package/test/index-codebase-edge-cases.test.js +254 -0
  82. package/test/index-codebase-errors.test.js +132 -0
  83. package/test/index-codebase-gap.test.js +239 -0
  84. package/test/index-codebase-lines.test.js +151 -0
  85. package/test/index-codebase-watcher.test.js +259 -0
  86. package/test/index-codebase-zone.test.js +259 -0
  87. package/test/index-codebase.test.js +371 -69
  88. package/test/index-memory.test.js +220 -0
  89. package/test/indexer-detailed.test.js +176 -0
  90. package/test/integration.test.js +148 -92
  91. package/test/json-worker.test.js +50 -0
  92. package/test/lifecycle.test.js +541 -0
  93. package/test/master.test.js +198 -0
  94. package/test/perfection.test.js +349 -0
  95. package/test/project-detector.test.js +65 -0
  96. package/test/register.test.js +262 -0
  97. package/test/tokenizer.test.js +55 -93
  98. package/test/ultra-maximizer.test.js +116 -0
  99. package/test/utils-branches.test.js +161 -0
  100. package/test/utils-extra.test.js +116 -0
  101. package/test/utils.test.js +131 -0
  102. package/test/verify_fixes.js +76 -0
  103. package/test/worker-errors.test.js +96 -0
  104. package/test/worker-init.test.js +102 -0
  105. package/test/worker_throttling.test.js +93 -0
  106. package/tools/scripts/benchmark-search.js +95 -0
  107. package/tools/scripts/cache-stats.js +71 -0
  108. package/tools/scripts/manual-search.js +34 -0
  109. package/vitest.config.js +19 -9
@@ -1,7 +1,7 @@
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";
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
- missing.push(file);
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
- const BATCH_SIZE = 200;
38
- for (let i = 0; i < missing.length; i += BATCH_SIZE) {
39
- const batch = missing.slice(i, i + BATCH_SIZE);
40
- await Promise.all(batch.map(async file => {
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
- const vectorStore = this.cache.getVectorStore();
58
-
59
- if (vectorStore.length === 0) {
60
- return {
61
- results: [],
62
- message: "No code has been indexed yet. Please wait for initial indexing to complete."
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
- // Generate query embedding
67
- const queryEmbed = await this.embedder(query, { pooling: "mean", normalize: true });
68
- const queryVector = Array.from(queryEmbed.data);
69
- const queryVectorTyped = queryEmbed.data;
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
- let candidates = vectorStore;
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, vectorStore.length);
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
- const seen = new Set();
79
- candidates = annLabels
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 && candidates.length < maxResults) {
90
- candidates = vectorStore;
91
- usedAnn = false;
122
+ if (!usedAnn) {
123
+ console.info(`[Search] Using full scan (${storeSize} chunks)`);
92
124
  }
93
125
 
94
- if (this.config.recencyBoost > 0) {
95
- await this.populateFileModTimes(candidates.map(chunk => chunk.file));
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
- // Score all chunks (synchronous map now, much faster)
99
- const scoredChunks = candidates.map(chunk => {
100
- // Semantic similarity (vectors are normalized)
101
- let score = dotSimilarity(queryVector, chunk.vector) * this.config.semanticWeight;
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
- // Exact match boost
104
- const lowerQuery = query.toLowerCase();
105
- const lowerContent = chunk.content.toLowerCase();
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
- if (lowerContent.includes(lowerQuery)) {
108
- score += this.config.exactMatchBoost;
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
- // Partial word matching
111
- const queryWords = lowerQuery.split(/\s+/);
112
- const matchedWords = queryWords.filter(word =>
113
- word.length > 2 && lowerContent.includes(word)
114
- ).length;
115
- score += (matchedWords / queryWords.length) * 0.3;
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
- // Recency boost - recently modified files rank higher
119
- if (this.config.recencyBoost > 0) {
120
- const mtime = this.fileModTimes.get(chunk.file);
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
- // Linear decay: full boost at 0 days, no boost after decayDays
126
- const recencyScore = Math.max(0, 1 - (daysSinceModified / decayDays));
127
- score += recencyScore * this.config.recencyBoost;
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
- return { ...chunk, score };
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 symbols = extractSymbolsFromContent(scoredChunks[i].content);
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 "No matching code found for your query.";
324
+ return 'No matching code found for your query.';
175
325
  }
176
326
 
177
- return results.map((r, idx) => {
327
+ const formatted = await Promise.all(results.map(async (r, idx) => {
178
328
  const relPath = path.relative(this.config.searchDirectory, r.file);
179
- return `## Result ${idx + 1} (Relevance: ${(r.score * 100).toFixed(1)}%)\n` +
180
- `**File:** \`${relPath}\`\n` +
181
- `**Lines:** ${r.startLine}-${r.endLine}\n\n` +
182
- "```" + path.extname(r.file).slice(1) + "\n" +
183
- r.content + "\n" +
184
- "```\n";
185
- }).join("\n");
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: "a_semantic_search",
193
- description: "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.",
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: "object",
354
+ type: 'object',
196
355
  properties: {
197
356
  query: {
198
- type: "string",
199
- description: "Search query - can be natural language (e.g., 'where do we handle user login') or specific terms"
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: "number",
203
- description: "Maximum number of results to return (default: from config)",
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: ["query"]
367
+ required: ['query'],
208
368
  },
209
369
  annotations: {
210
- title: "Semantic Code Search",
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: "text", text: message }]
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: "text", text: formattedText }]
395
+ content: [{ type: 'text', text: formattedText }],
236
396
  };
237
397
  }