codesearch-mcp 0.2.3-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 (141) hide show
  1. package/CHANGELOG.md +31 -1
  2. package/README.md +46 -26
  3. package/dist/app.js +5 -19
  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 +4 -3
  26. package/dist/src/commands/EvalCommand.js +85 -52
  27. package/dist/src/commands/IndexCommand.js +0 -22
  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/schema.d.ts +17 -0
  41. package/dist/src/db/schema.js +1 -6
  42. package/dist/src/declarations/DeclarationTree.d.ts +19 -0
  43. package/dist/src/declarations/DeclarationTree.js +66 -0
  44. package/dist/src/declarations/DeclarationTreeBuilder.d.ts +22 -0
  45. package/dist/src/declarations/DeclarationTreeBuilder.js +179 -0
  46. package/dist/src/declarations/SourceLines.d.ts +37 -0
  47. package/dist/src/declarations/SourceLines.js +415 -0
  48. package/dist/src/dependency-traversal.d.ts +0 -6
  49. package/dist/src/dependency-traversal.js +0 -6
  50. package/dist/src/enriched-prompt.d.ts +0 -6
  51. package/dist/src/enriched-prompt.js +0 -6
  52. package/dist/src/error-record.d.ts +12 -0
  53. package/dist/src/error-record.js +248 -0
  54. package/dist/src/eval-corpus.d.ts +16 -0
  55. package/dist/src/eval-corpus.js +169 -0
  56. package/dist/src/eval-results.d.ts +6 -3
  57. package/dist/src/eval-results.js +309 -26
  58. package/dist/src/file-utils.d.ts +0 -5
  59. package/dist/src/file-utils.js +0 -5
  60. package/dist/src/graph.d.ts +0 -25
  61. package/dist/src/graph.js +0 -28
  62. package/dist/src/hnsw-settings.d.ts +3 -0
  63. package/dist/src/hnsw-settings.js +24 -0
  64. package/dist/src/interfaces/IAstParser.d.ts +2 -39
  65. package/dist/src/interfaces/IChunker.d.ts +3 -12
  66. package/dist/src/interfaces/ICorpusInspector.d.ts +7 -0
  67. package/dist/src/interfaces/ICorpusInspector.js +2 -0
  68. package/dist/src/interfaces/IDatabase.d.ts +2 -22
  69. package/dist/src/interfaces/IEmbedder.d.ts +0 -9
  70. package/dist/src/interfaces/ILogger.d.ts +1 -11
  71. package/dist/src/interfaces/IMcpToolHandler.d.ts +0 -5
  72. package/dist/src/interfaces/IMonorepoResolver.d.ts +0 -22
  73. package/dist/src/interfaces/IProjectDiscovery.d.ts +0 -17
  74. package/dist/src/interfaces/IRetrievalEvaluator.d.ts +134 -12
  75. package/dist/src/interfaces/ISearcher.d.ts +16 -5
  76. package/dist/src/interrupt.js +0 -2
  77. package/dist/src/owner-path.d.ts +9 -10
  78. package/dist/src/owner-path.js +11 -23
  79. package/dist/src/project-scope.d.ts +0 -12
  80. package/dist/src/project-scope.js +0 -12
  81. package/dist/src/project-selector.d.ts +0 -5
  82. package/dist/src/project-selector.js +0 -11
  83. package/dist/src/retrieval-eval.d.ts +14 -6
  84. package/dist/src/retrieval-eval.js +345 -59
  85. package/dist/src/search/result-shaping.d.ts +4 -0
  86. package/dist/src/search/result-shaping.js +19 -0
  87. package/dist/src/services/AstParserService.d.ts +2 -62
  88. package/dist/src/services/AstParserService.js +25 -137
  89. package/dist/src/services/DatabaseService.d.ts +2 -7
  90. package/dist/src/services/DatabaseService.js +42 -29
  91. package/dist/src/services/EmbedderFactory.d.ts +0 -4
  92. package/dist/src/services/EmbedderFactory.js +0 -5
  93. package/dist/src/services/IndexerService.d.ts +0 -16
  94. package/dist/src/services/IndexerService.js +16 -45
  95. package/dist/src/services/InfinityEmbedder.d.ts +0 -21
  96. package/dist/src/services/InfinityEmbedder.js +0 -15
  97. package/dist/src/services/LoggerService.d.ts +7 -5
  98. package/dist/src/services/LoggerService.js +37 -41
  99. package/dist/src/services/McpServer.d.ts +22 -14
  100. package/dist/src/services/McpServer.js +70 -37
  101. package/dist/src/services/MonorepoAwareDiscoveryService.d.ts +1 -45
  102. package/dist/src/services/MonorepoAwareDiscoveryService.js +1 -51
  103. package/dist/src/services/NxMonorepoResolver.d.ts +0 -29
  104. package/dist/src/services/NxMonorepoResolver.js +5 -56
  105. package/dist/src/services/OpenAICompatibleEmbedder.d.ts +0 -6
  106. package/dist/src/services/OpenAICompatibleEmbedder.js +0 -14
  107. package/dist/src/services/ProjectDiscoveryService.d.ts +0 -4
  108. package/dist/src/services/ProjectDiscoveryService.js +0 -4
  109. package/dist/src/services/SearcherService.d.ts +9 -116
  110. package/dist/src/services/SearcherService.js +95 -176
  111. package/dist/src/services/VoyageEmbedder.d.ts +0 -6
  112. package/dist/src/services/VoyageEmbedder.js +0 -6
  113. package/dist/src/services/WebServerService.d.ts +1 -53
  114. package/dist/src/services/WebServerService.js +11 -57
  115. package/dist/src/services/http-retry.d.ts +0 -7
  116. package/dist/src/services/http-retry.js +1 -10
  117. package/dist/src/services/infinity-provisioning.d.ts +0 -74
  118. package/dist/src/services/infinity-provisioning.js +5 -88
  119. package/dist/src/services/legacy-ollama-index.d.ts +0 -16
  120. package/dist/src/services/legacy-ollama-index.js +0 -23
  121. package/dist/src/services/reranker.d.ts +0 -9
  122. package/dist/src/services/reranker.js +46 -12
  123. package/dist/src/setup.d.ts +11 -23
  124. package/dist/src/setup.js +98 -71
  125. package/dist/src/symbol-name.d.ts +0 -12
  126. package/dist/src/symbol-name.js +0 -13
  127. package/dist/src/symbol-rows.d.ts +0 -14
  128. package/dist/src/symbol-rows.js +0 -14
  129. package/dist/src/tool-call-log.d.ts +2 -0
  130. package/dist/src/tool-call-log.js +24 -0
  131. package/dist/src/tools/GetDependenciesTool.js +1 -1
  132. package/dist/src/tools/GetDependentsTool.js +1 -1
  133. package/dist/src/tools/SearchCodebaseTool.js +6 -3
  134. package/dist/src/tools/format-helpers.d.ts +0 -6
  135. package/dist/src/tools/format-helpers.js +87 -14
  136. package/dist/src/web/dashboard.d.ts +0 -8
  137. package/dist/src/web/dashboard.js +7 -10
  138. package/package.json +2 -2
  139. package/dist/src/services/AstChunker.d.ts +0 -42
  140. package/dist/src/services/AstChunker.js +0 -158
  141. 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,17 +1,3 @@
1
1
  import type { SymbolEntry } from './interfaces/IAstParser';
2
2
  export type CodeSymbolRow = Pick<SymbolEntry, 'project' | 'filePath' | 'name' | 'kind' | 'signature' | 'line' | 'col' | 'container' | 'isStatic' | 'target'>;
3
- /**
4
- * Combine top-level symbols, members and GraphQL-named rows into the rows
5
- * written to `code_symbol`. Overload signatures of one declaration — same
6
- * `(container, name, kind, isStatic)` — collapse to a single representative
7
- * row: the one with a body (the implementation) when present, otherwise the
8
- * earliest by line (the first signature in source order). Static and instance
9
- * members sharing a name are never collapsed into each other (`isStatic` is
10
- * part of the group key). `graphqlNames` bypass this collapse — decorators
11
- * cannot sit on overload signatures, so there is nothing to collapse, and the
12
- * collapse's `(container, name, kind, isStatic)` key would wrongly merge two
13
- * different methods exposing the same GraphQL name from two resolver classes
14
- * (both with container `Query`). A final dedup by `(container, name, kind,
15
- * line)` matches the DB's unique index.
16
- */
17
3
  export declare function buildSymbolRows(symbols: SymbolEntry[], members: SymbolEntry[], graphqlNames?: SymbolEntry[]): CodeSymbolRow[];
@@ -1,20 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.buildSymbolRows = buildSymbolRows;
4
- /**
5
- * Combine top-level symbols, members and GraphQL-named rows into the rows
6
- * written to `code_symbol`. Overload signatures of one declaration — same
7
- * `(container, name, kind, isStatic)` — collapse to a single representative
8
- * row: the one with a body (the implementation) when present, otherwise the
9
- * earliest by line (the first signature in source order). Static and instance
10
- * members sharing a name are never collapsed into each other (`isStatic` is
11
- * part of the group key). `graphqlNames` bypass this collapse — decorators
12
- * cannot sit on overload signatures, so there is nothing to collapse, and the
13
- * collapse's `(container, name, kind, isStatic)` key would wrongly merge two
14
- * different methods exposing the same GraphQL name from two resolver classes
15
- * (both with container `Query`). A final dedup by `(container, name, kind,
16
- * line)` matches the DB's unique index.
17
- */
18
4
  function buildSymbolRows(symbols, members, graphqlNames = []) {
19
5
  const groups = new Map();
20
6
  for (const entry of [...symbols, ...members]) {
@@ -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
+ }
@@ -12,7 +12,7 @@ class GetDependenciesTool {
12
12
  definition() {
13
13
  return {
14
14
  name: 'get_dependencies',
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. depType values: import | import-default | import-namespace | import-side-effect | 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.',
16
16
  inputSchema: {
17
17
  type: 'object',
18
18
  properties: {
@@ -12,7 +12,7 @@ class GetDependentsTool {
12
12
  definition() {
13
13
  return {
14
14
  name: 'get_dependents',
15
- description: 'Show which files import a given file (reverse graph), in any project. Use before refactoring to find all callers and avoid breaking changes. depType values: import | import-default | import-namespace | import-side-effect | reexport | reexport-namespace.',
15
+ description: 'Show which files import a given file (reverse graph), in any project. Use before refactoring to find all callers and avoid breaking changes. The direct section prints one line per importing file, with the names imported and the enclosing declarations that use them — only those resolved unambiguously, so the list can be partial or absent.',
16
16
  inputSchema: {
17
17
  type: 'object',
18
18
  properties: {
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SearchCodebaseTool = void 0;
4
4
  const format_helpers_1 = require("./format-helpers");
5
+ const result_shaping_1 = require("../search/result-shaping");
5
6
  class SearchCodebaseTool {
6
7
  searcher;
7
8
  defaultLimit;
@@ -28,7 +29,8 @@ class SearchCodebaseTool {
28
29
  include_neighbors: { type: 'boolean', description: 'Also include the chunk before and after each result from the same file (Small-to-Big retrieval). Useful when results look truncated.' },
29
30
  include_graph_context: { type: 'boolean', description: 'Append direct imports and dependents list to each result. Shows "Imports: X · Y" and "Used by: Z" without extra tool calls.' },
30
31
  threshold: { type: 'number', description: `Max cosine distance (0–1). Lower = stricter. Default: ${this.defaultThreshold}. Use 0.40–0.45 for precise technical queries, 0.55–0.60 for broad exploration.` },
31
- rerank: { type: 'boolean', description: 'Override whether this call reranks, regardless of config. false skips reranking (use for "what does X do" / "where is X" lookups described without an exact name — reranking measurably hurts these; if you know the exact symbol name, use find_symbol instead). true forces reranking on if a reranker is configured (no-op otherwise). Omit to use config.reranker.enabled (good default for conceptual/architectural queries).' },
32
+ per_file: { type: 'number', description: `Max results from one file (integer ≥ 1, default ${result_shaping_1.DEFAULT_PER_FILE}). Skipped results are backfilled from the next candidates. Raise it when one file is expected to hold several answers (e.g. several methods of one class).` },
33
+ rerank: { type: 'boolean', description: 'Override whether this call reranks, regardless of config. false skips reranking (use for "what does X do" / "where is X" lookups described without an exact name — reranking measurably hurts these; if you know the exact symbol name, use find_symbol instead). true forces reranking on if a reranker is configured (no-op otherwise). Omit to use config.reranker.enabled (fine for conceptual/architectural queries, where reranking is roughly neutral).' },
32
34
  },
33
35
  required: ['query'],
34
36
  },
@@ -47,11 +49,12 @@ class SearchCodebaseTool {
47
49
  ? Math.min(1, Math.max(0, parsedThreshold))
48
50
  : undefined;
49
51
  const rerank = typeof args.rerank === 'boolean' ? args.rerank : undefined;
52
+ const perFile = Number.isInteger(args.per_file) && args.per_file >= 1 ? args.per_file : result_shaping_1.DEFAULT_PER_FILE;
50
53
  const { query, translated } = await this.searcher.prepareQuery(raw);
51
- const results = await this.searcher.searchChunks(query, limit, project, threshold, rerank);
54
+ const results = await this.searcher.searchChunks(query, limit, project, threshold, rerank, perFile);
52
55
  this.logger.debug('SearchCodebaseTool: handle', {
53
56
  query: raw.slice(0, 120), project, threshold: threshold ?? this.defaultThreshold,
54
- limit, translated, count: results.length, ms: Date.now() - t0,
57
+ limit, perFile, translated, count: results.length, ms: Date.now() - t0,
55
58
  });
56
59
  if (results.length === 0) {
57
60
  return (0, format_helpers_1.textResponse)('No relevant results found.');
@@ -9,15 +9,9 @@ export declare function formatDependencies(filePath: string, rows: DependencyRow
9
9
  export declare function formatDependents(filePath: string, rows: DependencyRow[], transitive: boolean): string;
10
10
  export type PathKind = 'file' | 'directory';
11
11
  export declare const PATH_EXAMPLE: Record<PathKind, string>;
12
- /** Description of a path argument; `kind` names what the path points at. */
13
12
  export declare function pathArgDescription(kind?: PathKind): string;
14
13
  export type PathError = PathRefusal | PathFailure | Extract<FileRangeResult, {
15
14
  reason: 'past_end' | 'bad_range';
16
15
  }>;
17
- /**
18
- * Text of a path error. `input` is the raw argument (echoed by refusals);
19
- * `relPath` its canonical form, printed by every other message.
20
- */
21
16
  export declare function pathErrorText(argName: string, input: string, relPath: string, err: PathError, kind?: PathKind): string;
22
- /** Error response for a path error, logged at `warn` — a caller or index-freshness problem, not a server fault. */
23
17
  export declare function pathErrorResponse(logger: ILogger, tool: string, argName: string, input: string, relPath: string, err: PathError, kind?: PathKind): ToolResponse;