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
@@ -2,12 +2,6 @@ import type { Config } from '../config';
2
2
  import type { IEmbedder, InputType } from '../interfaces/IEmbedder';
3
3
  import type { ILogger } from '../interfaces/ILogger';
4
4
  import type { EmbedderDeps } from './OpenAICompatibleEmbedder';
5
- /**
6
- * Voyage AI embedder. Voyage's schema differs from OpenAI's: `input` is an array
7
- * of strings, `input_type` (`query` | `document`) is required for best quality,
8
- * and the optional `output_dimension` requests Matryoshka truncation. Response
9
- * is `{ data: [{ embedding, index }] }`.
10
- */
11
5
  export declare class VoyageEmbedder implements IEmbedder {
12
6
  private config;
13
7
  private logger;
@@ -6,12 +6,6 @@ const config_1 = require("../config");
6
6
  const reranker_1 = require("./reranker");
7
7
  const http_retry_1 = require("./http-retry");
8
8
  const constants_1 = require("../constants");
9
- /**
10
- * Voyage AI embedder. Voyage's schema differs from OpenAI's: `input` is an array
11
- * of strings, `input_type` (`query` | `document`) is required for best quality,
12
- * and the optional `output_dimension` requests Matryoshka truncation. Response
13
- * is `{ data: [{ embedding, index }] }`.
14
- */
15
9
  class VoyageEmbedder {
16
10
  config;
17
11
  logger;
@@ -17,45 +17,17 @@ export interface BackendStatus {
17
17
  finishedAt: string | null;
18
18
  }
19
19
  export interface WebServerDeps {
20
- /** Path to the managed docker-compose.yml. Defaults to `CONFIG_DIR/docker-compose.yml`. */
21
20
  composePath?: string;
22
- /** Injection seams (fetchImpl/runCommand/pollIntervalMs/readinessTimeoutMs) for the readiness primitive. */
23
21
  provisioning?: ProvisioningDeps;
24
- /** Reads the just-saved config back — defaults to `() => new Config()`. Injectable for tests. */
25
22
  readSavedConfig?: () => Config;
26
23
  }
27
- /**
28
- * Per-field choice lists for the config form. `strict` → render a dropdown that
29
- * only accepts a listed value (e.g. the embedding model, tied to the vector
30
- * dimension). Non-strict → a combobox: the values are suggestions but free text
31
- * is still allowed (e.g. any model served by a self-hosted backend).
32
- */
33
24
  export interface FieldOption {
34
25
  values: string[];
35
26
  strict: boolean;
36
27
  }
37
- /** Placeholder shown to the browser instead of a real secret. */
38
28
  export declare const PASSWORD_MASK = "********";
39
- /**
40
- * Merge a config submitted from the browser with the stored one. Any secret
41
- * field (DB password, embedding/reranker API keys) still holding the mask means
42
- * the user did not change it — restore the stored value rather than overwrite it
43
- * with the placeholder. `env:` references are not masked and round-trip as-is.
44
- * Pure — no I/O. Exported for unit testing.
45
- */
46
29
  export declare function mergeConfigForSave(incoming: JsonConfig, existing: JsonConfig | null): JsonConfig;
47
- /**
48
- * Return a copy of the config with raw secrets (DB password, raw API keys)
49
- * replaced by the mask, so real secrets never reach the browser. `env:`
50
- * references are left verbatim — they are not secret and the dashboard shows
51
- * them as "set via <VAR>". The masked value round-trips through
52
- * `mergeConfigForSave`. Pure — no I/O. Exported for unit testing.
53
- */
54
30
  export declare function maskConfig(cfg: JsonConfig): JsonConfig;
55
- /**
56
- * Local HTTP dashboard. Serves a schema-driven config editor, a project
57
- * overview, and the dependency graph. Bind to 127.0.0.1 only — no auth.
58
- */
59
31
  export declare class WebServer {
60
32
  private readonly searcher;
61
33
  private readonly config;
@@ -71,43 +43,19 @@ export declare class WebServer {
71
43
  constructor(searcher: ISearcher, config: Config, logger: ILogger, embedder: IEmbedder, deps?: WebServerDeps);
72
44
  start(port: number, host: string): Promise<void>;
73
45
  stop(): Promise<void>;
74
- /** The bound address once started (e.g. `.port` after listen(0)); null otherwise. */
75
46
  address(): {
76
47
  port: number;
77
48
  } | null;
78
49
  private handle;
79
50
  private maskedConfig;
80
51
  private saveConfig;
81
- /**
82
- * After a successful config write, decide the Infinity backend action per
83
- * the first-match decision table (`local-backend-provisioning`). Computes
84
- * the required set from the SAVED config (a fresh `Config` re-read from the
85
- * file just written — never the server's startup config/embedder). Never
86
- * throws — an unexpected error becomes `{ action: 'failed', message }`; the
87
- * saved config and `GET /api/backend-status` are unaffected either way.
88
- */
89
52
  private evaluateBackendAfterSave;
90
- /** Kick off (or continue, via a follow-up) a background Infinity recreate. */
91
53
  private startBackgroundTask;
92
- /**
93
- * Choice lists for model fields. Both are non-strict comboboxes (free text
94
- * allowed): the embedding model suggests the known registry models plus any
95
- * locally-installed models; the reranker model suggests installed models.
96
- * Backends beyond the registry (e.g. arbitrary HuggingFace models on Infinity)
97
- * still work — their dimension is probed at index time.
98
- */
54
+ private defaults;
99
55
  private fieldOptions;
100
56
  private installedModels;
101
- /** Tabbed graph views (Workspace + one per nested Nx repo). */
102
57
  private graphViews;
103
- /** `/api/graph` — the Workspace view's raw `{ nodes, edges }` (back-compat). */
104
58
  private graphData;
105
- /**
106
- * The full interactive graph page — the same HTML as the standalone
107
- * `project-graph.html` (inner tabs, filters, legend, project toggles). The
108
- * dashboard's Graph tab embeds this via an iframe rather than re-implementing
109
- * vis-network.
110
- */
111
59
  private graphPage;
112
60
  private readBody;
113
61
  private json;
@@ -47,20 +47,11 @@ const infinity_provisioning_1 = require("./infinity-provisioning");
47
47
  function sameModelSet(a, b) {
48
48
  return a.join(';') === b.join(';');
49
49
  }
50
- /** Placeholder shown to the browser instead of a real secret. */
51
50
  exports.PASSWORD_MASK = '********';
52
- const MAX_BODY = 1_000_000; // 1 MB — config payloads are tiny
53
- /** A secret worth masking: a non-empty string that is not an `env:` reference. */
51
+ const MAX_BODY = 1_000_000;
54
52
  function isRawSecret(v) {
55
53
  return typeof v === 'string' && v !== '' && !v.startsWith('env:');
56
54
  }
57
- /**
58
- * Merge a config submitted from the browser with the stored one. Any secret
59
- * field (DB password, embedding/reranker API keys) still holding the mask means
60
- * the user did not change it — restore the stored value rather than overwrite it
61
- * with the placeholder. `env:` references are not masked and round-trip as-is.
62
- * Pure — no I/O. Exported for unit testing.
63
- */
64
55
  function mergeConfigForSave(incoming, existing) {
65
56
  const merged = JSON.parse(JSON.stringify(incoming ?? {}));
66
57
  const restore = (grp, field) => {
@@ -78,13 +69,6 @@ function mergeConfigForSave(incoming, existing) {
78
69
  restore('reranker', 'apiKey');
79
70
  return merged;
80
71
  }
81
- /**
82
- * Return a copy of the config with raw secrets (DB password, raw API keys)
83
- * replaced by the mask, so real secrets never reach the browser. `env:`
84
- * references are left verbatim — they are not secret and the dashboard shows
85
- * them as "set via <VAR>". The masked value round-trips through
86
- * `mergeConfigForSave`. Pure — no I/O. Exported for unit testing.
87
- */
88
72
  function maskConfig(cfg) {
89
73
  const clone = JSON.parse(JSON.stringify(cfg ?? {}));
90
74
  if (clone.db?.password)
@@ -95,10 +79,6 @@ function maskConfig(cfg) {
95
79
  clone.reranker.apiKey = exports.PASSWORD_MASK;
96
80
  return clone;
97
81
  }
98
- /**
99
- * Local HTTP dashboard. Serves a schema-driven config editor, a project
100
- * overview, and the dependency graph. Bind to 127.0.0.1 only — no auth.
101
- */
102
82
  class WebServer {
103
83
  searcher;
104
84
  config;
@@ -108,8 +88,6 @@ class WebServer {
108
88
  composePath;
109
89
  provisioningDeps;
110
90
  readSavedConfig;
111
- // Background Infinity-recreate task state (D6). In-memory, per-process — see
112
- // `GET /api/backend-status`. `pending` holds at most one queued follow-up.
113
91
  backendStatus = { state: 'idle', message: null, startedAt: null, finishedAt: null };
114
92
  taskInFlight = false;
115
93
  pending = null;
@@ -125,7 +103,7 @@ class WebServer {
125
103
  async start(port, host) {
126
104
  this.server = http.createServer((req, res) => {
127
105
  this.handle(req, res).catch(err => {
128
- this.logger.error('WebServer: request failed', { reason: err instanceof Error ? err.message : String(err) });
106
+ this.logger.error('WebServer: request failed', { err });
129
107
  if (!res.headersSent) {
130
108
  res.writeHead(500);
131
109
  res.end('Internal error');
@@ -144,7 +122,6 @@ class WebServer {
144
122
  await new Promise(resolve => this.server.close(() => resolve()));
145
123
  this.server = undefined;
146
124
  }
147
- /** The bound address once started (e.g. `.port` after listen(0)); null otherwise. */
148
125
  address() {
149
126
  const a = this.server?.address();
150
127
  return a && typeof a === 'object' ? { port: a.port } : null;
@@ -158,7 +135,7 @@ class WebServer {
158
135
  if (m === 'GET' && p === '/api/schema')
159
136
  return this.json(res, 200, (0, config_store_1.buildJsonSchema)());
160
137
  if (m === 'GET' && p === '/api/defaults')
161
- return this.json(res, 200, config_store_1.CONFIG_DEFAULTS);
138
+ return this.json(res, 200, this.defaults());
162
139
  if (m === 'GET' && p === '/api/field-options')
163
140
  return this.json(res, 200, await this.fieldOptions());
164
141
  if (m === 'GET' && p === '/api/config')
@@ -184,7 +161,6 @@ class WebServer {
184
161
  body = await this.readBody(req);
185
162
  }
186
163
  catch (e) {
187
- // Distinguish the size limit from other stream errors.
188
164
  return this.json(res, 413, { error: e instanceof Error ? e.message : 'Request body too large' });
189
165
  }
190
166
  let incoming;
@@ -207,14 +183,6 @@ class WebServer {
207
183
  const backend = await this.evaluateBackendAfterSave(validated);
208
184
  this.json(res, 200, { ok: true, backend });
209
185
  }
210
- /**
211
- * After a successful config write, decide the Infinity backend action per
212
- * the first-match decision table (`local-backend-provisioning`). Computes
213
- * the required set from the SAVED config (a fresh `Config` re-read from the
214
- * file just written — never the server's startup config/embedder). Never
215
- * throws — an unexpected error becomes `{ action: 'failed', message }`; the
216
- * saved config and `GET /api/backend-status` are unaffected either way.
217
- */
218
186
  async evaluateBackendAfterSave(saved) {
219
187
  const provider = saved.embedding?.provider ?? config_store_1.CONFIG_DEFAULTS['embedding.provider'];
220
188
  if (provider !== 'infinity') {
@@ -232,7 +200,6 @@ class WebServer {
232
200
  this.pending = { modelSet, baseURL, composePath };
233
201
  return { action: 'already-running', message: 'A backend recreate is already in progress; this configuration will be applied next.' };
234
202
  }
235
- // No longer targets the managed container the in-flight task owns.
236
203
  this.pending = null;
237
204
  }
238
205
  const evaluation = await (0, infinity_provisioning_1.evaluateBackend)(baseURL, composePath, modelSet, this.provisioningDeps);
@@ -252,7 +219,6 @@ class WebServer {
252
219
  return { action: 'failed', message: err instanceof Error ? err.message : String(err) };
253
220
  }
254
221
  }
255
- /** Kick off (or continue, via a follow-up) a background Infinity recreate. */
256
222
  startBackgroundTask(baseURL, composePath, modelSet) {
257
223
  this.taskInFlight = true;
258
224
  this.backendStatus = { state: 'recreating', message: null, startedAt: new Date().toISOString(), finishedAt: null };
@@ -276,19 +242,17 @@ class WebServer {
276
242
  }
277
243
  });
278
244
  }
279
- /**
280
- * Choice lists for model fields. Both are non-strict comboboxes (free text
281
- * allowed): the embedding model suggests the known registry models plus any
282
- * locally-installed models; the reranker model suggests installed models.
283
- * Backends beyond the registry (e.g. arbitrary HuggingFace models on Infinity)
284
- * still work — their dimension is probed at index time.
285
- */
245
+ defaults() {
246
+ const ctx = (0, config_1.rerankContext)((0, config_store_1.readConfigJson)() ?? {});
247
+ return { ...config_store_1.CONFIG_DEFAULTS, 'reranker.enabled': ctx.wanted && !ctx.missingKey, 'reranker.model': ctx.model };
248
+ }
286
249
  async fieldOptions() {
287
250
  const installed = await this.installedModels();
288
251
  const registryModels = Object.keys(constants_1.MODEL_REGISTRY);
252
+ const rerankModels = (0, config_store_1.readConfigJson)()?.reranker?.provider === 'voyage' ? [...constants_1.VOYAGE_RERANK_MODELS] : installed;
289
253
  return {
290
254
  'embedding.model': { values: [...new Set([...registryModels, ...installed])], strict: false },
291
- 'reranker.model': { values: installed, strict: false },
255
+ 'reranker.model': { values: rerankModels, strict: false },
292
256
  };
293
257
  }
294
258
  async installedModels() {
@@ -296,32 +260,22 @@ class WebServer {
296
260
  return (await this.embedder.listModels?.()) ?? [];
297
261
  }
298
262
  catch (err) {
299
- this.logger.warn('WebServer: could not list models', { reason: err instanceof Error ? err.message : String(err) });
263
+ this.logger.warn('WebServer: could not list models', { err });
300
264
  return [];
301
265
  }
302
266
  }
303
- /** Tabbed graph views (Workspace + one per nested Nx repo). */
304
267
  async graphViews() {
305
268
  const projects = (await this.searcher.listProjects()).map(p => ({
306
269
  name: p.serviceName,
307
- // Real project root (resolved via discovery — handles nested-monorepo
308
- // names). Fall back to the flat assumption if listProjects omits it.
309
270
  path: p.path ?? path.join(this.config.workspaceRoot, p.serviceName),
310
271
  }));
311
272
  const projectEdges = await this.searcher.getProjectEdges();
312
273
  return (0, graph_1.buildGraphViews)(projects, projectEdges, this.config.workspaceRoot);
313
274
  }
314
- /** `/api/graph` — the Workspace view's raw `{ nodes, edges }` (back-compat). */
315
275
  async graphData() {
316
276
  const views = await this.graphViews();
317
277
  return { nodes: views[0].nodes, edges: views[0].edges };
318
278
  }
319
- /**
320
- * The full interactive graph page — the same HTML as the standalone
321
- * `project-graph.html` (inner tabs, filters, legend, project toggles). The
322
- * dashboard's Graph tab embeds this via an iframe rather than re-implementing
323
- * vis-network.
324
- */
325
279
  async graphPage() {
326
280
  return (0, graph_1.buildHtml)(await this.graphViews());
327
281
  }
@@ -331,7 +285,7 @@ class WebServer {
331
285
  let tooLarge = false;
332
286
  req.on('data', chunk => {
333
287
  if (tooLarge)
334
- return; // stop growing `data`; keep the socket alive so the 413 flushes
288
+ return;
335
289
  data += chunk;
336
290
  if (data.length > MAX_BODY) {
337
291
  tooLarge = true;
@@ -4,16 +4,9 @@ export declare class HttpError extends Error {
4
4
  constructor(status: number, message: string);
5
5
  }
6
6
  export interface RetryOpts {
7
- /** Total attempts including the first (default 4). */
8
7
  attempts?: number;
9
- /** Base backoff in ms; grows exponentially (default 500). Set 0 in tests. */
10
8
  baseDelayMs?: number;
11
9
  logger?: ILogger;
12
10
  label?: string;
13
11
  }
14
- /**
15
- * Run `fn` with bounded exponential backoff on HTTP 429/5xx and network errors.
16
- * Re-throws the last error once attempts are exhausted or the error is not
17
- * transient.
18
- */
19
12
  export declare function withRetry<T>(fn: () => Promise<T>, opts?: RetryOpts): Promise<T>;
@@ -14,22 +14,13 @@ exports.HttpError = HttpError;
14
14
  function sleep(ms) {
15
15
  return ms > 0 ? new Promise(r => setTimeout(r, ms)) : Promise.resolve();
16
16
  }
17
- /** True for transient failures worth retrying (rate limit / server error / network / timeout). */
18
17
  function isRetryable(err) {
19
18
  if (err instanceof HttpError)
20
19
  return err.status === 429 || (err.status >= 500 && err.status <= 599);
21
- // A request timeout (AbortSignal.timeout) rejects with a DOMException named
22
- // 'TimeoutError' — the most common transient cloud failure, so retry it.
23
20
  if (err != null && typeof err === 'object' && err.name === 'TimeoutError')
24
21
  return true;
25
- // Network-level errors (fetch throws) are transient too.
26
22
  return err instanceof TypeError;
27
23
  }
28
- /**
29
- * Run `fn` with bounded exponential backoff on HTTP 429/5xx and network errors.
30
- * Re-throws the last error once attempts are exhausted or the error is not
31
- * transient.
32
- */
33
24
  async function withRetry(fn, opts = {}) {
34
25
  const attempts = opts.attempts ?? 4;
35
26
  const base = opts.baseDelayMs ?? 500;
@@ -45,7 +36,7 @@ async function withRetry(fn, opts = {}) {
45
36
  const delay = base * Math.pow(2, attempt - 1);
46
37
  opts.logger?.warn('http-retry: transient failure, retrying', {
47
38
  label: opts.label, attempt, attempts, delayMs: delay,
48
- reason: err instanceof Error ? err.message : String(err),
39
+ err,
49
40
  });
50
41
  await sleep(delay);
51
42
  }
@@ -1,115 +1,41 @@
1
1
  import type { Config } from '../config';
2
- /** Result of a shell command run, mirroring the parts of `spawnSync` we use. */
3
2
  export interface CommandResult {
4
3
  status: number | null;
5
4
  stdout?: string;
6
5
  stderr?: string;
7
6
  }
8
- /** Injection seams for the provisioning primitive. All optional, defaulted to the real implementations. */
9
7
  export interface ProvisioningDeps {
10
8
  fetchImpl?: typeof fetch;
11
- /** Runs a shell command (docker). Defaults to `spawnSync` with captured output. */
12
9
  runCommand?: (cmd: string, args: string[]) => CommandResult;
13
- /** Sleep between readiness polls (ms), capped at 5000. Defaults to 2000; tests pass 0. */
14
10
  pollIntervalMs?: number;
15
- /** Readiness wait timeout (ms). Defaults to 1,800,000 (30 min); tests override to run fast. */
16
11
  readinessTimeoutMs?: number;
17
12
  }
18
- /**
19
- * Strip trailing slashes and a trailing `/embeddings` or `/v1` to get the server
20
- * root — Infinity serves `/health`, `/models`, `/rerank`, `/embeddings` from the
21
- * root, so a baseURL written with an OpenAI-style suffix must be normalized here
22
- * to keep the probe/models URLs aligned with the embeddings URL.
23
- */
24
13
  export declare function serverRoot(baseURL: string): string;
25
- /**
26
- * Probe an Infinity-style `/health` endpoint. Resolves `true` when the server
27
- * answers with a 2xx, `false` on any error, non-2xx, or timeout.
28
- */
29
14
  export declare function probeInfinityHealth(baseURL: string, fetchImpl?: typeof fetch, timeoutMs?: number): Promise<boolean>;
30
- /**
31
- * List the model ids an Infinity server currently serves via its OpenAI-style
32
- * `/models` listing (`{ data: [{ id }] }`). Returns `[]` on any error, non-2xx,
33
- * or timeout — callers treat an empty/unverifiable listing as "served".
34
- */
35
15
  export declare function listInfinityModels(baseURL: string, fetchImpl?: typeof fetch, timeoutMs?: number): Promise<string[]>;
36
- /**
37
- * Two URLs target the same server when scheme, (loopback-normalised) host, and
38
- * port (explicit or scheme default) all match; path is ignored. An unparsable
39
- * URL is never the same server as another.
40
- */
41
16
  export declare function sameServer(a: string, b: string): boolean;
42
- /**
43
- * "Managed" per the managed-vs-external requirement: the compose file exists,
44
- * declares an `infinity` service, the base URL host is loopback, and its port
45
- * (explicit or scheme default) equals the service's published host port.
46
- * Never throws — an unreadable/unparsable file or an unrecognised port form
47
- * is simply "external".
48
- */
49
17
  export declare function isManagedInfinity(baseURL: string, composePath: string): boolean;
50
- /** The literal (non-`${…}`) `INFINITY_MODEL_ID` value in the compose file's `infinity` service, or null. */
51
18
  export declare function extractModelIdValue(composeContent: string): string | null;
52
- /** Write `content` to `targetPath` via a same-directory temp file + rename; removes the temp file on failure. */
53
19
  export declare function atomicWriteFile(targetPath: string, content: string, mode: number): void;
54
- /**
55
- * Write `modelSet` (embedding model first) as the literal `INFINITY_MODEL_ID`
56
- * of the compose file's `infinity` service, and remove a `profiles:` entry of
57
- * that service if present (a file written by an earlier version). At most
58
- * those two targeted edits — every other byte unchanged. Atomic (temp file +
59
- * rename), keeps the original file's mode. Throws (no write, original file
60
- * unchanged) when the file cannot be read, has no `infinity` service, has no
61
- * `INFINITY_MODEL_ID:` line, or the write itself fails.
62
- */
63
20
  export declare function writeComposeModelSet(composePath: string, modelSet: string[]): void;
64
- /** True when an NVIDIA GPU + `nvidia-smi` is available on this host. */
65
21
  export declare function hasNvidiaGpu(): boolean;
66
- /** The `-f` file args a docker compose invocation against `composePath` should use (base, plus GPU override when present + detected). */
67
22
  export declare function composeFileArgs(composePath: string): string[];
68
23
  export type BackendAction = 'served' | 'missing-managed' | 'missing-external' | 'unreachable-managed' | 'unreachable-external';
69
24
  export interface BackendEvaluation {
70
25
  action: BackendAction;
71
26
  managed: boolean;
72
- /** Required models absent from the served listing; empty for `served` or an unverifiable listing. */
73
27
  missingModels: string[];
74
- /** The full served listing at evaluation time; empty for an unverifiable listing or when unreachable. */
75
28
  servedModels: string[];
76
29
  }
77
- /**
78
- * Classify the backend against `requiredModels` without touching Docker:
79
- * reachable+served (or unverifiable listing) → `served`; reachable+missing →
80
- * `missing-managed`/`missing-external`; unreachable → `unreachable-managed`/
81
- * `unreachable-external`. Shared by `ensureBackendReady`, the web-dashboard
82
- * decision table, and `rag-setup`'s declined-Docker branch.
83
- */
84
30
  export declare function evaluateBackend(baseURL: string, composePath: string, requiredModels: string[], deps?: ProvisioningDeps): Promise<BackendEvaluation>;
85
- /** Human-readable manual instructions for a `missing-external`/`unreachable-external` evaluation. */
86
31
  export declare function manualInstructionMessage(evaluation: BackendEvaluation, base: string, modelSetToWrite: string[]): string;
87
32
  export interface EnsureBackendReadyOpts {
88
33
  baseURL: string;
89
- /** Models that must be served for the call to succeed without a recreate/instructions. */
90
34
  requiredModels: string[];
91
- /** Full model set to write into the compose file and bind at startup when a start/recreate is needed. */
92
35
  modelSetToWrite: string[];
93
36
  composePath: string;
94
37
  deps?: ProvisioningDeps;
95
38
  }
96
- /**
97
- * Bring the Infinity backend to a state serving `requiredModels`, per the
98
- * managed-vs-external decision tree:
99
- * served → return (no Docker)
100
- * missing-managed → recreate (writes modelSetToWrite) + wait
101
- * missing-external → throw (manual --model-id instructions)
102
- * unreachable-managed → (re)create + wait
103
- * unreachable-external → throw (manual launch instructions)
104
- */
105
39
  export declare function ensureBackendReady(opts: EnsureBackendReadyOpts): Promise<void>;
106
- /** `ensureBackendReady` with the full set as both the requirement and the write (rag-setup, web dashboard). */
107
40
  export declare function ensureModelSetServed(baseURL: string, composePath: string, modelSet: string[], deps?: ProvisioningDeps): Promise<void>;
108
- /**
109
- * The model set a managed Infinity container must serve, computed from a
110
- * `Config`: the embedding model, followed by the reranker model only when
111
- * reranking is enabled with a model on the same server as embeddings (the
112
- * effective `reranker.url`, which `Config` has already resolved from an
113
- * explicit value or derived from `reranker.baseURL`).
114
- */
115
41
  export declare function requiredModelSet(cfg: Config): string[];