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.
Files changed (151) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +219 -0
  3. package/dist/cli/__tests__/mcp-setup.test.d.ts +6 -0
  4. package/dist/cli/__tests__/mcp-setup.test.js +597 -0
  5. package/dist/cli/app.d.ts +2 -0
  6. package/dist/cli/app.js +238 -0
  7. package/dist/cli/commands/handlers.d.ts +57 -0
  8. package/dist/cli/commands/handlers.js +231 -0
  9. package/dist/cli/commands/index.d.ts +2 -0
  10. package/dist/cli/commands/index.js +2 -0
  11. package/dist/cli/commands/mcp-setup.d.ts +107 -0
  12. package/dist/cli/commands/mcp-setup.js +509 -0
  13. package/dist/cli/commands/useRagCommands.d.ts +23 -0
  14. package/dist/cli/commands/useRagCommands.js +180 -0
  15. package/dist/cli/components/CleanWizard.d.ts +17 -0
  16. package/dist/cli/components/CleanWizard.js +169 -0
  17. package/dist/cli/components/InitWizard.d.ts +20 -0
  18. package/dist/cli/components/InitWizard.js +370 -0
  19. package/dist/cli/components/McpSetupWizard.d.ts +37 -0
  20. package/dist/cli/components/McpSetupWizard.js +387 -0
  21. package/dist/cli/components/SearchResultsDisplay.d.ts +13 -0
  22. package/dist/cli/components/SearchResultsDisplay.js +130 -0
  23. package/dist/cli/components/WelcomeBanner.d.ts +10 -0
  24. package/dist/cli/components/WelcomeBanner.js +26 -0
  25. package/dist/cli/components/index.d.ts +1 -0
  26. package/dist/cli/components/index.js +1 -0
  27. package/dist/cli/data/mcp-editors.d.ts +80 -0
  28. package/dist/cli/data/mcp-editors.js +270 -0
  29. package/dist/cli/index.d.ts +2 -0
  30. package/dist/cli/index.js +26 -0
  31. package/dist/cli-bundle.cjs +5269 -0
  32. package/dist/common/commands/terminalSetup.d.ts +2 -0
  33. package/dist/common/commands/terminalSetup.js +144 -0
  34. package/dist/common/components/CommandSuggestions.d.ts +9 -0
  35. package/dist/common/components/CommandSuggestions.js +20 -0
  36. package/dist/common/components/StaticWithResize.d.ts +23 -0
  37. package/dist/common/components/StaticWithResize.js +62 -0
  38. package/dist/common/components/StatusBar.d.ts +8 -0
  39. package/dist/common/components/StatusBar.js +64 -0
  40. package/dist/common/components/TextInput.d.ts +12 -0
  41. package/dist/common/components/TextInput.js +239 -0
  42. package/dist/common/components/index.d.ts +3 -0
  43. package/dist/common/components/index.js +3 -0
  44. package/dist/common/hooks/index.d.ts +4 -0
  45. package/dist/common/hooks/index.js +4 -0
  46. package/dist/common/hooks/useCommandHistory.d.ts +7 -0
  47. package/dist/common/hooks/useCommandHistory.js +51 -0
  48. package/dist/common/hooks/useCtrlC.d.ts +9 -0
  49. package/dist/common/hooks/useCtrlC.js +40 -0
  50. package/dist/common/hooks/useKittyKeyboard.d.ts +10 -0
  51. package/dist/common/hooks/useKittyKeyboard.js +26 -0
  52. package/dist/common/hooks/useStaticOutputBuffer.d.ts +31 -0
  53. package/dist/common/hooks/useStaticOutputBuffer.js +58 -0
  54. package/dist/common/hooks/useTerminalResize.d.ts +28 -0
  55. package/dist/common/hooks/useTerminalResize.js +51 -0
  56. package/dist/common/hooks/useTextBuffer.d.ts +13 -0
  57. package/dist/common/hooks/useTextBuffer.js +165 -0
  58. package/dist/common/index.d.ts +13 -0
  59. package/dist/common/index.js +17 -0
  60. package/dist/common/types.d.ts +162 -0
  61. package/dist/common/types.js +1 -0
  62. package/dist/mcp/index.d.ts +12 -0
  63. package/dist/mcp/index.js +66 -0
  64. package/dist/mcp/server.d.ts +25 -0
  65. package/dist/mcp/server.js +837 -0
  66. package/dist/mcp/watcher.d.ts +86 -0
  67. package/dist/mcp/watcher.js +334 -0
  68. package/dist/rag/__tests__/grammar-smoke.test.d.ts +9 -0
  69. package/dist/rag/__tests__/grammar-smoke.test.js +161 -0
  70. package/dist/rag/__tests__/helpers.d.ts +30 -0
  71. package/dist/rag/__tests__/helpers.js +67 -0
  72. package/dist/rag/__tests__/merkle.test.d.ts +5 -0
  73. package/dist/rag/__tests__/merkle.test.js +161 -0
  74. package/dist/rag/__tests__/metadata-extraction.test.d.ts +10 -0
  75. package/dist/rag/__tests__/metadata-extraction.test.js +202 -0
  76. package/dist/rag/__tests__/multi-language.test.d.ts +13 -0
  77. package/dist/rag/__tests__/multi-language.test.js +535 -0
  78. package/dist/rag/__tests__/rag.test.d.ts +10 -0
  79. package/dist/rag/__tests__/rag.test.js +311 -0
  80. package/dist/rag/__tests__/search-exhaustive.test.d.ts +9 -0
  81. package/dist/rag/__tests__/search-exhaustive.test.js +87 -0
  82. package/dist/rag/__tests__/search-filters.test.d.ts +10 -0
  83. package/dist/rag/__tests__/search-filters.test.js +250 -0
  84. package/dist/rag/__tests__/search-modes.test.d.ts +8 -0
  85. package/dist/rag/__tests__/search-modes.test.js +133 -0
  86. package/dist/rag/config/index.d.ts +61 -0
  87. package/dist/rag/config/index.js +111 -0
  88. package/dist/rag/constants.d.ts +41 -0
  89. package/dist/rag/constants.js +57 -0
  90. package/dist/rag/embeddings/fastembed.d.ts +62 -0
  91. package/dist/rag/embeddings/fastembed.js +124 -0
  92. package/dist/rag/embeddings/gemini.d.ts +26 -0
  93. package/dist/rag/embeddings/gemini.js +116 -0
  94. package/dist/rag/embeddings/index.d.ts +10 -0
  95. package/dist/rag/embeddings/index.js +9 -0
  96. package/dist/rag/embeddings/local-4b.d.ts +28 -0
  97. package/dist/rag/embeddings/local-4b.js +51 -0
  98. package/dist/rag/embeddings/local.d.ts +29 -0
  99. package/dist/rag/embeddings/local.js +119 -0
  100. package/dist/rag/embeddings/mistral.d.ts +22 -0
  101. package/dist/rag/embeddings/mistral.js +85 -0
  102. package/dist/rag/embeddings/openai.d.ts +22 -0
  103. package/dist/rag/embeddings/openai.js +85 -0
  104. package/dist/rag/embeddings/types.d.ts +37 -0
  105. package/dist/rag/embeddings/types.js +1 -0
  106. package/dist/rag/gitignore/index.d.ts +57 -0
  107. package/dist/rag/gitignore/index.js +178 -0
  108. package/dist/rag/index.d.ts +15 -0
  109. package/dist/rag/index.js +25 -0
  110. package/dist/rag/indexer/chunker.d.ts +129 -0
  111. package/dist/rag/indexer/chunker.js +1352 -0
  112. package/dist/rag/indexer/index.d.ts +6 -0
  113. package/dist/rag/indexer/index.js +6 -0
  114. package/dist/rag/indexer/indexer.d.ts +73 -0
  115. package/dist/rag/indexer/indexer.js +356 -0
  116. package/dist/rag/indexer/types.d.ts +68 -0
  117. package/dist/rag/indexer/types.js +47 -0
  118. package/dist/rag/logger/index.d.ts +20 -0
  119. package/dist/rag/logger/index.js +75 -0
  120. package/dist/rag/manifest/index.d.ts +50 -0
  121. package/dist/rag/manifest/index.js +97 -0
  122. package/dist/rag/merkle/diff.d.ts +26 -0
  123. package/dist/rag/merkle/diff.js +95 -0
  124. package/dist/rag/merkle/hash.d.ts +34 -0
  125. package/dist/rag/merkle/hash.js +165 -0
  126. package/dist/rag/merkle/index.d.ts +68 -0
  127. package/dist/rag/merkle/index.js +298 -0
  128. package/dist/rag/merkle/node.d.ts +51 -0
  129. package/dist/rag/merkle/node.js +69 -0
  130. package/dist/rag/search/filters.d.ts +21 -0
  131. package/dist/rag/search/filters.js +100 -0
  132. package/dist/rag/search/fts.d.ts +32 -0
  133. package/dist/rag/search/fts.js +61 -0
  134. package/dist/rag/search/hybrid.d.ts +17 -0
  135. package/dist/rag/search/hybrid.js +58 -0
  136. package/dist/rag/search/index.d.ts +89 -0
  137. package/dist/rag/search/index.js +367 -0
  138. package/dist/rag/search/types.d.ts +130 -0
  139. package/dist/rag/search/types.js +4 -0
  140. package/dist/rag/search/vector.d.ts +25 -0
  141. package/dist/rag/search/vector.js +44 -0
  142. package/dist/rag/storage/index.d.ts +92 -0
  143. package/dist/rag/storage/index.js +287 -0
  144. package/dist/rag/storage/lancedb-native.d.ts +7 -0
  145. package/dist/rag/storage/lancedb-native.js +10 -0
  146. package/dist/rag/storage/schema.d.ts +23 -0
  147. package/dist/rag/storage/schema.js +50 -0
  148. package/dist/rag/storage/types.d.ts +100 -0
  149. package/dist/rag/storage/types.js +68 -0
  150. package/package.json +67 -0
  151. 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
+ }