codesearch-mcp 0.2.3-alpha → 0.4.0-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (317) hide show
  1. package/CHANGELOG.md +88 -5
  2. package/LICENSE +661 -21
  3. package/README.md +76 -40
  4. package/dist/app.js +17 -38
  5. package/dist/src/ast/CallExtractor.d.ts +8 -0
  6. package/dist/src/ast/CallExtractor.js +68 -0
  7. package/dist/src/ast/GraphqlNameExtractor.d.ts +22 -0
  8. package/dist/src/ast/GraphqlNameExtractor.js +240 -0
  9. package/dist/src/ast/ImportEdgeExtractor.d.ts +11 -0
  10. package/dist/src/ast/ImportEdgeExtractor.js +129 -0
  11. package/dist/src/ast/IndexedNodeResolver.d.ts +14 -0
  12. package/dist/src/ast/IndexedNodeResolver.js +217 -0
  13. package/dist/src/ast/MemberExtractor.d.ts +8 -0
  14. package/dist/src/ast/MemberExtractor.js +129 -0
  15. package/dist/src/ast/TopLevelSymbolExtractor.d.ts +5 -0
  16. package/dist/src/ast/TopLevelSymbolExtractor.js +86 -0
  17. package/dist/src/ast/graphql-grammar.d.ts +24 -0
  18. package/dist/src/ast/graphql-grammar.js +93 -0
  19. package/dist/src/ast/node-helpers.d.ts +11 -0
  20. package/dist/src/ast/node-helpers.js +55 -0
  21. package/dist/src/chunking/AstChunker.d.ts +12 -0
  22. package/dist/src/chunking/AstChunker.js +30 -0
  23. package/dist/src/chunking/CharacterChunkStrategy.d.ts +8 -0
  24. package/dist/src/chunking/CharacterChunkStrategy.js +15 -0
  25. package/dist/src/chunking/DeclarationChunkStrategy.d.ts +15 -0
  26. package/dist/src/chunking/DeclarationChunkStrategy.js +87 -0
  27. package/dist/src/chunking/FragmentMerger.d.ts +11 -0
  28. package/dist/src/chunking/FragmentMerger.js +61 -0
  29. package/dist/src/chunking/ImportOnlyFilter.d.ts +5 -0
  30. package/dist/src/chunking/ImportOnlyFilter.js +14 -0
  31. package/dist/src/chunking/LangchainTextSplitter.d.ts +21 -0
  32. package/dist/src/chunking/LangchainTextSplitter.js +133 -0
  33. package/dist/src/chunking/MinSizeSplitPolicy.d.ts +9 -0
  34. package/dist/src/chunking/MinSizeSplitPolicy.js +18 -0
  35. package/dist/src/chunking/RunPacker.d.ts +12 -0
  36. package/dist/src/chunking/RunPacker.js +84 -0
  37. package/dist/src/chunking/create-chunker.d.ts +3 -0
  38. package/dist/src/chunking/create-chunker.js +15 -0
  39. package/dist/src/chunking/interfaces.d.ts +27 -0
  40. package/dist/src/commands/ConfigCommand.js +1 -9
  41. package/dist/src/commands/EvalCommand.d.ts +10 -5
  42. package/dist/src/commands/EvalCommand.js +60 -149
  43. package/dist/src/commands/IndexCommand.d.ts +10 -4
  44. package/dist/src/commands/IndexCommand.js +52 -146
  45. package/dist/src/commands/McpCommand.d.ts +6 -3
  46. package/dist/src/commands/McpCommand.js +19 -20
  47. package/dist/src/commands/WebCommand.d.ts +9 -3
  48. package/dist/src/commands/WebCommand.js +17 -11
  49. package/dist/src/commands/eval/baseline-comparison.d.ts +6 -0
  50. package/dist/src/commands/eval/baseline-comparison.js +54 -0
  51. package/dist/src/commands/eval/eval-args.d.ts +6 -0
  52. package/dist/src/commands/eval/eval-args.js +71 -0
  53. package/dist/src/commands/eval/eval-corpora.d.ts +6 -0
  54. package/dist/src/commands/eval/eval-corpora.js +76 -0
  55. package/dist/src/commands/eval/eval-metadata.d.ts +17 -0
  56. package/dist/src/commands/eval/eval-metadata.js +75 -0
  57. package/dist/src/commands/eval/index-inspector.d.ts +3 -0
  58. package/dist/src/commands/eval/index-inspector.js +21 -0
  59. package/dist/src/commands/eval/staged-search.d.ts +3 -0
  60. package/dist/src/commands/eval/staged-search.js +20 -0
  61. package/dist/src/commands/eval/suite-loader.d.ts +8 -0
  62. package/dist/src/commands/eval/suite-loader.js +52 -0
  63. package/dist/src/commands/index/backend-probe.d.ts +2 -0
  64. package/dist/src/commands/index/backend-probe.js +15 -0
  65. package/dist/src/commands/index/embedding-setup.d.ts +3 -0
  66. package/dist/src/commands/index/embedding-setup.js +16 -0
  67. package/dist/src/commands/index/index-summary.d.ts +2 -0
  68. package/dist/src/commands/index/index-summary.js +22 -0
  69. package/dist/src/commands/index/legacy-ollama-guard.d.ts +3 -0
  70. package/dist/src/commands/index/legacy-ollama-guard.js +23 -0
  71. package/dist/src/commands/index/prune-step.d.ts +3 -0
  72. package/dist/src/commands/index/prune-step.js +22 -0
  73. package/dist/src/commands/index/scan-targets.d.ts +8 -0
  74. package/dist/src/commands/index/scan-targets.js +107 -0
  75. package/dist/src/config-error.d.ts +0 -4
  76. package/dist/src/config-error.js +0 -4
  77. package/dist/src/config-store.d.ts +1 -55
  78. package/dist/src/config-store.js +11 -73
  79. package/dist/src/config.d.ts +9 -18
  80. package/dist/src/config.js +36 -35
  81. package/dist/src/constants.d.ts +17 -28
  82. package/dist/src/constants.js +35 -29
  83. package/dist/src/container.d.ts +10 -2
  84. package/dist/src/container.js +19 -8
  85. package/dist/src/db/drizzle-types.d.ts +7 -0
  86. package/dist/src/db/drizzle-types.js +2 -0
  87. package/dist/src/db/index.d.ts +1 -0
  88. package/dist/src/db/schema-steps.d.ts +8 -0
  89. package/dist/src/db/schema-steps.js +105 -0
  90. package/dist/src/db/schema.d.ts +17 -0
  91. package/dist/src/db/schema.js +1 -6
  92. package/dist/src/declarations/DeclarationTree.d.ts +19 -0
  93. package/dist/src/declarations/DeclarationTree.js +66 -0
  94. package/dist/src/declarations/DeclarationTreeBuilder.d.ts +22 -0
  95. package/dist/src/declarations/DeclarationTreeBuilder.js +179 -0
  96. package/dist/src/declarations/SourceLines.d.ts +37 -0
  97. package/dist/src/declarations/SourceLines.js +415 -0
  98. package/dist/src/dependency-traversal.d.ts +0 -6
  99. package/dist/src/dependency-traversal.js +0 -6
  100. package/dist/src/enriched-prompt.d.ts +0 -6
  101. package/dist/src/enriched-prompt.js +0 -6
  102. package/dist/src/error-record.d.ts +12 -0
  103. package/dist/src/error-record.js +248 -0
  104. package/dist/src/eval-corpus.d.ts +16 -0
  105. package/dist/src/eval-corpus.js +169 -0
  106. package/dist/src/eval-results.d.ts +6 -3
  107. package/dist/src/eval-results.js +323 -26
  108. package/dist/src/eval-vector-index.d.ts +5 -0
  109. package/dist/src/eval-vector-index.js +60 -0
  110. package/dist/src/file-utils.d.ts +0 -5
  111. package/dist/src/file-utils.js +0 -5
  112. package/dist/src/graph.d.ts +0 -25
  113. package/dist/src/graph.js +0 -28
  114. package/dist/src/help.js +9 -1
  115. package/dist/src/hnsw-settings.d.ts +3 -0
  116. package/dist/src/hnsw-settings.js +24 -0
  117. package/dist/src/index-args.d.ts +13 -0
  118. package/dist/src/index-args.js +48 -0
  119. package/dist/src/indexing/AnalysisBatcher.d.ts +12 -0
  120. package/dist/src/indexing/AnalysisBatcher.js +28 -0
  121. package/dist/src/indexing/ChunkEmbedder.d.ts +24 -0
  122. package/dist/src/indexing/ChunkEmbedder.js +37 -0
  123. package/dist/src/indexing/ConsoleScanReporter.d.ts +10 -0
  124. package/dist/src/indexing/ConsoleScanReporter.js +73 -0
  125. package/dist/src/indexing/EmbeddingSchemaPreparer.d.ts +14 -0
  126. package/dist/src/indexing/EmbeddingSchemaPreparer.js +66 -0
  127. package/dist/src/indexing/FileIngestor.d.ts +22 -0
  128. package/dist/src/indexing/FileIngestor.js +116 -0
  129. package/dist/src/indexing/IndexStore.d.ts +24 -0
  130. package/dist/src/indexing/IndexStore.js +69 -0
  131. package/dist/src/indexing/StaleFilePruner.d.ts +8 -0
  132. package/dist/src/indexing/StaleFilePruner.js +55 -0
  133. package/dist/src/indexing/analysis-rows.d.ts +4 -0
  134. package/dist/src/indexing/analysis-rows.js +65 -0
  135. package/dist/src/indexing/project-notices.d.ts +2 -0
  136. package/dist/src/indexing/project-notices.js +63 -0
  137. package/dist/src/interfaces/IAstParser.d.ts +4 -40
  138. package/dist/src/interfaces/ICallGraph.d.ts +28 -0
  139. package/dist/src/interfaces/ICallGraph.js +2 -0
  140. package/dist/src/interfaces/IChunkSearch.d.ts +42 -0
  141. package/dist/src/interfaces/IChunkSearch.js +2 -0
  142. package/dist/src/interfaces/IChunker.d.ts +3 -12
  143. package/dist/src/interfaces/ICorpusInspector.d.ts +7 -0
  144. package/dist/src/interfaces/ICorpusInspector.js +2 -0
  145. package/dist/src/interfaces/IDatabase.d.ts +8 -24
  146. package/dist/src/interfaces/IDependencyGraph.d.ts +24 -0
  147. package/dist/src/interfaces/IDependencyGraph.js +2 -0
  148. package/dist/src/interfaces/IEmbedder.d.ts +0 -9
  149. package/dist/src/interfaces/IIndexIdentityGuard.d.ts +3 -0
  150. package/dist/src/interfaces/IIndexIdentityGuard.js +2 -0
  151. package/dist/src/interfaces/IIndexedNodeResolver.d.ts +10 -0
  152. package/dist/src/interfaces/IIndexedNodeResolver.js +2 -0
  153. package/dist/src/interfaces/ILogger.d.ts +1 -11
  154. package/dist/src/interfaces/IMcpReadiness.d.ts +12 -0
  155. package/dist/src/interfaces/IMcpReadiness.js +2 -0
  156. package/dist/src/interfaces/IMcpToolHandler.d.ts +0 -5
  157. package/dist/src/interfaces/IMonorepoResolver.d.ts +0 -22
  158. package/dist/src/interfaces/IProjectCatalog.d.ts +13 -0
  159. package/dist/src/interfaces/IProjectCatalog.js +2 -0
  160. package/dist/src/interfaces/IProjectDiscovery.d.ts +0 -17
  161. package/dist/src/interfaces/IProjectRoots.d.ts +3 -0
  162. package/dist/src/interfaces/IProjectRoots.js +2 -0
  163. package/dist/src/interfaces/IRetrievalEvaluator.d.ts +153 -12
  164. package/dist/src/interfaces/IScanReporter.d.ts +13 -0
  165. package/dist/src/interfaces/IScanReporter.js +2 -0
  166. package/dist/src/interfaces/ISymbolLookup.d.ts +49 -0
  167. package/dist/src/interfaces/ISymbolLookup.js +2 -0
  168. package/dist/src/interfaces/IToolPaths.d.ts +62 -0
  169. package/dist/src/interfaces/IToolPaths.js +2 -0
  170. package/dist/src/interfaces/IWorkspaceFiles.d.ts +28 -0
  171. package/dist/src/interfaces/IWorkspaceFiles.js +2 -0
  172. package/dist/src/interfaces/SymbolKind.d.ts +1 -0
  173. package/dist/src/interfaces/SymbolKind.js +2 -0
  174. package/dist/src/interfaces/index.d.ts +12 -1
  175. package/dist/src/interrupt.js +0 -2
  176. package/dist/src/owner-path.d.ts +9 -10
  177. package/dist/src/owner-path.js +11 -23
  178. package/dist/src/project-scope.d.ts +1 -12
  179. package/dist/src/project-scope.js +5 -12
  180. package/dist/src/project-selector.d.ts +0 -5
  181. package/dist/src/project-selector.js +0 -11
  182. package/dist/src/retrieval-eval.d.ts +15 -6
  183. package/dist/src/retrieval-eval.js +388 -59
  184. package/dist/src/search/CallGraph.d.ts +9 -0
  185. package/dist/src/search/CallGraph.js +84 -0
  186. package/dist/src/search/ChunkSearch.d.ts +23 -0
  187. package/dist/src/search/ChunkSearch.js +115 -0
  188. package/dist/src/search/DependencyGraph.d.ts +19 -0
  189. package/dist/src/search/DependencyGraph.js +180 -0
  190. package/dist/src/search/IndexIdentityGuard.d.ts +11 -0
  191. package/dist/src/search/IndexIdentityGuard.js +37 -0
  192. package/dist/src/search/ProjectCatalog.d.ts +11 -0
  193. package/dist/src/search/ProjectCatalog.js +38 -0
  194. package/dist/src/search/SymbolLookup.d.ts +24 -0
  195. package/dist/src/search/SymbolLookup.js +97 -0
  196. package/dist/src/search/chunks/CandidateFetcher.d.ts +19 -0
  197. package/dist/src/search/chunks/CandidateFetcher.js +40 -0
  198. package/dist/src/search/chunks/IterativeScanGate.d.ts +10 -0
  199. package/dist/src/search/chunks/IterativeScanGate.js +42 -0
  200. package/dist/src/search/chunks/RerankStage.d.ts +15 -0
  201. package/dist/src/search/chunks/RerankStage.js +42 -0
  202. package/dist/src/search/chunks/search-metrics.d.ts +18 -0
  203. package/dist/src/search/chunks/search-metrics.js +32 -0
  204. package/dist/src/search/chunks/search-plan.d.ts +18 -0
  205. package/dist/src/search/chunks/search-plan.js +19 -0
  206. package/dist/src/search/create-search-services.d.ts +26 -0
  207. package/dist/src/search/create-search-services.js +29 -0
  208. package/dist/src/search/index-paths.d.ts +1 -0
  209. package/dist/src/search/index-paths.js +5 -0
  210. package/dist/src/search/result-shaping.d.ts +4 -0
  211. package/dist/src/search/result-shaping.js +19 -0
  212. package/dist/src/search/symbol-sql.d.ts +13 -0
  213. package/dist/src/search/symbol-sql.js +66 -0
  214. package/dist/src/search/symbols/SymbolGrepFallback.d.ts +19 -0
  215. package/dist/src/search/symbols/SymbolGrepFallback.js +106 -0
  216. package/dist/src/search/symbols/SymbolIndexQuery.d.ts +14 -0
  217. package/dist/src/search/symbols/SymbolIndexQuery.js +52 -0
  218. package/dist/src/search/symbols/SymbolTextSearch.d.ts +13 -0
  219. package/dist/src/search/symbols/SymbolTextSearch.js +84 -0
  220. package/dist/src/search/symbols/TargetColumnGate.d.ts +7 -0
  221. package/dist/src/search/symbols/TargetColumnGate.js +23 -0
  222. package/dist/src/search/symbols/find-symbol-constants.d.ts +4 -0
  223. package/dist/src/search/symbols/find-symbol-constants.js +8 -0
  224. package/dist/src/search/symbols/grep-args.d.ts +3 -0
  225. package/dist/src/search/symbols/grep-args.js +7 -0
  226. package/dist/src/search/symbols/grep-patterns.d.ts +4 -0
  227. package/dist/src/search/symbols/grep-patterns.js +60 -0
  228. package/dist/src/services/AstParserService.d.ts +10 -106
  229. package/dist/src/services/AstParserService.js +37 -1068
  230. package/dist/src/services/DatabaseService.d.ts +10 -17
  231. package/dist/src/services/DatabaseService.js +71 -115
  232. package/dist/src/services/EmbedderFactory.d.ts +0 -4
  233. package/dist/src/services/EmbedderFactory.js +0 -5
  234. package/dist/src/services/IndexerService.d.ts +7 -26
  235. package/dist/src/services/IndexerService.js +38 -286
  236. package/dist/src/services/InfinityEmbedder.d.ts +0 -21
  237. package/dist/src/services/InfinityEmbedder.js +0 -15
  238. package/dist/src/services/LoggerService.d.ts +7 -5
  239. package/dist/src/services/LoggerService.js +37 -41
  240. package/dist/src/services/McpReadiness.d.ts +18 -0
  241. package/dist/src/services/McpReadiness.js +73 -0
  242. package/dist/src/services/McpServer.d.ts +50 -19
  243. package/dist/src/services/McpServer.js +132 -62
  244. package/dist/src/services/MonorepoAwareDiscoveryService.d.ts +1 -45
  245. package/dist/src/services/MonorepoAwareDiscoveryService.js +1 -51
  246. package/dist/src/services/NxMonorepoResolver.d.ts +0 -29
  247. package/dist/src/services/NxMonorepoResolver.js +5 -56
  248. package/dist/src/services/OpenAICompatibleEmbedder.d.ts +0 -6
  249. package/dist/src/services/OpenAICompatibleEmbedder.js +0 -14
  250. package/dist/src/services/ProjectDiscoveryService.d.ts +0 -4
  251. package/dist/src/services/ProjectDiscoveryService.js +0 -4
  252. package/dist/src/services/VoyageEmbedder.d.ts +0 -6
  253. package/dist/src/services/VoyageEmbedder.js +0 -6
  254. package/dist/src/services/WebServerService.d.ts +5 -56
  255. package/dist/src/services/WebServerService.js +25 -63
  256. package/dist/src/services/http-retry.d.ts +0 -7
  257. package/dist/src/services/http-retry.js +1 -10
  258. package/dist/src/services/infinity-provisioning.d.ts +0 -74
  259. package/dist/src/services/infinity-provisioning.js +5 -88
  260. package/dist/src/services/legacy-ollama-index.d.ts +0 -16
  261. package/dist/src/services/legacy-ollama-index.js +0 -23
  262. package/dist/src/services/reranker.d.ts +0 -9
  263. package/dist/src/services/reranker.js +46 -12
  264. package/dist/src/setup.d.ts +24 -24
  265. package/dist/src/setup.js +205 -103
  266. package/dist/src/symbol-name.d.ts +1 -12
  267. package/dist/src/symbol-name.js +16 -25
  268. package/dist/src/symbol-rows.d.ts +0 -14
  269. package/dist/src/symbol-rows.js +0 -14
  270. package/dist/src/tool-call-log.d.ts +2 -0
  271. package/dist/src/tool-call-log.js +24 -0
  272. package/dist/src/tools/FindSymbolTool.d.ts +17 -7
  273. package/dist/src/tools/FindSymbolTool.js +54 -35
  274. package/dist/src/tools/GetCallersTool.d.ts +16 -8
  275. package/dist/src/tools/GetCallersTool.js +65 -35
  276. package/dist/src/tools/GetDependenciesTool.d.ts +16 -8
  277. package/dist/src/tools/GetDependenciesTool.js +36 -29
  278. package/dist/src/tools/GetDependentsTool.d.ts +20 -8
  279. package/dist/src/tools/GetDependentsTool.js +48 -34
  280. package/dist/src/tools/ListFilesTool.d.ts +16 -8
  281. package/dist/src/tools/ListFilesTool.js +20 -26
  282. package/dist/src/tools/ListProjectsTool.d.ts +11 -7
  283. package/dist/src/tools/ListProjectsTool.js +13 -11
  284. package/dist/src/tools/McpTool.d.ts +17 -0
  285. package/dist/src/tools/McpTool.js +28 -0
  286. package/dist/src/tools/ReadFileRangeTool.d.ts +16 -8
  287. package/dist/src/tools/ReadFileRangeTool.js +23 -25
  288. package/dist/src/tools/ReadFileTool.d.ts +15 -8
  289. package/dist/src/tools/ReadFileTool.js +19 -24
  290. package/dist/src/tools/SearchCodebaseTool.d.ts +22 -10
  291. package/dist/src/tools/SearchCodebaseTool.js +71 -50
  292. package/dist/src/tools/format-helpers.d.ts +9 -9
  293. package/dist/src/tools/format-helpers.js +146 -28
  294. package/dist/src/tools/tool-args.d.ts +38 -0
  295. package/dist/src/tools/tool-args.js +95 -0
  296. package/dist/src/web/dashboard.d.ts +0 -8
  297. package/dist/src/web/dashboard.js +7 -10
  298. package/dist/src/workspace/GrepRunner.d.ts +15 -0
  299. package/dist/src/workspace/GrepRunner.js +104 -0
  300. package/dist/src/workspace/ProjectRoots.d.ts +22 -0
  301. package/dist/src/workspace/ProjectRoots.js +102 -0
  302. package/dist/src/workspace/ToolPaths.d.ts +31 -0
  303. package/dist/src/workspace/ToolPaths.js +312 -0
  304. package/dist/src/workspace/WorkspaceFiles.d.ts +19 -0
  305. package/dist/src/workspace/WorkspaceFiles.js +285 -0
  306. package/dist/src/workspace/fs-access.d.ts +9 -0
  307. package/dist/src/workspace/fs-access.js +47 -0
  308. package/dist/src/workspace/path-helpers.d.ts +3 -0
  309. package/dist/src/workspace/path-helpers.js +43 -0
  310. package/package.json +5 -4
  311. package/dist/src/interfaces/ISearcher.d.ts +0 -35
  312. package/dist/src/services/AstChunker.d.ts +0 -42
  313. package/dist/src/services/AstChunker.js +0 -158
  314. package/dist/src/services/SearcherService.d.ts +0 -342
  315. package/dist/src/services/SearcherService.js +0 -1199
  316. package/docs/web-dashboard.md +0 -206
  317. /package/dist/src/{interfaces/ISearcher.js → chunking/interfaces.js} +0 -0
package/README.md CHANGED
@@ -7,7 +7,7 @@ Ask your AI agent things like *"where is the payment retry logic?"* or *"show me
7
7
  **Why codesearch-mcp:**
8
8
 
9
9
  - **Dramatically fewer tokens** — without RAG, answering a cross-project question means discovery calls, directory listings, and reading several wrong files before finding the right one; a workspace of 10 projects can easily mean 50 000+ lines to sift through. With RAG, one `search_codebase` call returns 5 targeted chunks (~200–500 lines total) from across *all* projects at once — the model gets the right code immediately, with no exploration overhead
10
- - **Reranker narrows the result further** — a cross-encoder reranker (enabled by default) re-scores the candidate pool so the top results are the most relevant, not just the closest vector; fewer results means less context consumed per answer
10
+ - **Reranker narrows the result further** — a cross-encoder reranker (on by default with the local Infinity backend; local or Voyage AI) re-scores the candidate pool so the top results are the most relevant, not just the closest vector; fewer results means less context consumed per answer
11
11
  - **Semantic, not grep** — embeddings capture intent; queries in plain English or pseudocode find the relevant code regardless of naming
12
12
  - **Local and private** — default setup runs entirely on your machine (Infinity embeddings + PostgreSQL); no code leaves your network
13
13
  - **Code-structure aware** — chunks align to function and class boundaries; each result shows which declaration it belongs to (`InvoiceService.create`, `collectFiles`, …), so the model understands context without reading the surrounding file
@@ -30,8 +30,8 @@ See `CHANGELOG.md` for version history.
30
30
 
31
31
  ## Requirements
32
32
 
33
- - Node.js 18+
34
- - PostgreSQL 15+ with pgvector (the symbol index's unique index relies on `NULLS NOT DISTINCT`, added in PostgreSQL 15)
33
+ - Node.js `^22.13.0` or `>=23.5.0`
34
+ - PostgreSQL 15+ with pgvector (the symbol index's unique index relies on `NULLS NOT DISTINCT`, added in PostgreSQL 15); pgvector ≥ 0.8 recommended (filtered search on large projects; older versions work with reduced recall)
35
35
  - Docker (for local DB + Infinity) — or bring your own PostgreSQL with pgvector + an embedding backend (see [Embedding providers](#embedding-providers))
36
36
 
37
37
  ## Installation
@@ -48,7 +48,8 @@ rag-setup
48
48
 
49
49
  Walks you through:
50
50
  - **Database** — enter connection details; optionally starts Docker containers automatically
51
- - **Embedding backend** — pick a provider (`infinity` / `openai-compatible` / `voyage`), its model, and an API key if the provider needs one (see [Embedding providers](#embedding-providers))
51
+ - **Embedding backend** — pick a provider (`infinity` / `openai-compatible` / `voyage`), its model, and an API key if the provider needs one (see [Embedding providers](#embedding-providers)), then a reranker
52
+ - **`voyage`** is a short path: the API key comes first and is required (stored once in `.env` as `VOYAGE_API_KEY`); no base URL is asked. The embedding model is picked from a numbered list (`voyage-code-4` default, `voyage-4-large`, `voyage-4`, `voyage-4-lite`) — enter a number, press Enter for the default, or type any model name. Then `Enable Voyage reranker? (y/n) (y)`: `y` picks `rerank-3` (default) or `rerank-3-lite` the same way and reuses the same key (`reranker.apiKey: env:VOYAGE_API_KEY`); `n` turns reranking off. Other rerankers for a Voyage embedder (an `openai-compatible` `/rerank` server, a separate Voyage key) are set with `rag-config` — see [Reranker with a cloud embedder](#reranker-with-a-cloud-or-external-embedder)
52
53
  - **Logging** — choose log level and directory (defaults: `error` level, `~/.config/codesearch-mcp/logs/`)
53
54
 
54
55
  Config is saved to `~/.config/codesearch-mcp/config.json`.
@@ -76,15 +77,28 @@ rag-index --select --reindex
76
77
  rag-index --project billing-service # scope this run to one project, by exact name
77
78
  rag-index --reindex --project billing-service # force a full reindex of just that project
78
79
  rag-index --project # no name — pick one from an interactive prompt
80
+ rag-index --project billing-service --require-cwd # refuse the run unless the current directory is that project's directory
79
81
  ```
80
82
 
81
83
  `--project` leaves every other indexed project untouched: it skips the `.ragprojects`/`.ragskip` selector entirely and never prunes projects outside the one(s) it resolves to. A name that doesn't match any discovered project or monorepo unit exits with an error before touching the database — matching exactly, no partial or fuzzy match.
82
84
 
85
+ `--require-cwd` is for scripts that must not read another checkout's index by accident: run from a linked worktree, a second clone, or a subdirectory of the project, `rag-index` prints both directories and exits 1 before any database or embedding work. It needs `--project <name>`; without a name it is a usage error, never a prompt. Directories are compared by real path, against the unit's directory when a unit name matched, else the leaf project's, and a relative `workspaceRoot` is refused. In a root-level Nx workspace with no discovered project at its root, no `--project` name matches that root, so `--require-cwd` refuses every run from there.
86
+
87
+ ### Exit codes
88
+
89
+ | Code | Meaning |
90
+ |------|---------|
91
+ | `0` | No file failed and nothing else failed. This includes an interrupted run, which may have left files unindexed. |
92
+ | `2` | The run finished with failed files and the rest of the index is usable. `Indexing finished with <n> of <total> file(s) failed.` is on stderr, and the next run retries those files. |
93
+ | `1` | Any other failure, including failed files in a run that was interrupted, skipped no file and stored no chunks, failed to prune, or ended with the database or the embedding backend unavailable (`Indexing backend unavailable: <cause>`). |
94
+
95
+ A failed file does not stop the run: the summary is still printed, and unless the run is interrupted the remaining files and projects are still indexed and the project graph is still produced.
96
+
83
97
  ## Monorepo support
84
98
 
85
99
  If `workspaceRoot` is an [Nx](https://nx.dev) workspace (detected by `nx.json`), `rag-index` discovers each project under `apps/`, `libs/`, and `packages/` as a **separate indexed project** rather than scanning the workspace as one blob. Cross-project imports like `@scope/shared` resolve through the workspace `tsconfig.base.json` path aliases, so `get_dependencies` / `get_dependents` report edges **across** projects.
86
100
 
87
- **Nested Nx repos.** `workspaceRoot` may also be a plain directory that *contains* an Nx repo in one of its immediate subdirectories (alongside ordinary projects). That nested repo is detected and expanded into its own projects, named `<repo>/<project>` (e.g. `micros-learning/account`), each resolved with the repo's own `tsconfig.base.json`. In the project selector the whole Nx repo appears as **one** selectable entry (so `.ragprojects` stores just `micros-learning`); selecting it indexes all its sub-projects. Ordinary sibling directories are unaffected.
101
+ **Nested Nx repos.** `workspaceRoot` may also be a plain directory that *contains* an Nx repo in one of its immediate subdirectories (alongside ordinary projects). That nested repo is detected and expanded into its own projects, named `<repo>/<project>` (e.g. `mu-monorepo/account`), each resolved with the repo's own `tsconfig.base.json`. In the project selector the whole Nx repo appears as **one** selectable entry (so `.ragprojects` stores just `mu-monorepo`); selecting it indexes all its sub-projects. Ordinary sibling directories are unaffected.
88
102
 
89
103
  No configuration needed — detection is automatic. Plain (non-Nx) workspaces keep the original behavior: each immediate subdirectory with a `package.json` is one project.
90
104
 
@@ -106,7 +120,7 @@ Every file or directory argument is **workspace-relative** (relative to `workspa
106
120
 
107
121
  | Tool | Description |
108
122
  |------|-------------|
109
- | `search_codebase` | Semantic search across all indexed projects. Accepts `include_graph_context` to append direct imports/dependents to each result, `include_neighbors` for surrounding chunks, `threshold` to override the similarity threshold for that query, and `rerank` to force reranking on/off for that call regardless of `reranker.enabled` |
123
+ | `search_codebase` | Semantic search across all indexed projects. Accepts `include_graph_context` to append direct imports/dependents to each result, `include_neighbors` for surrounding chunks, `threshold` to override the similarity threshold for that query, `rerank` to force reranking on/off for that call regardless of `reranker.enabled`, and `per_file` (default 2) to cap how many results come from one file |
110
124
  | `list_projects` | List projects with metadata, chunk counts and each project's directory (`path:` line) |
111
125
  | `find_symbol` | Find a declaration or member by exact name — bare, qualified (`Class.method`), quoted, or computed — faster than semantic search. Also finds a NestJS GraphQL operation, resolve field or type by its GraphQL name (`kind: graphql_query`/`graphql_mutation`/`graphql_subscription`/`graphql_field`/`graphql_type`) |
112
126
  | `list_files` | Browse the directory tree of any workspace directory |
@@ -126,7 +140,7 @@ Add `.ragmeta.json` to a project root to enrich `list_projects` and generate a v
126
140
  "stack": "NestJS, GraphQL, PostgreSQL",
127
141
  "notes": "Uses optimistic locking — see order-service/docs/concurrency.md",
128
142
  "relations": {
129
- "consumed_by": ["crm-frontend"],
143
+ "consumed_by": ["beta-frontend"],
130
144
  "consumes": ["billing-service", "auth-service"]
131
145
  }
132
146
  }
@@ -143,14 +157,14 @@ Pass `include_graph_context: true` to `search_codebase` to see direct imports an
143
157
  **Symbol:** InvoiceService.create
144
158
  **Line:** 45
145
159
  ...code...
146
- **Relevance:** 0.32
160
+ **Distance:** 0.32
147
161
  **Imports:** src/repositories/InvoiceRepo.ts · src/dto/CreateInvoiceDto.ts
148
162
  **Used by:** src/controllers/InvoiceController.ts
149
163
  ```
150
164
 
151
165
  Useful for immediately understanding a file's role in the dependency graph.
152
166
 
153
- Each result also shows a **Symbol** breadcrumb — the declaration the chunk belongs to (`InvoiceService.create`, `collectFiles`, …) — so you can see the enclosing class/method without opening the file. It is omitted for module-scope chunks and for projects indexed before this feature (re-index to populate).
167
+ Each result also shows a **Symbol** breadcrumb — the smallest declaration containing all of the chunk's declaration code (`InvoiceService.create`, `collectFiles`, …) — so you can see the enclosing class/method without opening the file. A chunk packing several small members shows their class. Module-scope code (top-level statements, enums, `declare module`) belongs to no declaration, so a chunk packing it with one small declaration shows that declaration; the breadcrumb is omitted for a chunk of module-scope code only, for chunks packing several small top-level declarations, and for projects indexed before this feature (re-index to populate).
154
168
 
155
169
  ## Excluding files
156
170
 
@@ -174,6 +188,12 @@ npx pino-pretty < ~/.config/codesearch-mcp/logs/indexer/current.log
174
188
  # Detailed indexing diagnostics (timing per file: deps / embed / db)
175
189
  rag-config set logging.level debug
176
190
 
191
+ # See what an agent called (needs logging.level info or debug), grouped by session and connection
192
+ jq -c 'select(.msg=="tool call") | {session, connectionId, callId, tool, args, isError, ms}' \
193
+ ~/.config/codesearch-mcp/logs/mcp/current.log
194
+ jq -s 'map(select(.msg=="tool call")) | group_by(.session, .connectionId) | map({session: .[0].session, connectionId: .[0].connectionId, calls: map(.tool)})' \
195
+ ~/.config/codesearch-mcp/logs/mcp/current.log
196
+
177
197
  # Disable file logging
178
198
  rag-config set logging.dir false
179
199
  ```
@@ -204,13 +224,13 @@ rag-config edit # open config in $EDITOR; validates on save
204
224
  | `embedding.model` | `jinaai/jina-embeddings-v2-base-code` | Embedding model |
205
225
  | `embedding.dimensions` | (derived) | Vector width override; required for models whose native width exceeds 2000 (Matryoshka). Otherwise derived from model → registry → runtime probe |
206
226
  | `embedding.batchSize` | `32` | Texts per batch embedding request |
207
- | `search.similarityThreshold` | `0.6` | Max cosine distance — raise if search returns nothing |
208
- | `reranker.enabled` | `true` | Enable cross-encoder reranking; requires `reranker.model` |
209
- | `reranker.provider` | `openai-compatible` | Rerank backend — `openai-compatible` `/rerank` only (Infinity, TEI, vLLM) |
210
- | `reranker.baseURL` | `embedding.baseURL` | Rerank server base URL (Infinity serves embed + rerank from one server) |
211
- | `reranker.model` | `mixedbread-ai/mxbai-rerank-xsmall-v1` | Reranker model name |
212
- | `reranker.overfetch` | `5` | Candidate multiplier before reranking (`limit × N`) |
213
- | `reranker.candidateFloor` | `80` | Minimum candidate pool fed to the reranker regardless of `limit` — `fetchLimit = min(max(limit × overfetch, candidateFloor), 200)`. Raise on GPU/fast hosts for better recall; lower on weak CPU to cut latency |
227
+ | `search.similarityThreshold` | per model | Max cosine distance, lower = stricter. Auto per model unless set: the recommended value of `embedding.model` (`0.95` for `voyage-code-4`, `voyage-4`, `voyage-4-large` and `voyage-4-lite`, `0.6` for `jinaai/jina-embeddings-v2-base-code`), `1.0` for a model with none. An explicit value more than `0.1` from the recommendation produces a warning in the MCP server instructions and the log, and on the eval console |
228
+ | `reranker.enabled` | context | Enable cross-encoder reranking. When absent: on with `infinity` embedding or when the `reranker` block names a server (`provider`/`baseURL`/`url`), off otherwise |
229
+ | `reranker.provider` | `openai-compatible` | Rerank backend — `openai-compatible` `/rerank` (Infinity, vLLM) or `voyage` (Voyage AI, needs `reranker.apiKey`) |
230
+ | `reranker.baseURL` | per-provider | Rerank server base URL — `embedding.baseURL` for `openai-compatible` (Infinity serves embed + rerank from one server), `https://api.voyageai.com/v1` for `voyage` |
231
+ | `reranker.model` | per-provider | Reranker model name — `mixedbread-ai/mxbai-rerank-xsmall-v1` for `openai-compatible`, `rerank-3` for `voyage` |
232
+ | `reranker.overfetch` | `5` | Candidate multiplier before reranking (`limit × N`); also sizes the pool of a search without reranking whose `per_file` is below `limit` |
233
+ | `reranker.candidateFloor` | `80` | Minimum candidate pool fed to the reranker regardless of `limit` — `fetchLimit = min(max(limit × overfetch, candidateFloor), 200)`. The same pool serves a search without reranking whose `per_file` is below `limit`. Raise on GPU/fast hosts for better recall; lower on weak CPU to cut latency |
214
234
  | `indexing.chunkSize` | `1500` | Max characters per chunk |
215
235
  | `indexing.chunkOverlap` | `200` | Overlap between chunks |
216
236
  | `indexing.embedBatchSize` | `5` | Parallel embedding requests per batch |
@@ -218,9 +238,8 @@ rag-config edit # open config in $EDITOR; validates on save
218
238
  | `indexing.chunkStrategy` | `ast` | How TS/JS files are chunked: `ast` = boundaries aligned to functions/classes, `character` = fixed-size splitter. Re-index to apply |
219
239
  | `search.defaultLimit` | `5` | Default result count returned by `search_codebase` |
220
240
  | `search.maxLimit` | `20` | Hard cap on result count |
221
- | `logging.level` | `error` | Log level written to file: `error` \| `warn` \| `info` \| `debug` |
241
+ | `logging.level` | `error` | Log level written to file: `error` \| `warn` \| `info` \| `debug`. `info` adds one `tool call` line per MCP tool call and one `search metrics` line per search |
222
242
  | `logging.dir` | `~/.config/codesearch-mcp/logs` | Base log directory, or `"false"` to disable |
223
- | `logging.searchMetrics` | `false` | Emit one structured search-tuning line per query (at `info`, independent of `logging.level`) — use it to size `candidateFloor`/`overfetch`/`threshold` for your hardware |
224
243
 
225
244
  ## Embedding providers
226
245
 
@@ -234,7 +253,7 @@ rag-config edit # open config in $EDITOR; validates on save
234
253
  | `openai-compatible` | OpenAI cloud, or any OpenAI-style server (TEI, vLLM, LM Studio, LocalAI, **or Ollama via its `/v1` endpoint**). | `baseURL` (+ `apiKey` for cloud) |
235
254
  | `voyage` | Voyage AI cloud, code-specialized embeddings. | Voyage `apiKey` |
236
255
 
237
- Easiest path: `rag-setup` asks for the provider, model, and any API key. Haven't run setup? It's the place to start. To change things later, edit config directly (below).
256
+ Easiest path: `rag-setup` asks for the provider, model, any API key, and a reranker. Haven't run setup? It's the place to start. To change things later, edit config directly (below).
238
257
 
239
258
  ### Switching provider or changing the model
240
259
 
@@ -246,14 +265,14 @@ rag-config set embedding.provider openai-compatible
246
265
  rag-config set embedding.baseURL https://api.openai.com/v1
247
266
  rag-config set embedding.model text-embedding-3-small
248
267
  rag-config set embedding.apiKey env:OPENAI_API_KEY # key lives in .env, not config.json
249
- rag-config set reranker.enabled false # OpenAI has no /rerank endpoint
268
+ rag-config set reranker.enabled false # only if rag-setup chose the `infinity` reranker: it stays aimed at the old server
250
269
  rag-index --reindex
251
270
 
252
271
  # Switch to Voyage AI
253
272
  rag-config set embedding.provider voyage
254
- rag-config set embedding.model voyage-code-3
273
+ rag-config set embedding.model voyage-code-4
255
274
  rag-config set embedding.apiKey env:VOYAGE_API_KEY # key lives in .env, not config.json
256
- rag-config set reranker.enabled false # Voyage has no /rerank endpoint
275
+ rag-config set reranker.enabled false # only if rag-setup chose the `infinity` reranker (or configure the Voyage reranker, below)
257
276
  rag-index --reindex
258
277
 
259
278
  # Change only the model on the current provider
@@ -272,9 +291,9 @@ rag-index --reindex
272
291
  Per-provider notes:
273
292
 
274
293
  - **infinity** — the model set is bound at container startup, so changing `embedding.model` (or the reranker model) **recreates** the managed container on the next `rag-setup`/`rag-index`/dashboard save. Defaults: `jinaai/jina-embeddings-v2-base-code` + reranker `mixedbread-ai/mxbai-rerank-xsmall-v1`. Both models are included in the compose template and reranking is **enabled by default** (`reranker.enabled: true`).
275
- - **openai-compatible** — point `embedding.baseURL` at the server; set `embedding.apiKey` (as `env:VAR`) for cloud, leave empty for local servers (TEI/vLLM/LM Studio/**Ollama**). E.g. `text-embedding-3-small`, `text-embedding-3-large`. Cloud endpoints (OpenAI, Azure) don't expose `/rerank` — disable the reranker or point it at a separate server (see [Reranker with a cloud embedder](#reranker-with-a-cloud-or-external-embedder)).
294
+ - **openai-compatible** — point `embedding.baseURL` at the server; set `embedding.apiKey` (as `env:VAR`) for cloud, leave empty for local servers (TEI/vLLM/LM Studio/**Ollama**). E.g. `text-embedding-3-small`, `text-embedding-3-large`. Cloud endpoints (OpenAI, Azure) don't expose `/rerank`, so without a `reranker` block reranking stays off unless you configure a separate rerank server; a block from `rag-setup`'s `infinity` reranker choice keeps reranking on at the old server until you set `reranker.enabled false` (see [Reranker with a cloud embedder](#reranker-with-a-cloud-or-external-embedder)).
276
295
  - **Ollama** (via `openai-compatible`) — set `embedding.baseURL` to `http://localhost:11434/v1` and `embedding.model` to any model you've pulled (e.g. `bge-m3`). No dedicated code path, no auto-pull — pull the model yourself with `ollama pull <model>` first.
277
- - **voyage** — set `embedding.apiKey` (as `env:VAR`); e.g. `voyage-code-3`, `voyage-3`. Voyage doesn't expose a `/rerank` endpoint in the OpenAI-compatible format — disable the reranker or point it at a separate server (see [Reranker with a cloud embedder](#reranker-with-a-cloud-or-external-embedder)).
296
+ - **voyage** — set `embedding.apiKey` (as `env:VAR`); e.g. `voyage-code-4` (default in `rag-setup`), `voyage-4-large`, `voyage-4`, `voyage-4-lite`. Older names such as `voyage-code-3` still work — their dimension is probed. Without a `reranker` block, reranking stays off unless you configure one (a block from `rag-setup`'s `infinity` reranker choice keeps it on at the old server until you set `reranker.enabled false`) — e.g. the Voyage reranker (see [Reranker with a cloud embedder](#reranker-with-a-cloud-or-external-embedder)).
278
297
 
279
298
  The vector **dimension** is derived automatically (model registry → one-shot probe); set `embedding.dimensions` only to force a Matryoshka-reduced width for models whose native size exceeds 2000.
280
299
 
@@ -291,23 +310,36 @@ The same "already running?" probe applies everywhere: a reachable backend defaul
291
310
 
292
311
  ### Reranker with a cloud or external embedder
293
312
 
294
- The reranker is **enabled by default** (`reranker.enabled: true`) and `reranker.baseURL` defaults to `embedding.baseURL`. Cloud embedding endpoints (OpenAI, Voyage, Azure) don't serve `/rerank` — the reranker call will fail silently and fall back to vector order on every query. Fix it one of two ways:
313
+ With a cloud embedder (OpenAI, Voyage, Azure) and no `reranker` block, reranking is **off** — those endpoints don't serve `/rerank`. `rag-setup` configures a reranker after the embedding step for every provider and always writes an explicit `reranker` block: a Voyage embedder gets one question, `Enable Voyage reranker?`, and shares its `VOYAGE_API_KEY`; other embedders choose `voyage` (own key, `VOYAGE_RERANK_API_KEY`), `openai-compatible` or `none`, plus `infinity` for an Infinity embedder. To configure one by hand:
295
314
 
296
- **Option A — disable reranking** (simplest, recommended for cloud-only setups):
315
+ These recipes edit whatever `reranker` block is already stored, so each one sets `reranker.enabled true` (an explicit `false` from `rag-setup`'s `none` choice, or the `n` answer to a Voyage embedder's `Enable Voyage reranker?`, would otherwise win) and clears the keys that would point the requests at the old server.
316
+
317
+ **Voyage reranker** (works with any embedder; with a Voyage embedder `reranker.apiKey` can point at the same `env:VOYAGE_API_KEY` instead of a separate key):
297
318
 
298
319
  ```bash
299
- rag-config set reranker.enabled false
320
+ echo 'VOYAGE_RERANK_API_KEY=pa-…' >> ~/.config/codesearch-mcp/.env
321
+ rag-config set reranker.enabled true
322
+ rag-config set reranker.provider voyage
323
+ rag-config set reranker.model rerank-3 # or rerank-3-lite
324
+ rag-config set reranker.apiKey env:VOYAGE_RERANK_API_KEY
325
+ rag-config reset reranker.baseURL # a stored Infinity baseURL would win over the Voyage default
326
+ rag-config reset reranker.url
300
327
  ```
301
328
 
302
- **Option B — point the reranker at a separate server** (e.g. a local Infinity or TEI instance while using Voyage/OpenAI for embeddings):
329
+ Without `reranker.apiKey` a Voyage reranker stays disabled and a warning is written to the log — visible only at `logging.level` `warn` or lower (the default `error` hides it).
330
+
331
+ **A separate `/rerank` server** (e.g. a local Infinity or vLLM instance while using Voyage/OpenAI for embeddings):
303
332
 
304
333
  ```bash
305
- rag-config set reranker.baseURL http://localhost:7997
306
- rag-config set reranker.model mixedbread-ai/mxbai-rerank-xsmall-v1
307
- # reranker.enabled is already true
334
+ rag-config set reranker.enabled true
335
+ rag-config set reranker.provider openai-compatible # a stored voyage provider would send the Voyage wire format
336
+ rag-config set reranker.baseURL http://localhost:7997
337
+ rag-config set reranker.model mixedbread-ai/mxbai-rerank-xsmall-v1
338
+ rag-config reset reranker.url
339
+ rag-config reset reranker.apiKey # unless the server needs one
308
340
  ```
309
341
 
310
- The embedding and reranking servers are fully independent — any combination works: Voyage embeddings + local Infinity reranker, OpenAI embeddings + remote TEI reranker, etc.
342
+ The embedding and reranking servers are fully independent — any combination works: Voyage embeddings + local Infinity reranker, OpenAI embeddings + remote vLLM reranker, etc.
311
343
 
312
344
  ### Secrets & external embedders
313
345
 
@@ -320,18 +352,20 @@ Switching the embedding model, provider, or dimension is applied by **re-indexin
320
352
  When reranking is enabled, `search_codebase` fetches a **candidate pool** by cosine distance, reranks it with the cross-encoder, and returns the top `limit`. Three knobs and one relationship govern how good the results are and how long they take:
321
353
 
322
354
  - **Pool size** — `fetchLimit = min(max(limit × overfetch, candidateFloor), 200)`. `candidateFloor` (default `80`) guarantees the reranker a large enough menu even at the default `limit = 5`; without it a small `limit` starves the reranker (only `5 × 5 = 25` candidates) and its best matches never enter the window.
323
- - **Threshold ↔ window** — raising `search.similarityThreshold` lets *more* chunks under the bar, so the pool must be wide enough to reach them; a strict threshold with a small pool silently truncates recall. Widen `candidateFloor`/`overfetch` when you raise the threshold.
355
+ - **Threshold ↔ window** — distance scales differ per embedding model, so an unset `search.similarityThreshold` follows the model. Raising the threshold lets *more* chunks under the bar, so the pool must be wide enough to reach them; when more chunks are under the threshold than the window takes, recall is silently truncated. Widen `candidateFloor`/`overfetch` when you raise the threshold.
324
356
  - **Latency** — the reranker cost grows with pool size (on CPU, tens of seconds; a rough guide is a large fixed cost plus a per-candidate increment). Bigger pool = better recall, slower query. GPU/fast hosts can raise `candidateFloor`; ultra-constrained hosts lower it.
325
357
 
358
+ The pool is reachable on large projects too. The HNSW index scan stops after `hnsw.ef_search` rows (PostgreSQL default 40) and only then applies the `project` filter and the threshold, so a scoped search on a project of a few thousand chunks or more (measured from ~5.8 k; the crossover depends on table statistics) — or an unscoped one — used to get at most 40 candidates, often far fewer. Each search now sets `hnsw.ef_search` to the requested row count (at least 40, at most 1000) and, on pgvector ≥ 0.8, `hnsw.iterative_scan = relaxed_order` for its own query, so `candidateFloor` / `overfetch` size the pool the reranker actually receives, unless the iterative scan stops at `hnsw.max_scan_tuples` (20 000) first. On an older pgvector only `ef_search` is raised, and one `warn` line per process names the version — or `unknown` if the first version read failed, with no later line naming it. Like the Voyage warning above, it is visible only at `logging.level` `warn` or lower.
359
+
326
360
  To size these for **your** hardware, turn on telemetry:
327
361
 
328
362
  ```bash
329
- rag-config set logging.searchMetrics true
363
+ rag-config set logging.level info
330
364
  ```
331
365
 
332
- Each search then writes one `info` line (independent of `logging.level`) with, among others: `fetchLimit`, `candidatesReturned`, `poolSaturated`, `maxCosineRankSurvived`, `distinctFilesInResult`, `topFileShare`, and `embedMs`/`rerankMs`/`dbMs`/`totalMs`. Read them as:
366
+ Each search then writes one `search metrics` line with, among others: `fetchLimit`, `candidatesReturned`, `poolSaturated`, `maxCosineRankSurvived`, `distinctFilesInResult`, `topFileShare`, `efSearch`/`iterativeScan` (the HNSW settings sent with the query), and `embedMs`/`rerankMs`/`dbMs`/`totalMs`. Read them as:
333
367
 
334
- - **`poolSaturated: true`** — more chunks were under the threshold than the window took → recall is being truncated; raise `candidateFloor`/`overfetch`.
368
+ - **`poolSaturated: true`** — more chunks were under the threshold than the window took → recall is being truncated; raise `candidateFloor`/`overfetch`. Under the `1.0` fallback (a model with no recommended value) nearly every chunk is under the threshold, so in a scope holding more chunks than the window this stays true at any pool size, and every search that reranks sends the full pool to the reranker; set `search.similarityThreshold` instead of widening the pool.
335
369
  - **`maxCosineRankSurvived`** — the deepest cosine rank that survived reranking. If it stays well below `fetchLimit`, the pool is bigger than needed → shrink it to save latency; if it approaches `fetchLimit`, widen.
336
370
 
337
371
  ## Index consistency
@@ -349,13 +383,13 @@ A failure pruning one project does not block the others or stop indexing of the
349
383
 
350
384
  ## Architecture
351
385
 
352
- **Stack:** TypeScript on Node.js 18+ · PostgreSQL 17 + pgvector (HNSW, cosine distance) via Drizzle ORM · ts-morph for AST/symbol/call-graph extraction · `@langchain/textsplitters` for chunk fallback · MCP SDK over stdio · Zod v4 for config validation and JSON-Schema generation · pino + pino-roll for logging · plain `node:http` dashboard with vis-network · Vitest · Docker Compose for Postgres + Infinity.
386
+ **Stack:** TypeScript on Node.js 22.13+ · PostgreSQL 17 + pgvector (HNSW, cosine distance) via Drizzle ORM · ts-morph for AST/symbol/call-graph extraction · `@langchain/textsplitters` for chunk fallback · MCP SDK over stdio · Zod v4 for config validation and JSON-Schema generation · pino + pino-roll for logging · plain `node:http` dashboard with vis-network · Vitest · Docker Compose for Postgres + Infinity.
353
387
 
354
388
  **Design:**
355
389
  - **Composition root + manual DI** — `src/container.ts` builds every service once and wires it through constructors; no module-level singletons, no DI framework.
356
- - **Dependency inversion** — services depend on interfaces (`IEmbedder`, `IDatabase`, `IAstParser`, `IChunker`, `ISearcher`, `IProjectDiscovery`, `IMonorepoResolver`, `IMcpToolHandler`), so providers (OpenAI-compatible/Voyage/Infinity — Ollama via `openai-compatible`), chunking strategy (`ast`/`character`), and monorepo resolvers are swappable.
390
+ - **Dependency inversion** — services depend on interfaces (`IEmbedder`, `IDatabase`, `IAstParser`, `IChunker`, the role interfaces of the search and path parts (`IChunkSearch`, `IToolPaths`, `ISymbolLookup`, …), `IProjectDiscovery`, `IMonorepoResolver`, `IMcpToolHandler`), so providers (OpenAI-compatible/Voyage/Infinity — Ollama via `openai-compatible`), chunking strategy (`ast`/`character`), and monorepo resolvers are swappable.
357
391
  - **Command pattern** — each CLI mode (`rag-index`, `rag-mcp`, `rag-config`, `rag-web`) is its own `ICommand`; `app.ts` only dispatches.
358
- - **Registry / Open-Closed** — MCP tools are `IMcpToolHandler` implementations registered in a `Map`; adding a tool never touches `McpServer`.
392
+ - **Registry / Open-Closed** — MCP tools extend `McpTool`, which implements `IMcpToolHandler`, and are registered in a `Map` by `McpServer.createTools()`; adding a tool means extending `McpTool` and registering it there, and the dispatch logic never changes.
359
393
  - **Fallback chains** — vector dimension: config override → model registry → runtime probe; `find_symbol`/`get_dependents`: indexed table → grep; failed rerank → vector order.
360
394
  - **Incremental indexing** — mtime fast path, SHA-256 fallback for clones/transfers, AST-aligned chunk boundaries so declarations aren't split arbitrarily.
361
395
  - **Index/model consistency guard** — `index_meta` tracks the embedding model that built the index; search refuses with a clear re-index message on mismatch instead of returning noise.
@@ -364,4 +398,6 @@ Full internals (per-file responsibilities, every pattern with file:line referenc
364
398
 
365
399
  ## License
366
400
 
367
- MIT
401
+ Copyright (C) 2026 Oleksandr Antoniuk.
402
+
403
+ Licensed under the [GNU Affero General Public License v3.0 only](LICENSE) (`AGPL-3.0-only`). Versions up to and including `0.3.0-alpha` were released under the MIT License and remain available under it.
package/dist/app.js CHANGED
@@ -37,6 +37,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
37
37
  const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
39
  const help_1 = require("./src/help");
40
+ const index_args_1 = require("./src/index-args");
40
41
  const container_1 = require("./src/container");
41
42
  const config_1 = require("./src/config");
42
43
  const config_store_1 = require("./src/config-store");
@@ -46,20 +47,13 @@ const McpCommand_1 = require("./src/commands/McpCommand");
46
47
  const ConfigCommand_1 = require("./src/commands/ConfigCommand");
47
48
  const WebCommand_1 = require("./src/commands/WebCommand");
48
49
  const setup_1 = require("./src/setup");
49
- /**
50
- * Load `~/.config/codesearch-mcp/.env` into `process.env` before config is read.
51
- * All five binaries share this file — `rag-mcp` in particular is spawned by the
52
- * MCP client and does not inherit shell exports. A real `process.env` value
53
- * always wins over the file. Minimal `KEY=VALUE` parser (no `dotenv` dep):
54
- * skips blanks and `#` comments, strips one layer of surrounding quotes.
55
- */
56
50
  function loadDotEnv(file) {
57
51
  let text;
58
52
  try {
59
53
  text = fs.readFileSync(file, 'utf-8');
60
54
  }
61
55
  catch {
62
- return; // absent / unreadable → no-op
56
+ return;
63
57
  }
64
58
  for (const rawLine of text.split(/\r?\n/)) {
65
59
  const line = rawLine.trim();
@@ -76,7 +70,7 @@ function loadDotEnv(file) {
76
70
  val = val.slice(1, -1);
77
71
  }
78
72
  if (!(key in process.env))
79
- process.env[key] = val; // real env wins
73
+ process.env[key] = val;
80
74
  }
81
75
  }
82
76
  async function main() {
@@ -88,8 +82,6 @@ async function main() {
88
82
  const isConfig = argv1 === 'rag-config' || process.argv[2] === 'config' || process.argv[2] === 'rag-config';
89
83
  const isWeb = argv1 === 'rag-web' || process.argv[2] === 'web' || process.argv[2] === 'rag-web';
90
84
  const isBinary = ['rag-index', 'rag-mcp', 'rag-setup', 'rag-config', 'rag-web'].includes(argv1);
91
- // `rag-index eval` — a sub-mode of index, not its own binary, so it sits one
92
- // token deeper than the other modes (`rag-index eval` / `index eval`).
93
85
  const evalSubArg = isBinary ? process.argv[2] : process.argv[3];
94
86
  const isEval = isIndex && evalSubArg === 'eval';
95
87
  const mode = isEval ? 'eval' : isIndex ? 'index' : isMcp ? 'mcp' : isSetup ? 'setup' : isConfig ? 'config' : isWeb ? 'web' : (process.argv[2] ?? '');
@@ -99,7 +91,6 @@ async function main() {
99
91
  process.exit(0);
100
92
  }
101
93
  const effectiveArgs = process.argv.slice(isEval ? (isBinary ? 3 : 4) : (isBinary ? 2 : 3));
102
- // Arg validation
103
94
  if (mode === 'eval') {
104
95
  const valueFlags = new Set(['--suite', '--baseline', '--project']);
105
96
  const unknown = [];
@@ -108,7 +99,7 @@ async function main() {
108
99
  if (valueFlags.has(a)) {
109
100
  const next = effectiveArgs[i + 1];
110
101
  if (next !== undefined && !next.startsWith('-'))
111
- i++; // consume the value
102
+ i++;
112
103
  continue;
113
104
  }
114
105
  unknown.push(a);
@@ -120,25 +111,17 @@ async function main() {
120
111
  }
121
112
  }
122
113
  else if (mode === 'index') {
123
- const boolFlags = new Set(['--reindex', '--select']);
124
- const unknown = [];
125
- for (let i = 0; i < effectiveArgs.length; i++) {
126
- const a = effectiveArgs[i];
127
- if (boolFlags.has(a))
128
- continue;
129
- if (a === '--project') {
130
- const next = effectiveArgs[i + 1];
131
- if (next !== undefined && !next.startsWith('-'))
132
- i++; // consume the name
133
- continue;
134
- }
135
- unknown.push(a);
136
- }
137
- if (unknown.length > 0) {
138
- console.error(`Unexpected argument: ${unknown[0]}\n`);
114
+ const unexpected = (0, index_args_1.findUnexpectedIndexArg)(effectiveArgs);
115
+ if (unexpected !== undefined) {
116
+ console.error(`Unexpected argument: ${unexpected}\n`);
139
117
  (0, help_1.printHelp)(isBinary ? argv1 : 'rag-index');
140
118
  process.exit(1);
141
119
  }
120
+ const cwdCause = (0, index_args_1.unreadableCwdCause)(effectiveArgs);
121
+ if (cwdCause !== undefined) {
122
+ console.error(`\n${cwdCause}`);
123
+ process.exit(1);
124
+ }
142
125
  }
143
126
  else if (mode === 'mcp' || mode === 'setup') {
144
127
  if (effectiveArgs.length > 0) {
@@ -158,7 +141,7 @@ async function main() {
158
141
  process.exit(1);
159
142
  }
160
143
  i++;
161
- continue; // skip its (validated) value
144
+ continue;
162
145
  }
163
146
  console.error(`Unexpected argument: ${effectiveArgs[i]}\n`);
164
147
  (0, help_1.printHelp)(isBinary ? argv1 : 'rag-web');
@@ -173,7 +156,6 @@ async function main() {
173
156
  await new ConfigCommand_1.ConfigCommand(effectiveArgs).execute();
174
157
  process.exit(0);
175
158
  }
176
- // All other modes require a valid config.json
177
159
  const loggerName = mode === 'index' || mode === 'eval' ? 'indexer' : mode === 'mcp' ? 'mcp' : mode === 'web' ? 'web' : undefined;
178
160
  let container;
179
161
  try {
@@ -193,14 +175,11 @@ async function main() {
193
175
  console.error(`workspaceRoot is not configured. ${hint}`);
194
176
  process.exit(1);
195
177
  }
196
- // Construct only the selected command — building every command eagerly would
197
- // run each constructor's arg parsing (e.g. WebCommand's --port) for modes that
198
- // never use it.
199
178
  const factories = {
200
- index: () => new IndexCommand_1.IndexCommand(container, argv1),
201
- eval: () => new EvalCommand_1.EvalCommand(container, argv1),
202
- mcp: () => new McpCommand_1.McpCommand(container),
203
- web: () => new WebCommand_1.WebCommand(container, effectiveArgs),
179
+ index: () => new IndexCommand_1.IndexCommand(container.db, container.embedder, container.indexer, container.search.dependencies, container.discovery, container.config, argv1),
180
+ eval: () => new EvalCommand_1.EvalCommand(container.db, container.search.chunks, container.search.catalog, container.config, argv1),
181
+ mcp: () => new McpCommand_1.McpCommand(container.readiness, container.mcp, container.logger),
182
+ web: () => new WebCommand_1.WebCommand(container.db, container.embedder, container.search.catalog, container.search.dependencies, container.config, container.logger, effectiveArgs),
204
183
  };
205
184
  const factory = factories[mode];
206
185
  if (!factory) {
@@ -0,0 +1,8 @@
1
+ import { type SourceFile } from 'ts-morph';
2
+ import type { CallEdge } from '../interfaces/IAstParser';
3
+ import type { IIndexedNodeResolver } from '../interfaces';
4
+ export declare class CallExtractor {
5
+ private readonly resolver;
6
+ constructor(resolver: IIndexedNodeResolver);
7
+ extractCalls(sourceFile: SourceFile, filePath: string, serviceName: string): CallEdge[];
8
+ }
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CallExtractor = void 0;
4
+ const ts_morph_1 = require("ts-morph");
5
+ class CallExtractor {
6
+ resolver;
7
+ constructor(resolver) {
8
+ this.resolver = resolver;
9
+ }
10
+ extractCalls(sourceFile, filePath, serviceName) {
11
+ const calls = [];
12
+ for (const callExpr of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.CallExpression)) {
13
+ const expr = callExpr.getExpression();
14
+ let calleeName = null;
15
+ let calleeFile = null;
16
+ let calleeDecl;
17
+ try {
18
+ if (ts_morph_1.Node.isPropertyAccessExpression(expr)) {
19
+ calleeName = expr.getName();
20
+ const sym = expr.getNameNode().getSymbol();
21
+ const decl = sym?.getDeclarations()?.[0];
22
+ const declPath = decl?.getSourceFile().getFilePath();
23
+ if (declPath && !declPath.includes('/node_modules/')) {
24
+ calleeFile = declPath;
25
+ calleeDecl = decl;
26
+ }
27
+ }
28
+ else if (ts_morph_1.Node.isIdentifier(expr)) {
29
+ calleeName = expr.getText();
30
+ const sym = expr.getSymbol();
31
+ if (sym) {
32
+ const firstDecl = sym.getDeclarations()?.[0];
33
+ const resolved = firstDecl && ts_morph_1.Node.isImportSpecifier(firstDecl)
34
+ ? (sym.getAliasedSymbol() ?? sym)
35
+ : sym;
36
+ const decl = resolved.getDeclarations()?.[0];
37
+ const declPath = decl?.getSourceFile().getFilePath();
38
+ if (declPath && !declPath.includes('/node_modules/')) {
39
+ calleeFile = declPath;
40
+ calleeDecl = decl;
41
+ }
42
+ }
43
+ }
44
+ }
45
+ catch {
46
+ }
47
+ if (!calleeName || !calleeFile)
48
+ continue;
49
+ const described = calleeDecl ? this.resolver.describeIndexedNode(calleeDecl) : null;
50
+ const calleeSymbol = described ? this.resolver.qualifiedName(described) : null;
51
+ const fromSymbol = this.resolver.findFromSymbol(callExpr);
52
+ const pos = sourceFile.compilerNode
53
+ .getLineAndCharacterOfPosition(callExpr.getStart());
54
+ calls.push({
55
+ project: serviceName,
56
+ fromFile: filePath,
57
+ fromSymbol,
58
+ calleeName,
59
+ calleeFile,
60
+ calleeSymbol,
61
+ line: pos.line + 1,
62
+ col: pos.character + 1,
63
+ });
64
+ }
65
+ return calls;
66
+ }
67
+ }
68
+ exports.CallExtractor = CallExtractor;
@@ -0,0 +1,22 @@
1
+ import { type SourceFile } from 'ts-morph';
2
+ import type { SymbolEntry } from '../interfaces/IAstParser';
3
+ import type { ILogger, IIndexedNodeResolver } from '../interfaces';
4
+ import type { DeclarationCandidate } from '../declarations/DeclarationTreeBuilder';
5
+ import { type ResolveDeclaration } from './node-helpers';
6
+ export declare class GraphqlNameExtractor {
7
+ private readonly resolver;
8
+ private readonly logger;
9
+ private readonly resolveDeclaration;
10
+ constructor(resolver: IIndexedNodeResolver, logger: ILogger, resolveDeclaration?: ResolveDeclaration);
11
+ private buildGraphqlImportMap;
12
+ private recognizedDecorator;
13
+ private deriveGraphqlName;
14
+ private logGraphqlSkip;
15
+ private declarationNameFor;
16
+ private classGraphqlTypeName;
17
+ private resolveResolverParentType;
18
+ private extractClassTypeRows;
19
+ private extractResolveFieldRow;
20
+ private extractMethodOperationRows;
21
+ extractGraphqlNames(sourceFile: SourceFile, filePath: string, serviceName: string, ownRows: DeclarationCandidate[]): SymbolEntry[];
22
+ }