codesearch-mcp 0.2.2-alpha → 0.3.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 (158) hide show
  1. package/CHANGELOG.md +65 -1
  2. package/README.md +60 -33
  3. package/dist/app.js +46 -22
  4. package/dist/src/chunking/AstChunker.d.ts +12 -0
  5. package/dist/src/chunking/AstChunker.js +30 -0
  6. package/dist/src/chunking/CharacterChunkStrategy.d.ts +8 -0
  7. package/dist/src/chunking/CharacterChunkStrategy.js +15 -0
  8. package/dist/src/chunking/DeclarationChunkStrategy.d.ts +15 -0
  9. package/dist/src/chunking/DeclarationChunkStrategy.js +87 -0
  10. package/dist/src/chunking/FragmentMerger.d.ts +11 -0
  11. package/dist/src/chunking/FragmentMerger.js +61 -0
  12. package/dist/src/chunking/ImportOnlyFilter.d.ts +5 -0
  13. package/dist/src/chunking/ImportOnlyFilter.js +14 -0
  14. package/dist/src/chunking/LangchainTextSplitter.d.ts +21 -0
  15. package/dist/src/chunking/LangchainTextSplitter.js +133 -0
  16. package/dist/src/chunking/MinSizeSplitPolicy.d.ts +9 -0
  17. package/dist/src/chunking/MinSizeSplitPolicy.js +18 -0
  18. package/dist/src/chunking/RunPacker.d.ts +12 -0
  19. package/dist/src/chunking/RunPacker.js +84 -0
  20. package/dist/src/chunking/create-chunker.d.ts +3 -0
  21. package/dist/src/chunking/create-chunker.js +15 -0
  22. package/dist/src/chunking/interfaces.d.ts +27 -0
  23. package/dist/src/chunking/interfaces.js +2 -0
  24. package/dist/src/commands/ConfigCommand.js +1 -9
  25. package/dist/src/commands/EvalCommand.d.ts +11 -0
  26. package/dist/src/commands/EvalCommand.js +264 -0
  27. package/dist/src/commands/IndexCommand.js +70 -66
  28. package/dist/src/commands/McpCommand.js +2 -8
  29. package/dist/src/commands/WebCommand.js +2 -5
  30. package/dist/src/config-error.d.ts +0 -4
  31. package/dist/src/config-error.js +0 -4
  32. package/dist/src/config-store.d.ts +1 -54
  33. package/dist/src/config-store.js +8 -69
  34. package/dist/src/config.d.ts +8 -17
  35. package/dist/src/config.js +35 -34
  36. package/dist/src/constants.d.ts +2 -28
  37. package/dist/src/constants.js +6 -28
  38. package/dist/src/container.d.ts +6 -0
  39. package/dist/src/container.js +12 -3
  40. package/dist/src/db/index.d.ts +1 -1
  41. package/dist/src/db/index.js +1 -2
  42. package/dist/src/db/schema.d.ts +22 -29
  43. package/dist/src/db/schema.js +3 -14
  44. package/dist/src/declarations/DeclarationTree.d.ts +19 -0
  45. package/dist/src/declarations/DeclarationTree.js +66 -0
  46. package/dist/src/declarations/DeclarationTreeBuilder.d.ts +22 -0
  47. package/dist/src/declarations/DeclarationTreeBuilder.js +179 -0
  48. package/dist/src/declarations/SourceLines.d.ts +37 -0
  49. package/dist/src/declarations/SourceLines.js +415 -0
  50. package/dist/src/dependency-traversal.d.ts +10 -0
  51. package/dist/src/dependency-traversal.js +39 -0
  52. package/dist/src/enriched-prompt.d.ts +0 -5
  53. package/dist/src/enriched-prompt.js +0 -5
  54. package/dist/src/error-record.d.ts +12 -0
  55. package/dist/src/error-record.js +248 -0
  56. package/dist/src/eval-corpus.d.ts +16 -0
  57. package/dist/src/eval-corpus.js +169 -0
  58. package/dist/src/eval-results.d.ts +15 -0
  59. package/dist/src/eval-results.js +397 -0
  60. package/dist/src/file-utils.d.ts +10 -0
  61. package/dist/src/file-utils.js +19 -1
  62. package/dist/src/graph.d.ts +0 -25
  63. package/dist/src/graph.js +0 -28
  64. package/dist/src/help.js +4 -1
  65. package/dist/src/hnsw-settings.d.ts +3 -0
  66. package/dist/src/hnsw-settings.js +24 -0
  67. package/dist/src/interfaces/IAstParser.d.ts +5 -30
  68. package/dist/src/interfaces/IChunker.d.ts +3 -12
  69. package/dist/src/interfaces/ICorpusInspector.d.ts +7 -0
  70. package/dist/src/interfaces/ICorpusInspector.js +2 -0
  71. package/dist/src/interfaces/IDatabase.d.ts +2 -36
  72. package/dist/src/interfaces/IEmbedder.d.ts +0 -9
  73. package/dist/src/interfaces/ILogger.d.ts +1 -11
  74. package/dist/src/interfaces/IMcpToolHandler.d.ts +0 -5
  75. package/dist/src/interfaces/IMonorepoResolver.d.ts +0 -22
  76. package/dist/src/interfaces/IProjectDiscovery.d.ts +0 -17
  77. package/dist/src/interfaces/IRetrievalEvaluator.d.ts +216 -0
  78. package/dist/src/interfaces/IRetrievalEvaluator.js +2 -0
  79. package/dist/src/interfaces/ISearcher.d.ts +26 -20
  80. package/dist/src/interfaces/index.d.ts +1 -0
  81. package/dist/src/interrupt.js +0 -2
  82. package/dist/src/owner-path.d.ts +9 -10
  83. package/dist/src/owner-path.js +11 -23
  84. package/dist/src/project-scope.d.ts +7 -0
  85. package/dist/src/project-scope.js +37 -0
  86. package/dist/src/project-selector.d.ts +1 -0
  87. package/dist/src/project-selector.js +13 -6
  88. package/dist/src/retrieval-eval.d.ts +24 -0
  89. package/dist/src/retrieval-eval.js +500 -0
  90. package/dist/src/search/result-shaping.d.ts +4 -0
  91. package/dist/src/search/result-shaping.js +19 -0
  92. package/dist/src/services/AstParserService.d.ts +28 -23
  93. package/dist/src/services/AstParserService.js +391 -98
  94. package/dist/src/services/DatabaseService.d.ts +2 -19
  95. package/dist/src/services/DatabaseService.js +44 -81
  96. package/dist/src/services/EmbedderFactory.d.ts +0 -4
  97. package/dist/src/services/EmbedderFactory.js +0 -5
  98. package/dist/src/services/IndexerService.d.ts +2 -10
  99. package/dist/src/services/IndexerService.js +29 -45
  100. package/dist/src/services/InfinityEmbedder.d.ts +0 -21
  101. package/dist/src/services/InfinityEmbedder.js +0 -15
  102. package/dist/src/services/LoggerService.d.ts +7 -5
  103. package/dist/src/services/LoggerService.js +37 -41
  104. package/dist/src/services/McpServer.d.ts +25 -10
  105. package/dist/src/services/McpServer.js +101 -64
  106. package/dist/src/services/MonorepoAwareDiscoveryService.d.ts +1 -45
  107. package/dist/src/services/MonorepoAwareDiscoveryService.js +1 -51
  108. package/dist/src/services/NxMonorepoResolver.d.ts +0 -29
  109. package/dist/src/services/NxMonorepoResolver.js +5 -56
  110. package/dist/src/services/OpenAICompatibleEmbedder.d.ts +0 -6
  111. package/dist/src/services/OpenAICompatibleEmbedder.js +1 -14
  112. package/dist/src/services/ProjectDiscoveryService.d.ts +0 -4
  113. package/dist/src/services/ProjectDiscoveryService.js +0 -4
  114. package/dist/src/services/SearcherService.d.ts +127 -70
  115. package/dist/src/services/SearcherService.js +526 -305
  116. package/dist/src/services/VoyageEmbedder.d.ts +0 -6
  117. package/dist/src/services/VoyageEmbedder.js +0 -6
  118. package/dist/src/services/WebServerService.d.ts +1 -53
  119. package/dist/src/services/WebServerService.js +11 -57
  120. package/dist/src/services/http-retry.d.ts +0 -7
  121. package/dist/src/services/http-retry.js +1 -10
  122. package/dist/src/services/infinity-provisioning.d.ts +0 -74
  123. package/dist/src/services/infinity-provisioning.js +5 -88
  124. package/dist/src/services/legacy-ollama-index.d.ts +0 -16
  125. package/dist/src/services/legacy-ollama-index.js +0 -23
  126. package/dist/src/services/reranker.d.ts +0 -9
  127. package/dist/src/services/reranker.js +46 -12
  128. package/dist/src/setup.d.ts +11 -23
  129. package/dist/src/setup.js +98 -71
  130. package/dist/src/symbol-name.d.ts +0 -12
  131. package/dist/src/symbol-name.js +0 -13
  132. package/dist/src/symbol-rows.d.ts +2 -11
  133. package/dist/src/symbol-rows.js +5 -13
  134. package/dist/src/tool-call-log.d.ts +2 -0
  135. package/dist/src/tool-call-log.js +24 -0
  136. package/dist/src/tools/FindSymbolTool.js +14 -5
  137. package/dist/src/tools/GetCallersTool.d.ts +3 -1
  138. package/dist/src/tools/GetCallersTool.js +16 -6
  139. package/dist/src/tools/GetDependenciesTool.d.ts +3 -1
  140. package/dist/src/tools/GetDependenciesTool.js +15 -8
  141. package/dist/src/tools/GetDependentsTool.d.ts +3 -1
  142. package/dist/src/tools/GetDependentsTool.js +31 -16
  143. package/dist/src/tools/ListFilesTool.d.ts +3 -1
  144. package/dist/src/tools/ListFilesTool.js +14 -12
  145. package/dist/src/tools/ListProjectsTool.js +5 -1
  146. package/dist/src/tools/ReadFileRangeTool.d.ts +3 -1
  147. package/dist/src/tools/ReadFileRangeTool.js +12 -11
  148. package/dist/src/tools/ReadFileTool.d.ts +3 -1
  149. package/dist/src/tools/ReadFileTool.js +12 -10
  150. package/dist/src/tools/SearchCodebaseTool.js +7 -2
  151. package/dist/src/tools/format-helpers.d.ts +10 -1
  152. package/dist/src/tools/format-helpers.js +145 -8
  153. package/dist/src/web/dashboard.d.ts +0 -8
  154. package/dist/src/web/dashboard.js +7 -10
  155. package/package.json +10 -9
  156. package/dist/src/services/AstChunker.d.ts +0 -42
  157. package/dist/src/services/AstChunker.js +0 -158
  158. package/docs/web-dashboard.md +0 -206
package/dist/src/setup.js CHANGED
@@ -35,6 +35,8 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.EMBEDDING_PROVIDER_CHOICES = void 0;
37
37
  exports.dockerPromptDefault = dockerPromptDefault;
38
+ exports.rerankerChoices = rerankerChoices;
39
+ exports.promptReranker = promptReranker;
38
40
  exports.prepareComposeAndStartDb = prepareComposeAndStartDb;
39
41
  exports.provisionInfinity = provisionInfinity;
40
42
  exports.runSetup = runSetup;
@@ -43,6 +45,7 @@ const fs = __importStar(require("fs"));
43
45
  const path = __importStar(require("path"));
44
46
  const os = __importStar(require("os"));
45
47
  const child_process_1 = require("child_process");
48
+ const constants_1 = require("./constants");
46
49
  const config_store_1 = require("./config-store");
47
50
  const config_1 = require("./config");
48
51
  const infinity_provisioning_1 = require("./services/infinity-provisioning");
@@ -68,51 +71,115 @@ function expandHome(p) {
68
71
  }
69
72
  const PROVIDER_DEFAULTS = {
70
73
  'openai-compatible': { baseURL: 'https://api.openai.com/v1', model: 'text-embedding-3-small', envVar: 'OPENAI_API_KEY' },
71
- 'voyage': { baseURL: 'https://api.voyageai.com/v1', model: 'voyage-code-3', envVar: 'VOYAGE_API_KEY' },
74
+ 'voyage': { baseURL: 'https://api.voyageai.com/v1', model: 'voyage-code-4', envVar: 'VOYAGE_API_KEY' },
72
75
  'infinity': { baseURL: 'http://localhost:7997', model: 'jinaai/jina-embeddings-v2-base-code', envVar: '' },
73
76
  };
74
- /** Providers the wizard offers, in prompt order; the first is the preselected default. Exported for unit testing. */
75
77
  exports.EMBEDDING_PROVIDER_CHOICES = ['infinity', 'openai-compatible', 'voyage'];
76
- /** Providers that run a local inference backend we can probe / provision. */
77
78
  function isLocalBackend(p) {
78
79
  return p === 'infinity';
79
80
  }
80
- /**
81
- * Probe whether the embedding backend is already reachable, so the "start
82
- * Docker?" prompt can default to no (mirrors the DB `checkRemoteDb` flow).
83
- * Cloud providers have no local backend → always "reachable" (nothing to start).
84
- */
85
81
  async function checkEmbeddingBackend(provider, baseURL) {
86
82
  if (!isLocalBackend(provider))
87
83
  return true;
88
84
  return (0, infinity_provisioning_1.probeInfinityHealth)(baseURL);
89
85
  }
90
- /**
91
- * Default answer for the "start Docker containers?" prompt: no only when both
92
- * the database and the embedding backend are already reachable. Pure — exported
93
- * for unit testing.
94
- */
95
86
  function dockerPromptDefault(dbReachable, embReachable) {
96
87
  return (dbReachable && embReachable) ? 'n' : 'y';
97
88
  }
98
- /**
99
- * Write (or replace) a single `KEY=VALUE` line in `~/.config/codesearch-mcp/.env`,
100
- * chmod 600. API keys live here — never in config.json — referenced as `env:KEY`.
101
- */
102
89
  function writeEnvKey(name, value) {
103
90
  fs.mkdirSync(config_store_1.CONFIG_DIR, { recursive: true });
104
91
  let existing = '';
105
92
  try {
106
93
  existing = fs.readFileSync(config_store_1.ENV_FILE, 'utf-8');
107
94
  }
108
- catch { /* absent */ }
95
+ catch { }
109
96
  const kept = existing.split(/\r?\n/).filter(l => l.trim() && !l.trim().startsWith(`${name}=`));
110
97
  kept.push(`${name}=${value}`);
111
98
  fs.writeFileSync(config_store_1.ENV_FILE, kept.join('\n') + '\n', { mode: 0o600 });
112
99
  try {
113
100
  fs.chmodSync(config_store_1.ENV_FILE, 0o600);
114
101
  }
115
- catch { /* best effort */ }
102
+ catch { }
103
+ }
104
+ const VOYAGE_RERANK_ENV = 'VOYAGE_RERANK_API_KEY';
105
+ const RERANKER_ENV = 'RERANKER_API_KEY';
106
+ function rerankerChoices(embProvider) {
107
+ if (embProvider === 'infinity')
108
+ return ['infinity', 'voyage', 'openai-compatible', 'none'];
109
+ if (embProvider === 'voyage')
110
+ return ['voyage', 'openai-compatible', 'none'];
111
+ return ['none', 'voyage', 'openai-compatible'];
112
+ }
113
+ async function askRequired(io, question) {
114
+ for (;;) {
115
+ const answer = (await io.ask(question)).trim();
116
+ if (answer)
117
+ return answer;
118
+ io.log(' A value is required.');
119
+ }
120
+ }
121
+ function isHttpUrl(value) {
122
+ try {
123
+ const { protocol, username, password } = new URL(value);
124
+ return (protocol === 'http:' || protocol === 'https:') && !username && !password;
125
+ }
126
+ catch {
127
+ return false;
128
+ }
129
+ }
130
+ async function askHttpUrl(io, question) {
131
+ for (;;) {
132
+ const answer = await askRequired(io, question);
133
+ if (isHttpUrl(answer))
134
+ return answer;
135
+ io.log(' Enter an absolute http(s):// URL without credentials, e.g. http://localhost:7997.');
136
+ }
137
+ }
138
+ async function promptReranker(embProvider, embBaseURL, io) {
139
+ const choices = rerankerChoices(embProvider);
140
+ io.log('\n--- Reranker ---');
141
+ if (choices.includes('infinity'))
142
+ io.log(' infinity — served by the same Infinity container');
143
+ io.log(' voyage — Voyage AI cloud (needs a Voyage API key)');
144
+ io.log(' openai-compatible — any /rerank server (Infinity, vLLM)');
145
+ io.log(' none — no reranking');
146
+ let choice;
147
+ for (;;) {
148
+ choice = (await io.prompt(`Reranker (${choices.join('/')})`, choices[0])).trim();
149
+ if (choices.includes(choice))
150
+ break;
151
+ io.log(` Invalid reranker. Choose: ${choices.join(', ')}`);
152
+ }
153
+ if (choice === 'none')
154
+ return { enabled: false };
155
+ if (choice === 'infinity') {
156
+ const model = (await io.prompt('Reranker model (served by the same Infinity container)', config_store_1.CONFIG_DEFAULTS['reranker.model'])).trim();
157
+ return { enabled: true, provider: 'openai-compatible', baseURL: embBaseURL, model };
158
+ }
159
+ if (choice === 'voyage') {
160
+ const model = (await io.prompt(`Reranker model (${constants_1.VOYAGE_RERANK_MODELS.join(' / ')})`, constants_1.VOYAGE_RERANK_MODELS[0])).trim();
161
+ for (;;) {
162
+ const key = (await io.ask(`Voyage reranker API key (stored in ${config_store_1.ENV_FILE} as ${VOYAGE_RERANK_ENV}): `)).trim();
163
+ if (key) {
164
+ io.writeEnvKey(VOYAGE_RERANK_ENV, key);
165
+ io.log(` Saved reranker API key to ${config_store_1.ENV_FILE} as ${VOYAGE_RERANK_ENV}.`);
166
+ return { enabled: true, provider: 'voyage', model, apiKey: `env:${VOYAGE_RERANK_ENV}` };
167
+ }
168
+ io.log(' The Voyage reranker is not configured without an API key.');
169
+ const disable = (await io.prompt('Disable the reranker? (y/n)', 'y')).trim().toLowerCase();
170
+ if (disable === 'y')
171
+ return { enabled: false };
172
+ }
173
+ }
174
+ const baseURL = await askHttpUrl(io, 'Rerank server base URL: ');
175
+ const model = await askRequired(io, 'Reranker model: ');
176
+ const key = (await io.ask(`Reranker API key (stored in ${config_store_1.ENV_FILE} as ${RERANKER_ENV}, blank for none): `)).trim();
177
+ if (key)
178
+ io.writeEnvKey(RERANKER_ENV, key);
179
+ return {
180
+ enabled: true, provider: 'openai-compatible', baseURL, model,
181
+ ...(key ? { apiKey: `env:${RERANKER_ENV}` } : {}),
182
+ };
116
183
  }
117
184
  async function waitForDb(host, port, user, pass, db, maxWaitSec = 60, logger, steps) {
118
185
  const { Client } = await Promise.resolve().then(() => __importStar(require('pg')));
@@ -147,18 +214,10 @@ async function checkRemoteDb(host, port, user, pass, db, logger, steps) {
147
214
  catch (err) {
148
215
  const msg = err instanceof Error ? err.message : String(err);
149
216
  console.error(` Connection failed: ${msg}`);
150
- logger?.error('checkRemoteDb: connection failed', { reason: msg, steps: JSON.stringify(steps) });
217
+ logger?.error('checkRemoteDb: connection failed', { err, steps: JSON.stringify(steps) });
151
218
  return false;
152
219
  }
153
220
  }
154
- /**
155
- * Regenerate `CONFIG_DIR/docker-compose.yml` from the shipped template
156
- * (carrying over an existing literal `INFINITY_MODEL_ID`, written atomically),
157
- * copy the GPU override, and start the DB container when requested. Runs
158
- * BEFORE the config file is written — its failure aborts setup unsaved,
159
- * matching the existing "abort without saving" behavior of the DB step. Does
160
- * NOT touch Infinity provisioning (moved after `writeConfigJson` — see D6a).
161
- */
162
221
  async function prepareComposeAndStartDb(opts) {
163
222
  const { dbPort, dbUser, dbPass, startDb, logger, steps } = opts;
164
223
  console.log('\nWriting docker-compose.yml to', DOCKER_COMPOSE_DST);
@@ -168,14 +227,11 @@ async function prepareComposeAndStartDb(opts) {
168
227
  .replace('POSTGRES_USER: dev', `POSTGRES_USER: ${yamlQuote(dbUser)}`)
169
228
  .replace('POSTGRES_PASSWORD: dev', `POSTGRES_PASSWORD: ${yamlQuote(dbPass)}`)
170
229
  .replace('"5433:5432"', `"${dbPort}:5432"`);
171
- // Carry over an existing literal INFINITY_MODEL_ID — never the set of the
172
- // config still being entered — so a setup that aborts before the config is
173
- // written leaves a file consistent with the previously saved config.
174
230
  let existingModelId = null;
175
231
  try {
176
232
  existingModelId = (0, infinity_provisioning_1.extractModelIdValue)(fs.readFileSync(DOCKER_COMPOSE_DST, 'utf-8'));
177
233
  }
178
- catch { /* no prior file */ }
234
+ catch { }
179
235
  if (existingModelId) {
180
236
  composeContent = composeContent.replace(/INFINITY_MODEL_ID:\s*"[^"]*"/, `INFINITY_MODEL_ID: "${existingModelId}"`);
181
237
  }
@@ -184,13 +240,12 @@ async function prepareComposeAndStartDb(opts) {
184
240
  try {
185
241
  existingMode = fs.statSync(DOCKER_COMPOSE_DST).mode;
186
242
  }
187
- catch { /* first-ever write */ }
243
+ catch { }
188
244
  (0, infinity_provisioning_1.atomicWriteFile)(DOCKER_COMPOSE_DST, composeContent, existingMode);
189
- // The GPU override is copied verbatim so the managed Infinity path can layer it.
190
245
  try {
191
246
  fs.copyFileSync(DOCKER_GPU_OVERRIDE_SRC, DOCKER_GPU_OVERRIDE_DST);
192
247
  }
193
- catch { /* absent → CPU only */ }
248
+ catch { }
194
249
  if (!startDb)
195
250
  return;
196
251
  console.log('\nStarting the database container...');
@@ -202,30 +257,19 @@ async function prepareComposeAndStartDb(opts) {
202
257
  }
203
258
  await waitForDb('localhost', parseInt(dbPort), dbUser, dbPass, 'postgres', 60, logger, steps);
204
259
  }
205
- /**
206
- * Infinity provisioning (D6a) — runs AFTER the config file is written, so no
207
- * outcome here can leave the config unsaved. `savedConfig` is a fresh read of
208
- * the just-written file (never the process's own startup config). Exit-status
209
- * rules: 0 for served / timeout (warning) / external instructions / declined
210
- * Docker; 1 for any other provisioning failure — the cause is printed and the
211
- * remaining setup steps (DB schema init) still run.
212
- */
213
260
  async function provisionInfinity(savedConfig, startDocker, logger, deps = {}, composePathOverride) {
214
261
  const composePath = composePathOverride ?? DOCKER_COMPOSE_DST;
215
262
  const baseURL = savedConfig.embedding.baseURL;
216
263
  const base = (0, infinity_provisioning_1.serverRoot)(baseURL);
217
264
  const set = (0, infinity_provisioning_1.requiredModelSet)(savedConfig);
218
265
  const managed = (0, infinity_provisioning_1.isManagedInfinity)(baseURL, composePath);
219
- // Targeted write happens for a managed backend regardless of what follows
220
- // (yes/no, served/missing), so the compose file always matches the saved
221
- // config — even when the running container already serves the set.
222
266
  if (managed) {
223
267
  try {
224
268
  (0, infinity_provisioning_1.writeComposeModelSet)(composePath, set);
225
269
  }
226
270
  catch (err) {
227
271
  console.error(`\nFailed to write the managed compose file: ${err.message}`);
228
- logger?.error('provisionInfinity: compose write failed', { reason: err.message });
272
+ logger?.error('provisionInfinity: compose write failed', { err });
229
273
  process.exitCode = 1;
230
274
  }
231
275
  }
@@ -239,17 +283,16 @@ async function provisionInfinity(savedConfig, startDocker, logger, deps = {}, co
239
283
  if (/did not become ready within/.test(msg)) {
240
284
  console.warn(`\nWarning: ${msg}`);
241
285
  console.warn('The container is running and likely still warming up. Check again once /health responds, then run: rag-index');
242
- logger?.warn('provisionInfinity: readiness timeout (non-fatal)', { reason: msg });
286
+ logger?.warn('provisionInfinity: readiness timeout (non-fatal)', { err });
243
287
  }
244
288
  else {
245
289
  console.error(`\nInfinity provisioning failed: ${msg}`);
246
- logger?.error('provisionInfinity: failed', { reason: msg });
290
+ logger?.error('provisionInfinity: failed', { err });
247
291
  process.exitCode = 1;
248
292
  }
249
293
  }
250
294
  return;
251
295
  }
252
- // Docker declined — no Docker command; print what would apply the set.
253
296
  const evaluation = await (0, infinity_provisioning_1.evaluateBackend)(baseURL, composePath, set, deps);
254
297
  if (evaluation.action === 'served')
255
298
  return;
@@ -311,12 +354,6 @@ async function runSetup(logger) {
311
354
  const pd = PROVIDER_DEFAULTS[embProvider];
312
355
  const embBaseURL = await loggedPrompt('Base URL', pd.baseURL);
313
356
  const embModel = await loggedPrompt('Embedding model', pd.model);
314
- // Infinity serves a reranker from the same container — offer to provision one.
315
- let rerankModel = '';
316
- if (embProvider === 'infinity') {
317
- rerankModel = (await loggedPrompt(`Reranker model (served by the same Infinity container; blank to disable)`, config_store_1.CONFIG_DEFAULTS['reranker.model'])).trim();
318
- }
319
- // API keys go into .env (chmod 600); config.json only stores the env: reference.
320
357
  let apiKeyRef = '';
321
358
  if (!isLocalBackend(embProvider)) {
322
359
  const key = (await loggedAsk(`API key (stored in ${config_store_1.ENV_FILE}, leave blank to set later): `)).trim();
@@ -330,6 +367,9 @@ async function runSetup(logger) {
330
367
  console.log(` No key entered — set it later: echo '${pd.envVar}=sk-…' >> ${config_store_1.ENV_FILE}`);
331
368
  }
332
369
  }
370
+ const reranker = await promptReranker(embProvider, embBaseURL, {
371
+ prompt: loggedPrompt, ask: loggedAsk, writeEnvKey, log: msg => console.log(msg),
372
+ });
333
373
  console.log('\n--- Database ---');
334
374
  const dbHost = await loggedPrompt('DB host', 'localhost');
335
375
  const dbPort = await loggedPrompt('DB port', '5433');
@@ -339,8 +379,6 @@ async function runSetup(logger) {
339
379
  const dbReachable = await checkRemoteDb(dbHost, dbPort, dbUser, dbPass, 'postgres', logger, steps);
340
380
  if (dbReachable)
341
381
  console.log(' Database is already reachable.');
342
- // Symmetric to the DB probe: a reachable embedding backend defaults "start
343
- // Docker?" to no. Cloud providers have no local backend to start.
344
382
  process.stdout.write('Checking embedding backend...');
345
383
  const embReachable = await checkEmbeddingBackend(embProvider, embBaseURL);
346
384
  if (isLocalBackend(embProvider)) {
@@ -400,26 +438,15 @@ async function runSetup(logger) {
400
438
  model: embModel,
401
439
  ...(apiKeyRef ? { apiKey: apiKeyRef } : {}),
402
440
  },
441
+ reranker,
403
442
  logging: { level: logLevel },
404
443
  workspaceRoot,
405
444
  };
406
- // Infinity serves the reranker from the same container — auto-wire reranker.*
407
- // to that server. `url` derives from provider + baseURL in the Config constructor.
408
- if (embProvider === 'infinity' && rerankModel) {
409
- cfg.reranker = {
410
- enabled: true,
411
- provider: 'openai-compatible',
412
- baseURL: embBaseURL,
413
- model: rerankModel,
414
- };
415
- }
416
445
  if (logDir)
417
446
  cfg.logging.dir = logDir;
418
447
  (0, config_store_1.writeConfigJson)(cfg);
419
448
  console.log(`\nConfig saved to ${config_store_1.CONFIG_FILE}`);
420
449
  rl.close();
421
- // Infinity provisioning happens only now — after the config is safely on
422
- // disk — so a provisioning failure never leaves the wizard's answers unsaved.
423
450
  if (embProvider === 'infinity') {
424
451
  await provisionInfinity(new config_1.Config(), startDocker, logger);
425
452
  }
@@ -438,7 +465,7 @@ async function runSetup(logger) {
438
465
  }
439
466
  if (initError) {
440
467
  console.error(`Database schema init failed: ${initError.message}`);
441
- logger?.error('runSetup: database schema init failed', { reason: initError.message, steps: JSON.stringify(steps) });
468
+ logger?.error('runSetup: database schema init failed', { err: initError, steps: JSON.stringify(steps) });
442
469
  process.exit(1);
443
470
  }
444
471
  console.log('\nSetup complete!');
@@ -16,22 +16,11 @@ export type FindSymbolQuery = {
16
16
  container: string | null;
17
17
  name: string;
18
18
  };
19
- /** Split `P.n` into container `P` and last segment `n`. Requires a qualified name. */
20
19
  export declare function splitQualified(input: string): {
21
20
  container: string;
22
21
  name: string;
23
22
  };
24
- /**
25
- * Parse a `find_symbol` name into one of the four accepted forms (bare,
26
- * qualified, quoted, computed), or null when it matches none — the caller
27
- * should reject with `Invalid symbol name` without querying anything.
28
- */
29
23
  export declare function parseFindSymbolName(input: string): FindSymbolQuery | null;
30
- /**
31
- * Parse a `get_callers` symbol into bare or qualified form only — the quoted
32
- * and computed forms of `find_symbol` are meaningless here (calls through
33
- * element access are never recorded in the call graph).
34
- */
35
24
  export declare function parseCallerSymbol(input: string): {
36
25
  form: 'bare';
37
26
  name: string;
@@ -40,5 +29,4 @@ export declare function parseCallerSymbol(input: string): {
40
29
  container: string;
41
30
  name: string;
42
31
  } | null;
43
- /** Escape `%`, `_` and `\` for a `LIKE ... ESCAPE '\'` pattern. */
44
32
  export declare function escapeLike(s: string): string;
@@ -11,22 +11,15 @@ const PFX_SRC = '(?:#?[\\w$]+(?:\\.#?[\\w$]+)*\\.)?';
11
11
  const QUOTED_RE = new RegExp(`^(${PFX_SRC})('[^'\\r\\n]+'|"[^"\\r\\n]+")$`);
12
12
  const COMPUTED_RE = new RegExp(`^(${PFX_SRC})(\\[[^\\]\\r\\n]+\\])$`);
13
13
  const MAX_QUOTED_COMPUTED_LEN = 256;
14
- /** Strip a qualified name's trailing `.` container prefix, if present. */
15
14
  function stripTrailingDot(prefix) {
16
15
  if (!prefix)
17
16
  return null;
18
17
  return prefix.slice(0, -1);
19
18
  }
20
- /** Split `P.n` into container `P` and last segment `n`. Requires a qualified name. */
21
19
  function splitQualified(input) {
22
20
  const idx = input.lastIndexOf('.');
23
21
  return { container: input.slice(0, idx), name: input.slice(idx + 1) };
24
22
  }
25
- /**
26
- * Parse a `find_symbol` name into one of the four accepted forms (bare,
27
- * qualified, quoted, computed), or null when it matches none — the caller
28
- * should reject with `Invalid symbol name` without querying anything.
29
- */
30
23
  function parseFindSymbolName(input) {
31
24
  if (input.length <= MAX_QUOTED_COMPUTED_LEN) {
32
25
  const quoted = input.match(QUOTED_RE);
@@ -47,11 +40,6 @@ function parseFindSymbolName(input) {
47
40
  }
48
41
  return null;
49
42
  }
50
- /**
51
- * Parse a `get_callers` symbol into bare or qualified form only — the quoted
52
- * and computed forms of `find_symbol` are meaningless here (calls through
53
- * element access are never recorded in the call graph).
54
- */
55
43
  function parseCallerSymbol(input) {
56
44
  if (exports.QUALIFIED_NAME_RE.test(input)) {
57
45
  const { container, name } = splitQualified(input);
@@ -62,7 +50,6 @@ function parseCallerSymbol(input) {
62
50
  }
63
51
  return null;
64
52
  }
65
- /** Escape `%`, `_` and `\` for a `LIKE ... ESCAPE '\'` pattern. */
66
53
  function escapeLike(s) {
67
54
  return s.replace(/\\/g, '\\\\').replace(/%/g, '\\%').replace(/_/g, '\\_');
68
55
  }
@@ -1,12 +1,3 @@
1
1
  import type { SymbolEntry } from './interfaces/IAstParser';
2
- export type CodeSymbolRow = Pick<SymbolEntry, 'project' | 'filePath' | 'name' | 'kind' | 'signature' | 'line' | 'col' | 'container' | 'isStatic'>;
3
- /**
4
- * Combine top-level symbols and members into the rows written to `code_symbol`.
5
- * Overload signatures of one declaration — same `(container, name, kind,
6
- * isStatic)` — collapse to a single representative row: the one with a body
7
- * (the implementation) when present, otherwise the earliest by line (the first
8
- * signature in source order). Static and instance members sharing a name are
9
- * never collapsed into each other (`isStatic` is part of the group key). A
10
- * final dedup by `(container, name, kind, line)` matches the DB's unique index.
11
- */
12
- export declare function buildSymbolRows(symbols: SymbolEntry[], members: SymbolEntry[]): CodeSymbolRow[];
2
+ export type CodeSymbolRow = Pick<SymbolEntry, 'project' | 'filePath' | 'name' | 'kind' | 'signature' | 'line' | 'col' | 'container' | 'isStatic' | 'target'>;
3
+ export declare function buildSymbolRows(symbols: SymbolEntry[], members: SymbolEntry[], graphqlNames?: SymbolEntry[]): CodeSymbolRow[];
@@ -1,16 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.buildSymbolRows = buildSymbolRows;
4
- /**
5
- * Combine top-level symbols and members into the rows written to `code_symbol`.
6
- * Overload signatures of one declaration — same `(container, name, kind,
7
- * isStatic)` — collapse to a single representative row: the one with a body
8
- * (the implementation) when present, otherwise the earliest by line (the first
9
- * signature in source order). Static and instance members sharing a name are
10
- * never collapsed into each other (`isStatic` is part of the group key). A
11
- * final dedup by `(container, name, kind, line)` matches the DB's unique index.
12
- */
13
- function buildSymbolRows(symbols, members) {
4
+ function buildSymbolRows(symbols, members, graphqlNames = []) {
14
5
  const groups = new Map();
15
6
  for (const entry of [...symbols, ...members]) {
16
7
  const key = JSON.stringify([entry.container, entry.name, entry.kind, entry.isStatic]);
@@ -30,9 +21,10 @@ function buildSymbolRows(symbols, members) {
30
21
  collapsed.push(implementation ?? group.reduce((a, b) => ((a.line ?? 0) <= (b.line ?? 0) ? a : b)));
31
22
  }
32
23
  const dedup = new Map();
33
- for (const entry of collapsed) {
24
+ for (const entry of [...collapsed, ...graphqlNames]) {
34
25
  const key = JSON.stringify([entry.container, entry.name, entry.kind, entry.line]);
35
- dedup.set(key, entry);
26
+ if (!dedup.has(key))
27
+ dedup.set(key, entry);
36
28
  }
37
- return [...dedup.values()].map(({ project, filePath, name, kind, signature, line, col, container, isStatic }) => ({ project, filePath, name, kind, signature, line, col, container, isStatic }));
29
+ return [...dedup.values()].map(({ project, filePath, name, kind, signature, line, col, container, isStatic, target }) => ({ project, filePath, name, kind, signature, line, col, container, isStatic, target: target ?? null }));
38
30
  }
@@ -0,0 +1,2 @@
1
+ export declare function truncateForLog(value: unknown, depth?: number): unknown;
2
+ export declare function resolveSession(env: Record<string, string | undefined>): string | null;
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.truncateForLog = truncateForLog;
4
+ exports.resolveSession = resolveSession;
5
+ const MAX_STRING = 500;
6
+ const MAX_ITEMS = 20;
7
+ const MAX_DEPTH = 4;
8
+ function truncateForLog(value, depth = 1) {
9
+ if (typeof value === 'string') {
10
+ return value.length > MAX_STRING ? `${value.slice(0, MAX_STRING)}…(+${value.length - MAX_STRING})` : value;
11
+ }
12
+ if (value === null || typeof value !== 'object')
13
+ return value;
14
+ if (depth > MAX_DEPTH)
15
+ return '[depth]';
16
+ if (Array.isArray(value)) {
17
+ const kept = value.slice(0, MAX_ITEMS).map(v => truncateForLog(v, depth + 1));
18
+ return value.length > MAX_ITEMS ? [...kept, `…(+${value.length - MAX_ITEMS})`] : kept;
19
+ }
20
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, truncateForLog(v, depth + 1)]));
21
+ }
22
+ function resolveSession(env) {
23
+ return env.CLAUDE_CODE_SESSION_ID ?? null;
24
+ }
@@ -5,8 +5,14 @@ const format_helpers_1 = require("./format-helpers");
5
5
  const VALID_KINDS = [
6
6
  'function', 'class', 'type', 'interface', 'variable',
7
7
  'method', 'get_accessor', 'set_accessor', 'constructor', 'function_property', 'method_signature',
8
+ 'graphql_query', 'graphql_mutation', 'graphql_subscription', 'graphql_field', 'graphql_type',
8
9
  'all',
9
10
  ];
11
+ const GRAPHQL_KINDS = new Set([
12
+ 'graphql_query', 'graphql_mutation', 'graphql_subscription', 'graphql_field', 'graphql_type',
13
+ ]);
14
+ const GRAPHQL_STALE_INDEX_HINT = 'Files indexed before GraphQL indexing was added have no GraphQL rows until `rag-index --reindex` — ' +
15
+ 'this miss does not prove the name is absent.';
10
16
  class FindSymbolTool {
11
17
  searcher;
12
18
  constructor(searcher) {
@@ -20,7 +26,10 @@ class FindSymbolTool {
20
26
  'properties), interface and type-literal method signatures, and namespace-level declarations. Accepts a bare name ' +
21
27
  '(any container), a qualified name (`Class.method`, `NS.Class.method`), a quoted name for string-literal members ' +
22
28
  "(`Router.'kebab-name'`), or a computed name (`Stack.[Symbol.iterator]`). Use when you know the symbol name and " +
23
- 'want its definition location fast.',
29
+ 'want its definition location fast. Also finds a NestJS GraphQL operation (`@Query`/`@Mutation`/`@Subscription`), ' +
30
+ 'resolve field (`@ResolveField`) or type (`@ObjectType`/`@InputType`/`@InterfaceType`) by its GraphQL name — the ' +
31
+ 'name the schema exposes, which may differ from the TypeScript declaration name. Pass the matching `graphql_*` ' +
32
+ 'kind when the name comes from a schema, a GraphQL document or a frontend.',
24
33
  inputSchema: {
25
34
  type: 'object',
26
35
  properties: {
@@ -45,21 +54,21 @@ class FindSymbolTool {
45
54
  return (0, format_helpers_1.errorResponse)(err.message);
46
55
  }
47
56
  const { hits, truncated, grepFailed } = parsed;
48
- const notice = await this.searcher.outdatedNotice(project);
49
57
  const lines = [];
50
- if (notice)
51
- lines.push(notice + '\n');
52
58
  if (hits.length === 0) {
53
59
  const suffix = grepFailed ? ' (grep fallback failed — see log)' : '';
54
60
  lines.push(`No definitions found for symbol: ${name}${suffix}`);
61
+ if (GRAPHQL_KINDS.has(kind))
62
+ lines.push(GRAPHQL_STALE_INDEX_HINT);
55
63
  return (0, format_helpers_1.textResponse)(lines.join('\n'));
56
64
  }
57
65
  if (truncated)
58
66
  lines.push(`*(truncated to ${hits.length} results)*\n`);
59
67
  for (const h of hits) {
60
68
  const staticTag = h.isStatic ? ' static' : '';
69
+ const targetTag = h.target ? ` → \`${h.target}\`` : '';
61
70
  const grepTag = h.source === 'grep' ? ' (grep — not in index; may be stale or imprecise)' : '';
62
- lines.push(`**${h.kind}** \`${h.qualified}\`${staticTag} \`${h.filePath}\` line ${h.line}, col ${h.column}${grepTag}`);
71
+ lines.push(`**${h.kind}** \`${h.qualified}\`${staticTag}${targetTag} \`${h.filePath}\` line ${h.line}, col ${h.column}${grepTag}`);
63
72
  lines.push(`\`\`\`\n${h.signature}\n\`\`\``);
64
73
  }
65
74
  return (0, format_helpers_1.textResponse)(lines.join('\n'));
@@ -1,8 +1,10 @@
1
1
  import type { IMcpToolHandler, ToolDefinition, ToolResponse } from '../interfaces/IMcpToolHandler';
2
2
  import type { ISearcher } from '../interfaces/ISearcher';
3
+ import type { ILogger } from '../interfaces/ILogger';
3
4
  export declare class GetCallersTool implements IMcpToolHandler {
4
5
  private searcher;
5
- constructor(searcher: ISearcher);
6
+ private logger;
7
+ constructor(searcher: ISearcher, logger: ILogger);
6
8
  definition(): ToolDefinition;
7
9
  handle(args: Record<string, unknown>): Promise<ToolResponse>;
8
10
  }
@@ -4,8 +4,10 @@ exports.GetCallersTool = void 0;
4
4
  const format_helpers_1 = require("./format-helpers");
5
5
  class GetCallersTool {
6
6
  searcher;
7
- constructor(searcher) {
7
+ logger;
8
+ constructor(searcher, logger) {
8
9
  this.searcher = searcher;
10
+ this.logger = logger;
9
11
  }
10
12
  definition() {
11
13
  return {
@@ -20,7 +22,7 @@ class GetCallersTool {
20
22
  properties: {
21
23
  symbol: { type: 'string', description: 'Bare or qualified (Class.method) function/method name (case-sensitive)' },
22
24
  project: { type: 'string', description: 'Project name (from list_projects)' },
23
- callee_file: { type: 'string', description: 'Optional: filter by definition file path (as returned by find_symbol) to disambiguate overloaded names' },
25
+ callee_file: { type: 'string', description: `Optional: only calls to the definition in this file, to disambiguate overloaded names. ${(0, format_helpers_1.pathArgDescription)()}` },
24
26
  },
25
27
  required: ['symbol', 'project'],
26
28
  },
@@ -29,7 +31,18 @@ class GetCallersTool {
29
31
  async handle(args) {
30
32
  const symbol = String(args.symbol ?? '');
31
33
  const project = String(args.project ?? '');
32
- const calleeFile = args.callee_file ? String(args.callee_file) : null;
34
+ const rawCallee = args.callee_file;
35
+ let calleeFile = null;
36
+ if (rawCallee != null && rawCallee !== '') {
37
+ const input = String(rawCallee);
38
+ const resolved = await this.searcher.resolveToolPath(input);
39
+ if (!resolved.ok)
40
+ return (0, format_helpers_1.pathErrorResponse)(this.logger, 'get_callers', 'callee_file', input, input, resolved);
41
+ const failure = await this.searcher.checkGraphPath(resolved.relPath);
42
+ if (failure)
43
+ return (0, format_helpers_1.pathErrorResponse)(this.logger, 'get_callers', 'callee_file', input, resolved.relPath, failure);
44
+ calleeFile = resolved.relPath;
45
+ }
33
46
  let rows;
34
47
  try {
35
48
  rows = await this.searcher.getCallers(symbol, project, calleeFile);
@@ -37,10 +50,7 @@ class GetCallersTool {
37
50
  catch (err) {
38
51
  return (0, format_helpers_1.errorResponse)(err.message);
39
52
  }
40
- const notice = await this.searcher.outdatedNotice(project);
41
53
  const lines = [];
42
- if (notice)
43
- lines.push(notice + '\n');
44
54
  if (rows.length === 0) {
45
55
  lines.push(`No call sites found for \`${symbol}\` in project \`${project}\`. Run \`rag-index --reindex\` if the call graph has not been populated yet.`);
46
56
  return (0, format_helpers_1.textResponse)(lines.join('\n'));
@@ -1,8 +1,10 @@
1
1
  import type { IMcpToolHandler, ToolDefinition, ToolResponse } from '../interfaces/IMcpToolHandler';
2
2
  import type { ISearcher } from '../interfaces/ISearcher';
3
+ import type { ILogger } from '../interfaces/ILogger';
3
4
  export declare class GetDependenciesTool implements IMcpToolHandler {
4
5
  private searcher;
5
- constructor(searcher: ISearcher);
6
+ private logger;
7
+ constructor(searcher: ISearcher, logger: ILogger);
6
8
  definition(): ToolDefinition;
7
9
  handle(args: Record<string, unknown>): Promise<ToolResponse>;
8
10
  }
@@ -4,32 +4,39 @@ exports.GetDependenciesTool = void 0;
4
4
  const format_helpers_1 = require("./format-helpers");
5
5
  class GetDependenciesTool {
6
6
  searcher;
7
- constructor(searcher) {
7
+ logger;
8
+ constructor(searcher, logger) {
8
9
  this.searcher = searcher;
10
+ this.logger = logger;
9
11
  }
10
12
  definition() {
11
13
  return {
12
14
  name: 'get_dependencies',
13
- description: 'Show what files a given file imports (direct or transitive). Use to understand what a file depends on before modifying it. depType values: import | import-default | import-namespace | reexport | reexport-namespace.',
15
+ description: 'Show what files a given file imports (direct or transitive), in any project. Use to understand what a file depends on before modifying it. The direct section prints one line per imported file, with the names imported from it.',
14
16
  inputSchema: {
15
17
  type: 'object',
16
18
  properties: {
17
- file_path: { type: 'string', description: 'File path as returned by search_codebase' },
18
- project: { type: 'string', description: 'Project name (use list_projects to find it)' },
19
+ file_path: { type: 'string', description: (0, format_helpers_1.pathArgDescription)() },
19
20
  symbol: { type: 'string', description: 'Filter to a specific imported symbol name (optional)' },
20
21
  transitive: { type: 'boolean', description: 'Include transitive (indirect) dependencies. Default: false.' },
21
22
  },
22
- required: ['file_path', 'project'],
23
+ required: ['file_path'],
23
24
  },
24
25
  };
25
26
  }
26
27
  async handle(args) {
27
- const filePath = String(args.file_path);
28
- const project = String(args.project);
28
+ const input = args.file_path == null ? '' : String(args.file_path);
29
29
  const symbol = args.symbol ? String(args.symbol) : null;
30
30
  const transitive = Boolean(args.transitive);
31
- const rows = await this.searcher.getDependencies(filePath, project, symbol, transitive);
31
+ const resolved = await this.searcher.resolveToolPath(input);
32
+ if (!resolved.ok)
33
+ return (0, format_helpers_1.pathErrorResponse)(this.logger, 'get_dependencies', 'file_path', input, input, resolved);
34
+ const filePath = resolved.relPath;
35
+ const rows = await this.searcher.getDependencies(filePath, symbol, transitive);
32
36
  if (rows.length === 0) {
37
+ const failure = await this.searcher.checkGraphPath(filePath);
38
+ if (failure)
39
+ return (0, format_helpers_1.pathErrorResponse)(this.logger, 'get_dependencies', 'file_path', input, filePath, failure);
33
40
  return (0, format_helpers_1.textResponse)(`No dependencies found for: ${filePath}`);
34
41
  }
35
42
  return (0, format_helpers_1.textResponse)((0, format_helpers_1.formatDependencies)(filePath, rows, transitive));