viberag 0.1.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/LICENSE +661 -0
- package/README.md +219 -0
- package/dist/cli/__tests__/mcp-setup.test.d.ts +6 -0
- package/dist/cli/__tests__/mcp-setup.test.js +597 -0
- package/dist/cli/app.d.ts +2 -0
- package/dist/cli/app.js +238 -0
- package/dist/cli/commands/handlers.d.ts +57 -0
- package/dist/cli/commands/handlers.js +231 -0
- package/dist/cli/commands/index.d.ts +2 -0
- package/dist/cli/commands/index.js +2 -0
- package/dist/cli/commands/mcp-setup.d.ts +107 -0
- package/dist/cli/commands/mcp-setup.js +509 -0
- package/dist/cli/commands/useRagCommands.d.ts +23 -0
- package/dist/cli/commands/useRagCommands.js +180 -0
- package/dist/cli/components/CleanWizard.d.ts +17 -0
- package/dist/cli/components/CleanWizard.js +169 -0
- package/dist/cli/components/InitWizard.d.ts +20 -0
- package/dist/cli/components/InitWizard.js +370 -0
- package/dist/cli/components/McpSetupWizard.d.ts +37 -0
- package/dist/cli/components/McpSetupWizard.js +387 -0
- package/dist/cli/components/SearchResultsDisplay.d.ts +13 -0
- package/dist/cli/components/SearchResultsDisplay.js +130 -0
- package/dist/cli/components/WelcomeBanner.d.ts +10 -0
- package/dist/cli/components/WelcomeBanner.js +26 -0
- package/dist/cli/components/index.d.ts +1 -0
- package/dist/cli/components/index.js +1 -0
- package/dist/cli/data/mcp-editors.d.ts +80 -0
- package/dist/cli/data/mcp-editors.js +270 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +26 -0
- package/dist/cli-bundle.cjs +5269 -0
- package/dist/common/commands/terminalSetup.d.ts +2 -0
- package/dist/common/commands/terminalSetup.js +144 -0
- package/dist/common/components/CommandSuggestions.d.ts +9 -0
- package/dist/common/components/CommandSuggestions.js +20 -0
- package/dist/common/components/StaticWithResize.d.ts +23 -0
- package/dist/common/components/StaticWithResize.js +62 -0
- package/dist/common/components/StatusBar.d.ts +8 -0
- package/dist/common/components/StatusBar.js +64 -0
- package/dist/common/components/TextInput.d.ts +12 -0
- package/dist/common/components/TextInput.js +239 -0
- package/dist/common/components/index.d.ts +3 -0
- package/dist/common/components/index.js +3 -0
- package/dist/common/hooks/index.d.ts +4 -0
- package/dist/common/hooks/index.js +4 -0
- package/dist/common/hooks/useCommandHistory.d.ts +7 -0
- package/dist/common/hooks/useCommandHistory.js +51 -0
- package/dist/common/hooks/useCtrlC.d.ts +9 -0
- package/dist/common/hooks/useCtrlC.js +40 -0
- package/dist/common/hooks/useKittyKeyboard.d.ts +10 -0
- package/dist/common/hooks/useKittyKeyboard.js +26 -0
- package/dist/common/hooks/useStaticOutputBuffer.d.ts +31 -0
- package/dist/common/hooks/useStaticOutputBuffer.js +58 -0
- package/dist/common/hooks/useTerminalResize.d.ts +28 -0
- package/dist/common/hooks/useTerminalResize.js +51 -0
- package/dist/common/hooks/useTextBuffer.d.ts +13 -0
- package/dist/common/hooks/useTextBuffer.js +165 -0
- package/dist/common/index.d.ts +13 -0
- package/dist/common/index.js +17 -0
- package/dist/common/types.d.ts +162 -0
- package/dist/common/types.js +1 -0
- package/dist/mcp/index.d.ts +12 -0
- package/dist/mcp/index.js +66 -0
- package/dist/mcp/server.d.ts +25 -0
- package/dist/mcp/server.js +837 -0
- package/dist/mcp/watcher.d.ts +86 -0
- package/dist/mcp/watcher.js +334 -0
- package/dist/rag/__tests__/grammar-smoke.test.d.ts +9 -0
- package/dist/rag/__tests__/grammar-smoke.test.js +161 -0
- package/dist/rag/__tests__/helpers.d.ts +30 -0
- package/dist/rag/__tests__/helpers.js +67 -0
- package/dist/rag/__tests__/merkle.test.d.ts +5 -0
- package/dist/rag/__tests__/merkle.test.js +161 -0
- package/dist/rag/__tests__/metadata-extraction.test.d.ts +10 -0
- package/dist/rag/__tests__/metadata-extraction.test.js +202 -0
- package/dist/rag/__tests__/multi-language.test.d.ts +13 -0
- package/dist/rag/__tests__/multi-language.test.js +535 -0
- package/dist/rag/__tests__/rag.test.d.ts +10 -0
- package/dist/rag/__tests__/rag.test.js +311 -0
- package/dist/rag/__tests__/search-exhaustive.test.d.ts +9 -0
- package/dist/rag/__tests__/search-exhaustive.test.js +87 -0
- package/dist/rag/__tests__/search-filters.test.d.ts +10 -0
- package/dist/rag/__tests__/search-filters.test.js +250 -0
- package/dist/rag/__tests__/search-modes.test.d.ts +8 -0
- package/dist/rag/__tests__/search-modes.test.js +133 -0
- package/dist/rag/config/index.d.ts +61 -0
- package/dist/rag/config/index.js +111 -0
- package/dist/rag/constants.d.ts +41 -0
- package/dist/rag/constants.js +57 -0
- package/dist/rag/embeddings/fastembed.d.ts +62 -0
- package/dist/rag/embeddings/fastembed.js +124 -0
- package/dist/rag/embeddings/gemini.d.ts +26 -0
- package/dist/rag/embeddings/gemini.js +116 -0
- package/dist/rag/embeddings/index.d.ts +10 -0
- package/dist/rag/embeddings/index.js +9 -0
- package/dist/rag/embeddings/local-4b.d.ts +28 -0
- package/dist/rag/embeddings/local-4b.js +51 -0
- package/dist/rag/embeddings/local.d.ts +29 -0
- package/dist/rag/embeddings/local.js +119 -0
- package/dist/rag/embeddings/mistral.d.ts +22 -0
- package/dist/rag/embeddings/mistral.js +85 -0
- package/dist/rag/embeddings/openai.d.ts +22 -0
- package/dist/rag/embeddings/openai.js +85 -0
- package/dist/rag/embeddings/types.d.ts +37 -0
- package/dist/rag/embeddings/types.js +1 -0
- package/dist/rag/gitignore/index.d.ts +57 -0
- package/dist/rag/gitignore/index.js +178 -0
- package/dist/rag/index.d.ts +15 -0
- package/dist/rag/index.js +25 -0
- package/dist/rag/indexer/chunker.d.ts +129 -0
- package/dist/rag/indexer/chunker.js +1352 -0
- package/dist/rag/indexer/index.d.ts +6 -0
- package/dist/rag/indexer/index.js +6 -0
- package/dist/rag/indexer/indexer.d.ts +73 -0
- package/dist/rag/indexer/indexer.js +356 -0
- package/dist/rag/indexer/types.d.ts +68 -0
- package/dist/rag/indexer/types.js +47 -0
- package/dist/rag/logger/index.d.ts +20 -0
- package/dist/rag/logger/index.js +75 -0
- package/dist/rag/manifest/index.d.ts +50 -0
- package/dist/rag/manifest/index.js +97 -0
- package/dist/rag/merkle/diff.d.ts +26 -0
- package/dist/rag/merkle/diff.js +95 -0
- package/dist/rag/merkle/hash.d.ts +34 -0
- package/dist/rag/merkle/hash.js +165 -0
- package/dist/rag/merkle/index.d.ts +68 -0
- package/dist/rag/merkle/index.js +298 -0
- package/dist/rag/merkle/node.d.ts +51 -0
- package/dist/rag/merkle/node.js +69 -0
- package/dist/rag/search/filters.d.ts +21 -0
- package/dist/rag/search/filters.js +100 -0
- package/dist/rag/search/fts.d.ts +32 -0
- package/dist/rag/search/fts.js +61 -0
- package/dist/rag/search/hybrid.d.ts +17 -0
- package/dist/rag/search/hybrid.js +58 -0
- package/dist/rag/search/index.d.ts +89 -0
- package/dist/rag/search/index.js +367 -0
- package/dist/rag/search/types.d.ts +130 -0
- package/dist/rag/search/types.js +4 -0
- package/dist/rag/search/vector.d.ts +25 -0
- package/dist/rag/search/vector.js +44 -0
- package/dist/rag/storage/index.d.ts +92 -0
- package/dist/rag/storage/index.js +287 -0
- package/dist/rag/storage/lancedb-native.d.ts +7 -0
- package/dist/rag/storage/lancedb-native.js +10 -0
- package/dist/rag/storage/schema.d.ts +23 -0
- package/dist/rag/storage/schema.js +50 -0
- package/dist/rag/storage/types.d.ts +100 -0
- package/dist/rag/storage/types.js +68 -0
- package/package.json +67 -0
- package/scripts/check-node-version.js +37 -0
|
@@ -0,0 +1,837 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Server for VibeRAG
|
|
3
|
+
*
|
|
4
|
+
* Exposes RAG functionality via Model Context Protocol.
|
|
5
|
+
* Tools: codebase_search, codebase_parallel_search, viberag_index, viberag_status, viberag_watch_status
|
|
6
|
+
*
|
|
7
|
+
* Includes file watcher for automatic incremental indexing.
|
|
8
|
+
*/
|
|
9
|
+
import { createRequire } from 'node:module';
|
|
10
|
+
import { FastMCP } from 'fastmcp';
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
import { SearchEngine, Indexer, configExists, loadManifest, manifestExists, loadConfig, saveConfig, PROVIDER_CONFIGS, getSchemaVersionInfo, } from '../rag/index.js';
|
|
13
|
+
import { FileWatcher } from './watcher.js';
|
|
14
|
+
const require = createRequire(import.meta.url);
|
|
15
|
+
const pkg = require('../../package.json');
|
|
16
|
+
/**
|
|
17
|
+
* Error thrown when project is not initialized.
|
|
18
|
+
*/
|
|
19
|
+
class NotInitializedError extends Error {
|
|
20
|
+
constructor(projectRoot) {
|
|
21
|
+
super(`VibeRAG not initialized in ${projectRoot}. ` +
|
|
22
|
+
`Run 'npx viberag' and use /init command first.`);
|
|
23
|
+
this.name = 'NotInitializedError';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Verify project is initialized, throw if not.
|
|
28
|
+
*/
|
|
29
|
+
async function ensureInitialized(projectRoot) {
|
|
30
|
+
const exists = await configExists(projectRoot);
|
|
31
|
+
if (!exists) {
|
|
32
|
+
throw new NotInitializedError(projectRoot);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Default maximum response size in bytes (100KB).
|
|
37
|
+
* Reduces result count to fit; does NOT truncate text.
|
|
38
|
+
*/
|
|
39
|
+
const DEFAULT_MAX_RESPONSE_SIZE = 100 * 1024;
|
|
40
|
+
/**
|
|
41
|
+
* Maximum allowed response size (500KB).
|
|
42
|
+
*/
|
|
43
|
+
const MAX_RESPONSE_SIZE = 500 * 1024;
|
|
44
|
+
/**
|
|
45
|
+
* Overhead per result in JSON (metadata fields, formatting).
|
|
46
|
+
*/
|
|
47
|
+
const RESULT_OVERHEAD_BYTES = 200;
|
|
48
|
+
/**
|
|
49
|
+
* Estimate JSON response size for a set of results.
|
|
50
|
+
*/
|
|
51
|
+
function estimateResponseSize(results) {
|
|
52
|
+
const textSize = results.reduce((sum, r) => sum + r.text.length, 0);
|
|
53
|
+
const overhead = results.length * RESULT_OVERHEAD_BYTES + 500; // Base JSON overhead
|
|
54
|
+
return textSize + overhead;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Cap results to fit within max response size.
|
|
58
|
+
* Removes results from the end (lowest relevance) until size fits.
|
|
59
|
+
*/
|
|
60
|
+
function capResultsToSize(results, maxSize) {
|
|
61
|
+
if (results.length === 0)
|
|
62
|
+
return results;
|
|
63
|
+
// Quick check: if current size is within limit, return as-is
|
|
64
|
+
const currentSize = estimateResponseSize(results);
|
|
65
|
+
if (currentSize <= maxSize)
|
|
66
|
+
return results;
|
|
67
|
+
// Binary search for optimal result count
|
|
68
|
+
let low = 1;
|
|
69
|
+
let high = results.length;
|
|
70
|
+
let bestCount = 1;
|
|
71
|
+
while (low <= high) {
|
|
72
|
+
const mid = Math.floor((low + high) / 2);
|
|
73
|
+
const subset = results.slice(0, mid);
|
|
74
|
+
const size = estimateResponseSize(subset);
|
|
75
|
+
if (size <= maxSize) {
|
|
76
|
+
bestCount = mid;
|
|
77
|
+
low = mid + 1;
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
high = mid - 1;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return results.slice(0, bestCount);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Format search results for MCP response.
|
|
87
|
+
*
|
|
88
|
+
* @param maxResponseSize - Maximum response size in bytes. Reduces result count to fit.
|
|
89
|
+
*/
|
|
90
|
+
function formatSearchResults(results, includeDebug = false, maxResponseSize = DEFAULT_MAX_RESPONSE_SIZE) {
|
|
91
|
+
if (results.results.length === 0) {
|
|
92
|
+
const response = {
|
|
93
|
+
message: `No results found for "${results.query}"`,
|
|
94
|
+
mode: results.searchType,
|
|
95
|
+
elapsedMs: results.elapsedMs,
|
|
96
|
+
results: [],
|
|
97
|
+
};
|
|
98
|
+
// Include debug info even for empty results (helps diagnose issues)
|
|
99
|
+
if (includeDebug && results.debug) {
|
|
100
|
+
response['debug'] = formatDebugInfo(results.debug);
|
|
101
|
+
}
|
|
102
|
+
return JSON.stringify(response);
|
|
103
|
+
}
|
|
104
|
+
// Cap results to fit within max response size
|
|
105
|
+
const cappedResults = capResultsToSize(results.results, maxResponseSize);
|
|
106
|
+
const wasReduced = cappedResults.length < results.results.length;
|
|
107
|
+
const response = {
|
|
108
|
+
query: results.query,
|
|
109
|
+
mode: results.searchType,
|
|
110
|
+
elapsedMs: results.elapsedMs,
|
|
111
|
+
resultCount: cappedResults.length,
|
|
112
|
+
results: cappedResults.map(r => ({
|
|
113
|
+
type: r.type,
|
|
114
|
+
name: r.name || '(anonymous)',
|
|
115
|
+
filepath: r.filepath,
|
|
116
|
+
startLine: r.startLine,
|
|
117
|
+
endLine: r.endLine,
|
|
118
|
+
score: Number(r.score.toFixed(4)),
|
|
119
|
+
vectorScore: r.vectorScore ? Number(r.vectorScore.toFixed(4)) : undefined,
|
|
120
|
+
ftsScore: r.ftsScore ? Number(r.ftsScore.toFixed(4)) : undefined,
|
|
121
|
+
signature: r.signature ?? undefined,
|
|
122
|
+
isExported: r.isExported ?? undefined,
|
|
123
|
+
text: r.text,
|
|
124
|
+
})),
|
|
125
|
+
};
|
|
126
|
+
// Add indicator if results were reduced due to size
|
|
127
|
+
if (wasReduced) {
|
|
128
|
+
response['originalResultCount'] = results.results.length;
|
|
129
|
+
response['reducedForSize'] = true;
|
|
130
|
+
}
|
|
131
|
+
// Add totalMatches for exhaustive mode
|
|
132
|
+
if (results.totalMatches !== undefined) {
|
|
133
|
+
response['totalMatches'] = results.totalMatches;
|
|
134
|
+
}
|
|
135
|
+
// Add debug info for AI evaluation
|
|
136
|
+
if (includeDebug && results.debug) {
|
|
137
|
+
response['debug'] = formatDebugInfo(results.debug);
|
|
138
|
+
}
|
|
139
|
+
return JSON.stringify(response);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Format debug info with quality assessment and suggestions.
|
|
143
|
+
*/
|
|
144
|
+
function formatDebugInfo(debug) {
|
|
145
|
+
const searchQuality = debug.maxVectorScore > 0.5
|
|
146
|
+
? 'high'
|
|
147
|
+
: debug.maxVectorScore > 0.3
|
|
148
|
+
? 'medium'
|
|
149
|
+
: 'low';
|
|
150
|
+
const result = {
|
|
151
|
+
maxVectorScore: Number(debug.maxVectorScore.toFixed(4)),
|
|
152
|
+
maxFtsScore: Number(debug.maxFtsScore.toFixed(4)),
|
|
153
|
+
requestedBm25Weight: Number(debug.requestedBm25Weight.toFixed(2)),
|
|
154
|
+
effectiveBm25Weight: Number(debug.effectiveBm25Weight.toFixed(2)),
|
|
155
|
+
autoBoostApplied: debug.autoBoostApplied,
|
|
156
|
+
vectorResultCount: debug.vectorResultCount,
|
|
157
|
+
ftsResultCount: debug.ftsResultCount,
|
|
158
|
+
searchQuality,
|
|
159
|
+
};
|
|
160
|
+
// Add oversample info if present
|
|
161
|
+
if (debug.oversampleMultiplier !== undefined) {
|
|
162
|
+
result['oversampleMultiplier'] = Number(debug.oversampleMultiplier.toFixed(2));
|
|
163
|
+
}
|
|
164
|
+
if (debug.dynamicOversampleApplied !== undefined) {
|
|
165
|
+
result['dynamicOversampleApplied'] = debug.dynamicOversampleApplied;
|
|
166
|
+
}
|
|
167
|
+
// Add suggestion if search quality is low but FTS found results
|
|
168
|
+
if (debug.maxVectorScore < 0.3 && debug.maxFtsScore > 1) {
|
|
169
|
+
result['suggestion'] =
|
|
170
|
+
'Consider exact mode or higher bm25_weight for this query';
|
|
171
|
+
}
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Format index stats for MCP response.
|
|
176
|
+
*/
|
|
177
|
+
function formatIndexStats(stats) {
|
|
178
|
+
return JSON.stringify({
|
|
179
|
+
message: 'Index complete',
|
|
180
|
+
filesScanned: stats.filesScanned,
|
|
181
|
+
filesNew: stats.filesNew,
|
|
182
|
+
filesModified: stats.filesModified,
|
|
183
|
+
filesDeleted: stats.filesDeleted,
|
|
184
|
+
chunksAdded: stats.chunksAdded,
|
|
185
|
+
chunksDeleted: stats.chunksDeleted,
|
|
186
|
+
embeddingsComputed: stats.embeddingsComputed,
|
|
187
|
+
embeddingsCached: stats.embeddingsCached,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Create and configure the MCP server with file watcher.
|
|
192
|
+
*/
|
|
193
|
+
export function createMcpServer(projectRoot) {
|
|
194
|
+
const server = new FastMCP({
|
|
195
|
+
name: 'viberag',
|
|
196
|
+
version: pkg.version,
|
|
197
|
+
});
|
|
198
|
+
// Create file watcher
|
|
199
|
+
const watcher = new FileWatcher(projectRoot);
|
|
200
|
+
// Filters schema for transparent, AI-controlled filtering
|
|
201
|
+
const filtersSchema = z
|
|
202
|
+
.object({
|
|
203
|
+
path_prefix: z
|
|
204
|
+
.string()
|
|
205
|
+
.optional()
|
|
206
|
+
.describe('Scope to directory (e.g., "src/api/")'),
|
|
207
|
+
path_contains: z
|
|
208
|
+
.array(z.string())
|
|
209
|
+
.optional()
|
|
210
|
+
.describe('Path must contain ALL strings - AND logic (e.g., ["services", "user"])'),
|
|
211
|
+
path_not_contains: z
|
|
212
|
+
.array(z.string())
|
|
213
|
+
.optional()
|
|
214
|
+
.describe('Exclude if path contains ANY string - OR logic (e.g., ["test", "__tests__", "_test.", ".spec."])'),
|
|
215
|
+
type: z
|
|
216
|
+
.array(z.enum(['function', 'class', 'method', 'module']))
|
|
217
|
+
.optional()
|
|
218
|
+
.describe('Match ANY type - OR logic (e.g., ["function", "method"])'),
|
|
219
|
+
extension: z
|
|
220
|
+
.array(z.string())
|
|
221
|
+
.optional()
|
|
222
|
+
.describe('Match ANY extension - OR logic (e.g., [".ts", ".py"])'),
|
|
223
|
+
is_exported: z
|
|
224
|
+
.boolean()
|
|
225
|
+
.optional()
|
|
226
|
+
.describe('Only public/exported symbols (Go: Capitalized, Python: no _ prefix, JS/TS: export)'),
|
|
227
|
+
decorator_contains: z
|
|
228
|
+
.string()
|
|
229
|
+
.optional()
|
|
230
|
+
.describe('Has decorator/attribute containing string (Python: @route, Java: @GetMapping, Rust: #[test])'),
|
|
231
|
+
has_docstring: z
|
|
232
|
+
.boolean()
|
|
233
|
+
.optional()
|
|
234
|
+
.describe('Only code with doc comments'),
|
|
235
|
+
})
|
|
236
|
+
.optional();
|
|
237
|
+
// Tool: codebase_search
|
|
238
|
+
server.addTool({
|
|
239
|
+
name: 'codebase_search',
|
|
240
|
+
description: `
|
|
241
|
+
Codebase search: semantic search, keyword search, and hybrid search options.
|
|
242
|
+
Use this when you need to find code that matches semantic meaning and keyword patterns.
|
|
243
|
+
This tool helps you perform exhaustive searches of the codebase and get the best
|
|
244
|
+
context and understanding when exploring and searching the codebase, docs, etc.
|
|
245
|
+
|
|
246
|
+
USE FOR CODEBASE EXPLORATION:
|
|
247
|
+
- Finds related code that grep/glob/read would miss.
|
|
248
|
+
- Semantic codebase search will find more relevant files.
|
|
249
|
+
|
|
250
|
+
EXAMPLES:
|
|
251
|
+
- "How does X work?"
|
|
252
|
+
- "Where is Y handled?"
|
|
253
|
+
- "Find patterns for Z"
|
|
254
|
+
- "Help me understand X"
|
|
255
|
+
- "How are we handling [exception type]"
|
|
256
|
+
- "Find every instance of Y"
|
|
257
|
+
|
|
258
|
+
WHEN TO USE codebase_search:
|
|
259
|
+
- You're exploring or investigating
|
|
260
|
+
- You need comprehensive results (not just the first match)
|
|
261
|
+
- The question is conceptual ("how does X work" vs "where is function Y")
|
|
262
|
+
- You want to find related code you might not have thought to search for
|
|
263
|
+
- Find semantically similar code when you don't know the exact function or variable names
|
|
264
|
+
|
|
265
|
+
REAL EXAMPLE: Asked "how are API keys configured?"
|
|
266
|
+
- grep finds: 3 files with "API_KEY" pattern
|
|
267
|
+
- viberag finds: 36 results including provider implementations, ADR docs explaining
|
|
268
|
+
design decisions, init wizard UI, error messages, and config interfaces
|
|
269
|
+
|
|
270
|
+
DELEGATE TO A BACKGROUND AGENT
|
|
271
|
+
- Tell sub-agents / background agents to run viberag codebase_search mcp tool
|
|
272
|
+
- Tell task/explore to use viberag codebase_search and then synthesize findings and results and return a summary
|
|
273
|
+
- Context management: This reduces token usage in the main conversation context
|
|
274
|
+
- For simple lookups ("where is function X?"), direct viberag calls are fine.
|
|
275
|
+
|
|
276
|
+
MODE SELECTION:
|
|
277
|
+
- 'hybrid' (default): Combined semantic + keyword. Start here for most queries.
|
|
278
|
+
- 'semantic': Pure meaning-based search. Best for conceptual queries.
|
|
279
|
+
- 'exact': Pure keyword/BM25. Best for symbol names, specific strings.
|
|
280
|
+
- 'definition': Direct symbol lookup. Fastest for "where is X defined?"
|
|
281
|
+
- 'similar': Find code similar to a snippet. Pass code_snippet parameter.
|
|
282
|
+
|
|
283
|
+
WEIGHT TUNING (hybrid mode):
|
|
284
|
+
The bm25_weight parameter (0-1) balances keyword vs semantic matching:
|
|
285
|
+
- 0.2-0.3: Favor semantic (conceptual queries like "how does X work")
|
|
286
|
+
- 0.5: Balanced (documentation, prose, mixed content)
|
|
287
|
+
- 0.7-0.9: Favor keywords (symbol names, exact strings, specific terms)
|
|
288
|
+
|
|
289
|
+
AUTO-BOOST:
|
|
290
|
+
By default, auto_boost=true increases keyword weight when semantic scores are low.
|
|
291
|
+
This helps find content that doesn't match code embeddings (docs, comments, prose).
|
|
292
|
+
Set auto_boost=false for precise control or comparative searches.
|
|
293
|
+
|
|
294
|
+
ITERATIVE STRATEGY:
|
|
295
|
+
For thorough searches, consider:
|
|
296
|
+
1. Start with hybrid mode, default weights
|
|
297
|
+
2. Check debug info to evaluate search quality
|
|
298
|
+
3. If maxVectorScore < 0.3, try exact mode or higher bm25_weight
|
|
299
|
+
4. If results seem incomplete, try codebase_parallel_search for comparison
|
|
300
|
+
5. Use exhaustive=true for refactoring tasks needing ALL matches
|
|
301
|
+
|
|
302
|
+
RESULT INTERPRETATION:
|
|
303
|
+
- score: Combined relevance (higher = better)
|
|
304
|
+
- vectorScore: Semantic similarity (0-1, may be missing for exact mode)
|
|
305
|
+
- ftsScore: Keyword match strength (BM25 score)
|
|
306
|
+
- debug.searchQuality: 'high', 'medium', or 'low' based on vector scores
|
|
307
|
+
- debug.suggestion: Hints when different settings might work better
|
|
308
|
+
|
|
309
|
+
FILTERS (transparent, you control what's excluded):
|
|
310
|
+
Path filters:
|
|
311
|
+
- path_prefix: Scope to directory (e.g., "src/api/")
|
|
312
|
+
- path_contains: Path must contain ALL strings (AND logic)
|
|
313
|
+
- path_not_contains: Exclude if path contains ANY string (OR logic)
|
|
314
|
+
|
|
315
|
+
Code filters:
|
|
316
|
+
- type: Match ANY of ["function", "class", "method", "module"]
|
|
317
|
+
- extension: Match ANY extension (e.g., [".ts", ".py"])
|
|
318
|
+
|
|
319
|
+
Metadata filters:
|
|
320
|
+
- is_exported: Only public/exported symbols
|
|
321
|
+
- has_docstring: Only code with documentation comments
|
|
322
|
+
- decorator_contains: Has decorator/attribute matching string
|
|
323
|
+
|
|
324
|
+
COMMON PATTERNS:
|
|
325
|
+
Exclude tests: { path_not_contains: ["test", "__tests__", ".spec.", "mock"] }
|
|
326
|
+
Find API endpoints: { decorator_contains: "Get", is_exported: true }
|
|
327
|
+
Production code: { path_not_contains: ["test", "mock", "fixture"], is_exported: true }`,
|
|
328
|
+
parameters: z.object({
|
|
329
|
+
query: z.string().describe('The search query in natural language'),
|
|
330
|
+
mode: z
|
|
331
|
+
.enum(['semantic', 'exact', 'hybrid', 'definition', 'similar'])
|
|
332
|
+
.optional()
|
|
333
|
+
.default('hybrid')
|
|
334
|
+
.describe('Search mode (default: hybrid)'),
|
|
335
|
+
code_snippet: z
|
|
336
|
+
.string()
|
|
337
|
+
.optional()
|
|
338
|
+
.describe("For mode='similar': code to find similar matches for"),
|
|
339
|
+
symbol_name: z
|
|
340
|
+
.string()
|
|
341
|
+
.optional()
|
|
342
|
+
.describe("For mode='definition': exact symbol name to look up"),
|
|
343
|
+
limit: z
|
|
344
|
+
.number()
|
|
345
|
+
.min(1)
|
|
346
|
+
.max(100)
|
|
347
|
+
.optional()
|
|
348
|
+
.default(10)
|
|
349
|
+
.describe('Maximum number of results (1-100, default: 10)'),
|
|
350
|
+
exhaustive: z
|
|
351
|
+
.boolean()
|
|
352
|
+
.optional()
|
|
353
|
+
.default(false)
|
|
354
|
+
.describe('Return all matches (for refactoring/auditing)'),
|
|
355
|
+
min_score: z
|
|
356
|
+
.number()
|
|
357
|
+
.min(0)
|
|
358
|
+
.max(1)
|
|
359
|
+
.optional()
|
|
360
|
+
.describe('Minimum relevance score threshold (0-1)'),
|
|
361
|
+
filters: filtersSchema.describe('Transparent filters (see description)'),
|
|
362
|
+
bm25_weight: z
|
|
363
|
+
.number()
|
|
364
|
+
.min(0)
|
|
365
|
+
.max(1)
|
|
366
|
+
.optional()
|
|
367
|
+
.describe('Balance between keyword (BM25) and semantic search in hybrid mode. ' +
|
|
368
|
+
'Higher values favor exact keyword matches, lower values favor semantic similarity. ' +
|
|
369
|
+
'Guidelines: 0.7-0.9 for symbol names/exact strings, 0.5 for documentation/prose, ' +
|
|
370
|
+
'0.2-0.3 for conceptual queries (default: 0.3). Ignored for non-hybrid modes.'),
|
|
371
|
+
auto_boost: z
|
|
372
|
+
.boolean()
|
|
373
|
+
.optional()
|
|
374
|
+
.default(true)
|
|
375
|
+
.describe('When true (default), automatically boosts BM25 weight if semantic scores are low. ' +
|
|
376
|
+
'Set to false for precise control over weights or when running comparative searches.'),
|
|
377
|
+
auto_boost_threshold: z
|
|
378
|
+
.number()
|
|
379
|
+
.min(0)
|
|
380
|
+
.max(1)
|
|
381
|
+
.optional()
|
|
382
|
+
.default(0.3)
|
|
383
|
+
.describe('Vector score threshold below which auto-boost activates (default: 0.3). ' +
|
|
384
|
+
'Lower values make auto-boost more aggressive. Only applies when auto_boost=true.'),
|
|
385
|
+
return_debug: z
|
|
386
|
+
.boolean()
|
|
387
|
+
.optional()
|
|
388
|
+
.describe('Include search diagnostics: max_vector_score, max_fts_score, effective_bm25_weight. ' +
|
|
389
|
+
'Defaults to true for hybrid mode, false for other modes. ' +
|
|
390
|
+
'Useful for evaluating search quality and tuning parameters.'),
|
|
391
|
+
max_response_size: z
|
|
392
|
+
.number()
|
|
393
|
+
.min(1024)
|
|
394
|
+
.max(MAX_RESPONSE_SIZE)
|
|
395
|
+
.optional()
|
|
396
|
+
.default(DEFAULT_MAX_RESPONSE_SIZE)
|
|
397
|
+
.describe('Maximum response size in bytes (default: 100KB, max: 500KB). ' +
|
|
398
|
+
'Reduces result count to fit within limit; does NOT truncate text content. ' +
|
|
399
|
+
'Use a larger value for exhaustive searches.'),
|
|
400
|
+
}),
|
|
401
|
+
execute: async (args) => {
|
|
402
|
+
await ensureInitialized(projectRoot);
|
|
403
|
+
// Convert snake_case filter keys to camelCase
|
|
404
|
+
const filters = args.filters
|
|
405
|
+
? {
|
|
406
|
+
pathPrefix: args.filters.path_prefix,
|
|
407
|
+
pathContains: args.filters.path_contains,
|
|
408
|
+
pathNotContains: args.filters.path_not_contains,
|
|
409
|
+
type: args.filters.type,
|
|
410
|
+
extension: args.filters.extension,
|
|
411
|
+
isExported: args.filters.is_exported,
|
|
412
|
+
decoratorContains: args.filters.decorator_contains,
|
|
413
|
+
hasDocstring: args.filters.has_docstring,
|
|
414
|
+
}
|
|
415
|
+
: undefined;
|
|
416
|
+
const engine = new SearchEngine(projectRoot);
|
|
417
|
+
try {
|
|
418
|
+
// Determine if debug info should be returned
|
|
419
|
+
const returnDebug = args.return_debug ??
|
|
420
|
+
(args.mode === 'hybrid' || args.mode === undefined);
|
|
421
|
+
const results = await engine.search(args.query, {
|
|
422
|
+
mode: args.mode,
|
|
423
|
+
limit: args.limit,
|
|
424
|
+
exhaustive: args.exhaustive,
|
|
425
|
+
minScore: args.min_score,
|
|
426
|
+
filters,
|
|
427
|
+
codeSnippet: args.code_snippet,
|
|
428
|
+
symbolName: args.symbol_name,
|
|
429
|
+
bm25Weight: args.bm25_weight,
|
|
430
|
+
autoBoost: args.auto_boost,
|
|
431
|
+
autoBoostThreshold: args.auto_boost_threshold,
|
|
432
|
+
returnDebug,
|
|
433
|
+
});
|
|
434
|
+
return formatSearchResults(results, returnDebug, args.max_response_size);
|
|
435
|
+
}
|
|
436
|
+
finally {
|
|
437
|
+
engine.close();
|
|
438
|
+
}
|
|
439
|
+
},
|
|
440
|
+
});
|
|
441
|
+
// Tool: viberag_index
|
|
442
|
+
server.addTool({
|
|
443
|
+
name: 'viberag_index',
|
|
444
|
+
description: 'Index the codebase for semantic search. Uses incremental indexing by default ' +
|
|
445
|
+
'(only processes changed files based on Merkle tree diff). ' +
|
|
446
|
+
'Use force=true for full reindex after config changes. ' +
|
|
447
|
+
'NOTE: Indexing can take time for large codebases. Consider running in a background ' +
|
|
448
|
+
'agent or delegating to a sub-agent if your platform supports it.',
|
|
449
|
+
parameters: z.object({
|
|
450
|
+
force: z
|
|
451
|
+
.boolean()
|
|
452
|
+
.optional()
|
|
453
|
+
.default(false)
|
|
454
|
+
.describe('Force full reindex, ignoring change detection (default: false)'),
|
|
455
|
+
}),
|
|
456
|
+
execute: async (args) => {
|
|
457
|
+
await ensureInitialized(projectRoot);
|
|
458
|
+
// When forcing reindex, sync config dimensions with current provider settings
|
|
459
|
+
// This handles cases where PROVIDER_CONFIGS dimensions changed (e.g., Gemini 768→1536)
|
|
460
|
+
if (args.force) {
|
|
461
|
+
const config = await loadConfig(projectRoot);
|
|
462
|
+
const currentDimensions = PROVIDER_CONFIGS[config.embeddingProvider]?.dimensions;
|
|
463
|
+
if (currentDimensions &&
|
|
464
|
+
config.embeddingDimensions !== currentDimensions) {
|
|
465
|
+
const updatedConfig = {
|
|
466
|
+
...config,
|
|
467
|
+
embeddingDimensions: currentDimensions,
|
|
468
|
+
embeddingModel: PROVIDER_CONFIGS[config.embeddingProvider].model,
|
|
469
|
+
};
|
|
470
|
+
await saveConfig(projectRoot, updatedConfig);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
const indexer = new Indexer(projectRoot);
|
|
474
|
+
try {
|
|
475
|
+
const stats = await indexer.index({ force: args.force });
|
|
476
|
+
return formatIndexStats(stats);
|
|
477
|
+
}
|
|
478
|
+
finally {
|
|
479
|
+
indexer.close();
|
|
480
|
+
}
|
|
481
|
+
},
|
|
482
|
+
});
|
|
483
|
+
// Tool: viberag_status
|
|
484
|
+
server.addTool({
|
|
485
|
+
name: 'viberag_status',
|
|
486
|
+
description: 'Get index status including file count, chunk count, embedding provider, schema version, and last update time. ' +
|
|
487
|
+
'If schema version is outdated, run viberag_index with force=true to reindex. ' +
|
|
488
|
+
'TIP: Check status before delegating exploration to sub-agents to ensure the index is current.',
|
|
489
|
+
parameters: z.object({}),
|
|
490
|
+
execute: async () => {
|
|
491
|
+
await ensureInitialized(projectRoot);
|
|
492
|
+
if (!(await manifestExists(projectRoot))) {
|
|
493
|
+
return JSON.stringify({
|
|
494
|
+
status: 'not_indexed',
|
|
495
|
+
message: 'No index found. Run viberag_index to create one.',
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
const manifest = await loadManifest(projectRoot);
|
|
499
|
+
const config = await loadConfig(projectRoot);
|
|
500
|
+
const schemaInfo = getSchemaVersionInfo(manifest);
|
|
501
|
+
const response = {
|
|
502
|
+
status: 'indexed',
|
|
503
|
+
version: manifest.version,
|
|
504
|
+
schemaVersion: schemaInfo.current,
|
|
505
|
+
createdAt: manifest.createdAt,
|
|
506
|
+
updatedAt: manifest.updatedAt,
|
|
507
|
+
totalFiles: manifest.stats.totalFiles,
|
|
508
|
+
totalChunks: manifest.stats.totalChunks,
|
|
509
|
+
embeddingProvider: config.embeddingProvider,
|
|
510
|
+
embeddingModel: config.embeddingModel,
|
|
511
|
+
embeddingDimensions: config.embeddingDimensions,
|
|
512
|
+
};
|
|
513
|
+
// Warn if schema version is outdated
|
|
514
|
+
if (schemaInfo.needsReindex) {
|
|
515
|
+
response['warning'] =
|
|
516
|
+
`Schema version ${schemaInfo.current} is outdated (current: ${schemaInfo.required}). ` +
|
|
517
|
+
`Run viberag_index with force=true to reindex and enable new features.`;
|
|
518
|
+
}
|
|
519
|
+
return JSON.stringify(response);
|
|
520
|
+
},
|
|
521
|
+
});
|
|
522
|
+
// Tool: viberag_watch_status
|
|
523
|
+
server.addTool({
|
|
524
|
+
name: 'viberag_watch_status',
|
|
525
|
+
description: 'Get file watcher status. Shows if auto-indexing is active, ' +
|
|
526
|
+
'how many files are being watched, pending changes, and last update time.',
|
|
527
|
+
parameters: z.object({}),
|
|
528
|
+
execute: async () => {
|
|
529
|
+
const status = watcher.getStatus();
|
|
530
|
+
return JSON.stringify(status);
|
|
531
|
+
},
|
|
532
|
+
});
|
|
533
|
+
// Tool: codebase_parallel_search
|
|
534
|
+
server.addTool({
|
|
535
|
+
name: 'codebase_parallel_search',
|
|
536
|
+
description: `
|
|
537
|
+
Codebase Parallel Search: run multiple semantic search, keyword search, and hybrid searches in parallel and compare results.
|
|
538
|
+
Use this when you need to run multiple searches at once to find code that matches semantic meaning and keyword patterns.
|
|
539
|
+
This tool helps you perform exhaustive searches of the codebase and get the best
|
|
540
|
+
context and understanding when exploring and searching the codebase, docs, etc.
|
|
541
|
+
|
|
542
|
+
NOTE: This is for narrower sets of queries. Parallel searches may return a large number of results,
|
|
543
|
+
it is best to keep search filters narrow and specific. For separate broader searches, use codebase_search one at a time.
|
|
544
|
+
|
|
545
|
+
USE FOR CODEBASE EXPLORATION:
|
|
546
|
+
- Finds related code that grep/glob/read would miss.
|
|
547
|
+
- Semantic codebase search will find more relevant files.
|
|
548
|
+
|
|
549
|
+
EXAMPLE: "How are embeddings configured?"
|
|
550
|
+
- codebase_search: Found 8 results (embedding provider files)
|
|
551
|
+
- codebase_parallel_search with 3 strategies: Found 24 unique results including:
|
|
552
|
+
* Provider implementations (what single search found)
|
|
553
|
+
* ADR docs explaining why certain providers were chosen
|
|
554
|
+
* Init wizard showing user-facing configuration
|
|
555
|
+
* Type definitions and interfaces
|
|
556
|
+
* Error handling and validation
|
|
557
|
+
|
|
558
|
+
WHEN TO USE:
|
|
559
|
+
- Need to test several search strategies at once
|
|
560
|
+
- Exploring a feature or system (not just looking up one thing)
|
|
561
|
+
- You want comprehensive coverage without multiple round-trips
|
|
562
|
+
- The topic has multiple related concepts (auth → session, JWT, tokens, login)
|
|
563
|
+
- You're not sure which search mode will work best
|
|
564
|
+
|
|
565
|
+
MODE SELECTION:
|
|
566
|
+
- 'hybrid' (default): Combined semantic + keyword. Start here for most queries.
|
|
567
|
+
- 'semantic': Pure meaning-based search. Best for conceptual queries.
|
|
568
|
+
- 'exact': Pure keyword/BM25. Best for symbol names, specific strings.
|
|
569
|
+
- 'definition': Direct symbol lookup. Fastest for "where is X defined?"
|
|
570
|
+
- 'similar': Find code similar to a snippet. Pass code_snippet parameter.
|
|
571
|
+
|
|
572
|
+
WEIGHT TUNING (hybrid mode):
|
|
573
|
+
The bm25_weight parameter (0-1) balances keyword vs semantic matching:
|
|
574
|
+
- 0.2-0.3: Favor semantic (conceptual queries like "how does X work")
|
|
575
|
+
- 0.5: Balanced (documentation, prose, mixed content)
|
|
576
|
+
- 0.7-0.9: Favor keywords (symbol names, exact strings, specific terms)
|
|
577
|
+
|
|
578
|
+
AUTO-BOOST:
|
|
579
|
+
By default, auto_boost=true increases keyword weight when semantic scores are low.
|
|
580
|
+
This helps find content that doesn't match code embeddings (docs, comments, prose).
|
|
581
|
+
Set auto_boost=false for precise control or comparative searches.
|
|
582
|
+
|
|
583
|
+
PARALLEL SEARCH STRATEGIES:
|
|
584
|
+
1. Mode comparison: [{mode:'semantic'}, {mode:'exact'}, {mode:'hybrid'}]
|
|
585
|
+
2. Related concepts: [{query:'auth'}, {query:'session'}, {query:'login'}]
|
|
586
|
+
3. Weight tuning: [{bm25_weight:0.2}, {bm25_weight:0.5}, {bm25_weight:0.8}]
|
|
587
|
+
|
|
588
|
+
USE CASES:
|
|
589
|
+
- Compare semantic vs keyword results for the same query
|
|
590
|
+
- Run same query with different weights to find optimal settings
|
|
591
|
+
- Search multiple related queries and aggregate results
|
|
592
|
+
- Implement multi-phase search strategies
|
|
593
|
+
|
|
594
|
+
RESULT INTERPRETATION:
|
|
595
|
+
- score: Combined relevance (higher = better)
|
|
596
|
+
- vectorScore: Semantic similarity (0-1, may be missing for exact mode)
|
|
597
|
+
- ftsScore: Keyword match strength (BM25 score)
|
|
598
|
+
- debug.searchQuality: 'high', 'medium', or 'low' based on vector scores
|
|
599
|
+
- debug.suggestion: Hints when different settings might work better
|
|
600
|
+
|
|
601
|
+
FILTERS (transparent, you control what's excluded):
|
|
602
|
+
Path filters:
|
|
603
|
+
- path_prefix: Scope to directory (e.g., "src/api/")
|
|
604
|
+
- path_contains: Path must contain ALL strings (AND logic)
|
|
605
|
+
- path_not_contains: Exclude if path contains ANY string (OR logic)
|
|
606
|
+
|
|
607
|
+
Code filters:
|
|
608
|
+
- type: Match ANY of ["function", "class", "method", "module"]
|
|
609
|
+
- extension: Match ANY extension (e.g., [".ts", ".py"])
|
|
610
|
+
|
|
611
|
+
Metadata filters:
|
|
612
|
+
- is_exported: Only public/exported symbols
|
|
613
|
+
- has_docstring: Only code with documentation comments
|
|
614
|
+
- decorator_contains: Has decorator/attribute matching string
|
|
615
|
+
`,
|
|
616
|
+
parameters: z.object({
|
|
617
|
+
searches: z
|
|
618
|
+
.array(z.object({
|
|
619
|
+
query: z.string().describe('Search query'),
|
|
620
|
+
mode: z
|
|
621
|
+
.enum(['semantic', 'exact', 'hybrid', 'definition', 'similar'])
|
|
622
|
+
.optional()
|
|
623
|
+
.describe('Search mode'),
|
|
624
|
+
bm25_weight: z
|
|
625
|
+
.number()
|
|
626
|
+
.min(0)
|
|
627
|
+
.max(1)
|
|
628
|
+
.optional()
|
|
629
|
+
.describe('BM25 weight for hybrid mode'),
|
|
630
|
+
auto_boost: z.boolean().optional().describe('Enable auto-boost'),
|
|
631
|
+
limit: z
|
|
632
|
+
.number()
|
|
633
|
+
.min(1)
|
|
634
|
+
.max(50)
|
|
635
|
+
.optional()
|
|
636
|
+
.default(10)
|
|
637
|
+
.describe('Max results per search'),
|
|
638
|
+
filters: filtersSchema,
|
|
639
|
+
}))
|
|
640
|
+
.min(1)
|
|
641
|
+
.max(5)
|
|
642
|
+
.describe('Array of search configurations (1-5)'),
|
|
643
|
+
merge_results: z
|
|
644
|
+
.boolean()
|
|
645
|
+
.optional()
|
|
646
|
+
.default(true)
|
|
647
|
+
.describe('Combine and dedupe results across all searches'),
|
|
648
|
+
merge_strategy: z
|
|
649
|
+
.enum(['rrf', 'dedupe'])
|
|
650
|
+
.optional()
|
|
651
|
+
.default('rrf')
|
|
652
|
+
.describe('How to merge: "rrf" - Reciprocal Rank Fusion (results in multiple searches rank higher), ' +
|
|
653
|
+
'"dedupe" - Simple deduplication (keep highest score)'),
|
|
654
|
+
merged_limit: z
|
|
655
|
+
.number()
|
|
656
|
+
.min(1)
|
|
657
|
+
.max(100)
|
|
658
|
+
.optional()
|
|
659
|
+
.default(20)
|
|
660
|
+
.describe('Max results in merged output'),
|
|
661
|
+
max_response_size: z
|
|
662
|
+
.number()
|
|
663
|
+
.min(1024)
|
|
664
|
+
.max(MAX_RESPONSE_SIZE)
|
|
665
|
+
.optional()
|
|
666
|
+
.default(DEFAULT_MAX_RESPONSE_SIZE)
|
|
667
|
+
.describe('Maximum response size in bytes (default: 100KB). ' +
|
|
668
|
+
'Reduces merged result count to fit; does NOT truncate text.'),
|
|
669
|
+
}),
|
|
670
|
+
execute: async (args) => {
|
|
671
|
+
await ensureInitialized(projectRoot);
|
|
672
|
+
const engine = new SearchEngine(projectRoot);
|
|
673
|
+
try {
|
|
674
|
+
// Run all searches in parallel
|
|
675
|
+
const searchPromises = args.searches.map(async (config, index) => {
|
|
676
|
+
const filters = config.filters
|
|
677
|
+
? {
|
|
678
|
+
pathPrefix: config.filters.path_prefix,
|
|
679
|
+
pathContains: config.filters.path_contains,
|
|
680
|
+
pathNotContains: config.filters.path_not_contains,
|
|
681
|
+
type: config.filters.type,
|
|
682
|
+
extension: config.filters.extension,
|
|
683
|
+
isExported: config.filters.is_exported,
|
|
684
|
+
decoratorContains: config.filters.decorator_contains,
|
|
685
|
+
hasDocstring: config.filters.has_docstring,
|
|
686
|
+
}
|
|
687
|
+
: undefined;
|
|
688
|
+
const results = await engine.search(config.query, {
|
|
689
|
+
mode: config.mode,
|
|
690
|
+
limit: config.limit,
|
|
691
|
+
filters,
|
|
692
|
+
bm25Weight: config.bm25_weight,
|
|
693
|
+
autoBoost: config.auto_boost,
|
|
694
|
+
returnDebug: true,
|
|
695
|
+
});
|
|
696
|
+
return {
|
|
697
|
+
index,
|
|
698
|
+
config: {
|
|
699
|
+
query: config.query,
|
|
700
|
+
mode: config.mode ?? 'hybrid',
|
|
701
|
+
bm25Weight: config.bm25_weight,
|
|
702
|
+
},
|
|
703
|
+
results,
|
|
704
|
+
};
|
|
705
|
+
});
|
|
706
|
+
const searchResults = await Promise.all(searchPromises);
|
|
707
|
+
// Build individual results
|
|
708
|
+
const individual = searchResults.map(sr => ({
|
|
709
|
+
searchIndex: sr.index,
|
|
710
|
+
config: sr.config,
|
|
711
|
+
resultCount: sr.results.results.length,
|
|
712
|
+
results: sr.results.results.map(r => ({
|
|
713
|
+
id: r.id,
|
|
714
|
+
type: r.type,
|
|
715
|
+
name: r.name || '(anonymous)',
|
|
716
|
+
filepath: r.filepath,
|
|
717
|
+
startLine: r.startLine,
|
|
718
|
+
endLine: r.endLine,
|
|
719
|
+
score: Number(r.score.toFixed(4)),
|
|
720
|
+
})),
|
|
721
|
+
debug: sr.results.debug,
|
|
722
|
+
}));
|
|
723
|
+
// Build merged results if requested
|
|
724
|
+
let merged;
|
|
725
|
+
if (args.merge_results) {
|
|
726
|
+
// Collect all results with their sources
|
|
727
|
+
const allResults = [];
|
|
728
|
+
// Group results by ID
|
|
729
|
+
const resultMap = new Map();
|
|
730
|
+
for (const sr of searchResults) {
|
|
731
|
+
sr.results.results.forEach((result, rank) => {
|
|
732
|
+
const existing = resultMap.get(result.id);
|
|
733
|
+
if (existing) {
|
|
734
|
+
existing.sources.push(sr.index);
|
|
735
|
+
existing.ranks.push(rank);
|
|
736
|
+
// Keep highest score
|
|
737
|
+
if (result.score > existing.result.score) {
|
|
738
|
+
existing.result = result;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
else {
|
|
742
|
+
resultMap.set(result.id, {
|
|
743
|
+
result,
|
|
744
|
+
sources: [sr.index],
|
|
745
|
+
ranks: [rank],
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
// Convert to array for sorting
|
|
751
|
+
for (const [, value] of resultMap) {
|
|
752
|
+
allResults.push(value);
|
|
753
|
+
}
|
|
754
|
+
// Sort by merge strategy
|
|
755
|
+
if (args.merge_strategy === 'rrf') {
|
|
756
|
+
// RRF: Sum of 1/(rank+k) across all sources
|
|
757
|
+
const k = 60; // RRF constant
|
|
758
|
+
allResults.sort((a, b) => {
|
|
759
|
+
const rrfA = a.ranks.reduce((sum, r) => sum + 1 / (r + k), 0);
|
|
760
|
+
const rrfB = b.ranks.reduce((sum, r) => sum + 1 / (r + k), 0);
|
|
761
|
+
return rrfB - rrfA; // Higher RRF score first
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
else {
|
|
765
|
+
// Dedupe: Sort by score, then by number of sources
|
|
766
|
+
allResults.sort((a, b) => {
|
|
767
|
+
if (b.sources.length !== a.sources.length) {
|
|
768
|
+
return b.sources.length - a.sources.length;
|
|
769
|
+
}
|
|
770
|
+
return b.result.score - a.result.score;
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
// Take top merged_limit results
|
|
774
|
+
let mergedResults = allResults
|
|
775
|
+
.slice(0, args.merged_limit)
|
|
776
|
+
.map(item => ({
|
|
777
|
+
id: item.result.id,
|
|
778
|
+
type: item.result.type,
|
|
779
|
+
name: item.result.name || '(anonymous)',
|
|
780
|
+
filepath: item.result.filepath,
|
|
781
|
+
startLine: item.result.startLine,
|
|
782
|
+
endLine: item.result.endLine,
|
|
783
|
+
score: Number(item.result.score.toFixed(4)),
|
|
784
|
+
sources: item.sources,
|
|
785
|
+
text: item.result.text,
|
|
786
|
+
}));
|
|
787
|
+
// Apply size capping to merged results
|
|
788
|
+
const cappedMerged = capResultsToSize(mergedResults.map(r => ({
|
|
789
|
+
...r,
|
|
790
|
+
id: r.id,
|
|
791
|
+
filename: '',
|
|
792
|
+
vectorScore: undefined,
|
|
793
|
+
ftsScore: undefined,
|
|
794
|
+
signature: undefined,
|
|
795
|
+
isExported: undefined,
|
|
796
|
+
})), args.max_response_size);
|
|
797
|
+
// Reduce to capped length if needed
|
|
798
|
+
if (cappedMerged.length < mergedResults.length) {
|
|
799
|
+
mergedResults = mergedResults.slice(0, cappedMerged.length);
|
|
800
|
+
}
|
|
801
|
+
// Calculate overlap statistics
|
|
802
|
+
const uniqueToSearch = args.searches.map((_, i) => allResults.filter(r => r.sources.length === 1 && r.sources[0] === i).length);
|
|
803
|
+
const overlapping = allResults.filter(r => r.sources.length > 1).length;
|
|
804
|
+
merged = {
|
|
805
|
+
strategy: args.merge_strategy,
|
|
806
|
+
totalUnique: allResults.length,
|
|
807
|
+
resultCount: mergedResults.length,
|
|
808
|
+
overlap: overlapping,
|
|
809
|
+
uniquePerSearch: uniqueToSearch,
|
|
810
|
+
results: mergedResults,
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
return JSON.stringify({
|
|
814
|
+
searchCount: args.searches.length,
|
|
815
|
+
individual,
|
|
816
|
+
merged,
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
finally {
|
|
820
|
+
engine.close();
|
|
821
|
+
}
|
|
822
|
+
},
|
|
823
|
+
});
|
|
824
|
+
return {
|
|
825
|
+
server,
|
|
826
|
+
watcher,
|
|
827
|
+
startWatcher: async () => {
|
|
828
|
+
// Only start watcher if project is initialized
|
|
829
|
+
if (await configExists(projectRoot)) {
|
|
830
|
+
await watcher.start();
|
|
831
|
+
}
|
|
832
|
+
},
|
|
833
|
+
stopWatcher: async () => {
|
|
834
|
+
await watcher.stop();
|
|
835
|
+
},
|
|
836
|
+
};
|
|
837
|
+
}
|