codesearch-mcp 0.1.0-alpha → 0.2.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 (44) hide show
  1. package/CHANGELOG.md +24 -1
  2. package/README.md +141 -39
  3. package/dist/app.js +2 -2
  4. package/dist/src/commands/IndexCommand.js +42 -1
  5. package/dist/src/config-error.d.ts +7 -0
  6. package/dist/src/config-error.js +14 -0
  7. package/dist/src/config-store.d.ts +30 -12
  8. package/dist/src/config-store.js +81 -26
  9. package/dist/src/config.d.ts +5 -4
  10. package/dist/src/config.js +14 -16
  11. package/dist/src/constants.d.ts +8 -2
  12. package/dist/src/constants.js +8 -5
  13. package/dist/src/help.js +1 -1
  14. package/dist/src/interfaces/IDatabase.d.ts +17 -0
  15. package/dist/src/interfaces/ILogger.d.ts +7 -0
  16. package/dist/src/services/AstChunker.d.ts +11 -0
  17. package/dist/src/services/AstChunker.js +44 -0
  18. package/dist/src/services/DatabaseService.d.ts +4 -0
  19. package/dist/src/services/DatabaseService.js +46 -0
  20. package/dist/src/services/EmbedderFactory.d.ts +1 -1
  21. package/dist/src/services/EmbedderFactory.js +2 -5
  22. package/dist/src/services/IndexerService.d.ts +4 -0
  23. package/dist/src/services/IndexerService.js +27 -7
  24. package/dist/src/services/InfinityEmbedder.d.ts +14 -55
  25. package/dist/src/services/InfinityEmbedder.js +30 -141
  26. package/dist/src/services/LoggerService.d.ts +3 -1
  27. package/dist/src/services/LoggerService.js +17 -3
  28. package/dist/src/services/SearcherService.d.ts +15 -1
  29. package/dist/src/services/SearcherService.js +105 -23
  30. package/dist/src/services/WebServerService.d.ts +41 -3
  31. package/dist/src/services/WebServerService.js +90 -2
  32. package/dist/src/services/infinity-provisioning.d.ts +115 -0
  33. package/dist/src/services/infinity-provisioning.js +516 -0
  34. package/dist/src/services/legacy-ollama-index.d.ts +24 -0
  35. package/dist/src/services/legacy-ollama-index.js +61 -0
  36. package/dist/src/services/reranker.d.ts +3 -3
  37. package/dist/src/services/reranker.js +3 -3
  38. package/dist/src/setup.d.ts +37 -0
  39. package/dist/src/setup.js +111 -157
  40. package/docker/docker-compose.yml +5 -15
  41. package/docs/web-dashboard.md +36 -17
  42. package/package.json +1 -5
  43. package/dist/src/services/OllamaEmbedder.d.ts +0 -25
  44. package/dist/src/services/OllamaEmbedder.js +0 -111
@@ -1,13 +1,34 @@
1
- import type { Config } from '../config';
1
+ import { Config } from '../config';
2
2
  import type { ISearcher } from '../interfaces';
3
3
  import type { IEmbedder } from '../interfaces';
4
4
  import type { ILogger } from '../interfaces';
5
5
  import { type JsonConfig } from '../config-store';
6
+ import { type ProvisioningDeps } from './infinity-provisioning';
7
+ export type BackendActionKind = 'started' | 'already-running' | 'not-needed' | 'manual-required' | 'failed';
8
+ export interface BackendActionResult {
9
+ action: BackendActionKind;
10
+ message: string | null;
11
+ }
12
+ export type BackendStatusState = 'idle' | 'recreating' | 'ready' | 'failed';
13
+ export interface BackendStatus {
14
+ state: BackendStatusState;
15
+ message: string | null;
16
+ startedAt: string | null;
17
+ finishedAt: string | null;
18
+ }
19
+ export interface WebServerDeps {
20
+ /** Path to the managed docker-compose.yml. Defaults to `CONFIG_DIR/docker-compose.yml`. */
21
+ composePath?: string;
22
+ /** Injection seams (fetchImpl/runCommand/pollIntervalMs/readinessTimeoutMs) for the readiness primitive. */
23
+ provisioning?: ProvisioningDeps;
24
+ /** Reads the just-saved config back — defaults to `() => new Config()`. Injectable for tests. */
25
+ readSavedConfig?: () => Config;
26
+ }
6
27
  /**
7
28
  * Per-field choice lists for the config form. `strict` → render a dropdown that
8
29
  * only accepts a listed value (e.g. the embedding model, tied to the vector
9
30
  * dimension). Non-strict → a combobox: the values are suggestions but free text
10
- * is still allowed (e.g. any Ollama model the user has pulled).
31
+ * is still allowed (e.g. any model served by a self-hosted backend).
11
32
  */
12
33
  export interface FieldOption {
13
34
  values: string[];
@@ -41,7 +62,13 @@ export declare class WebServer {
41
62
  private readonly logger;
42
63
  private readonly embedder;
43
64
  private server?;
44
- constructor(searcher: ISearcher, config: Config, logger: ILogger, embedder: IEmbedder);
65
+ private readonly composePath;
66
+ private readonly provisioningDeps;
67
+ private readonly readSavedConfig;
68
+ private backendStatus;
69
+ private taskInFlight;
70
+ private pending;
71
+ constructor(searcher: ISearcher, config: Config, logger: ILogger, embedder: IEmbedder, deps?: WebServerDeps);
45
72
  start(port: number, host: string): Promise<void>;
46
73
  stop(): Promise<void>;
47
74
  /** The bound address once started (e.g. `.port` after listen(0)); null otherwise. */
@@ -51,6 +78,17 @@ export declare class WebServer {
51
78
  private handle;
52
79
  private maskedConfig;
53
80
  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
+ private evaluateBackendAfterSave;
90
+ /** Kick off (or continue, via a follow-up) a background Infinity recreate. */
91
+ private startBackgroundTask;
54
92
  /**
55
93
  * Choice lists for model fields. Both are non-strict comboboxes (free text
56
94
  * allowed): the embedding model suggests the known registry models plus any
@@ -38,10 +38,15 @@ exports.mergeConfigForSave = mergeConfigForSave;
38
38
  exports.maskConfig = maskConfig;
39
39
  const http = __importStar(require("http"));
40
40
  const path = __importStar(require("path"));
41
+ const config_1 = require("../config");
41
42
  const config_store_1 = require("../config-store");
42
43
  const constants_1 = require("../constants");
43
44
  const graph_1 = require("../graph");
44
45
  const dashboard_1 = require("../web/dashboard");
46
+ const infinity_provisioning_1 = require("./infinity-provisioning");
47
+ function sameModelSet(a, b) {
48
+ return a.join(';') === b.join(';');
49
+ }
45
50
  /** Placeholder shown to the browser instead of a real secret. */
46
51
  exports.PASSWORD_MASK = '********';
47
52
  const MAX_BODY = 1_000_000; // 1 MB — config payloads are tiny
@@ -100,11 +105,22 @@ class WebServer {
100
105
  logger;
101
106
  embedder;
102
107
  server;
103
- constructor(searcher, config, logger, embedder) {
108
+ composePath;
109
+ provisioningDeps;
110
+ 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
+ backendStatus = { state: 'idle', message: null, startedAt: null, finishedAt: null };
114
+ taskInFlight = false;
115
+ pending = null;
116
+ constructor(searcher, config, logger, embedder, deps = {}) {
104
117
  this.searcher = searcher;
105
118
  this.config = config;
106
119
  this.logger = logger;
107
120
  this.embedder = embedder;
121
+ this.composePath = deps.composePath ?? path.join(config_store_1.CONFIG_DIR, 'docker-compose.yml');
122
+ this.provisioningDeps = deps.provisioning ?? {};
123
+ this.readSavedConfig = deps.readSavedConfig ?? (() => new config_1.Config());
108
124
  }
109
125
  async start(port, host) {
110
126
  this.server = http.createServer((req, res) => {
@@ -149,6 +165,8 @@ class WebServer {
149
165
  return this.json(res, 200, this.maskedConfig());
150
166
  if (m === 'PUT' && p === '/api/config')
151
167
  return this.saveConfig(req, res);
168
+ if (m === 'GET' && p === '/api/backend-status')
169
+ return this.json(res, 200, this.backendStatus);
152
170
  if (m === 'GET' && p === '/api/projects')
153
171
  return this.json(res, 200, await this.searcher.listProjects());
154
172
  if (m === 'GET' && p === '/api/graph')
@@ -186,7 +204,77 @@ class WebServer {
186
204
  }
187
205
  (0, config_store_1.writeConfigJson)(validated);
188
206
  this.logger.info('WebServer: config updated');
189
- this.json(res, 200, { ok: true });
207
+ const backend = await this.evaluateBackendAfterSave(validated);
208
+ this.json(res, 200, { ok: true, backend });
209
+ }
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
+ async evaluateBackendAfterSave(saved) {
219
+ const provider = saved.embedding?.provider ?? config_store_1.CONFIG_DEFAULTS['embedding.provider'];
220
+ if (provider !== 'infinity') {
221
+ this.pending = null;
222
+ return { action: 'not-needed', message: null };
223
+ }
224
+ try {
225
+ const freshConfig = this.readSavedConfig();
226
+ const baseURL = freshConfig.embedding.baseURL;
227
+ const composePath = this.composePath;
228
+ const modelSet = (0, infinity_provisioning_1.requiredModelSet)(freshConfig);
229
+ const managed = (0, infinity_provisioning_1.isManagedInfinity)(baseURL, composePath);
230
+ if (this.taskInFlight) {
231
+ if (managed) {
232
+ this.pending = { modelSet, baseURL, composePath };
233
+ return { action: 'already-running', message: 'A backend recreate is already in progress; this configuration will be applied next.' };
234
+ }
235
+ // No longer targets the managed container the in-flight task owns.
236
+ this.pending = null;
237
+ }
238
+ const evaluation = await (0, infinity_provisioning_1.evaluateBackend)(baseURL, composePath, modelSet, this.provisioningDeps);
239
+ switch (evaluation.action) {
240
+ case 'served':
241
+ return { action: 'not-needed', message: null };
242
+ case 'missing-external':
243
+ case 'unreachable-external':
244
+ return { action: 'manual-required', message: (0, infinity_provisioning_1.manualInstructionMessage)(evaluation, (0, infinity_provisioning_1.serverRoot)(baseURL), modelSet) };
245
+ case 'missing-managed':
246
+ case 'unreachable-managed':
247
+ this.startBackgroundTask(baseURL, composePath, modelSet);
248
+ return { action: 'started', message: 'Recreating the managed Infinity container in the background.' };
249
+ }
250
+ }
251
+ catch (err) {
252
+ return { action: 'failed', message: err instanceof Error ? err.message : String(err) };
253
+ }
254
+ }
255
+ /** Kick off (or continue, via a follow-up) a background Infinity recreate. */
256
+ startBackgroundTask(baseURL, composePath, modelSet) {
257
+ this.taskInFlight = true;
258
+ this.backendStatus = { state: 'recreating', message: null, startedAt: new Date().toISOString(), finishedAt: null };
259
+ (0, infinity_provisioning_1.ensureModelSetServed)(baseURL, composePath, modelSet, this.provisioningDeps)
260
+ .then(() => {
261
+ this.backendStatus = { ...this.backendStatus, state: 'ready', message: null, finishedAt: new Date().toISOString() };
262
+ })
263
+ .catch((err) => {
264
+ this.backendStatus = {
265
+ ...this.backendStatus, state: 'failed',
266
+ message: err instanceof Error ? err.message : String(err),
267
+ finishedAt: new Date().toISOString(),
268
+ };
269
+ })
270
+ .finally(() => {
271
+ this.taskInFlight = false;
272
+ const next = this.pending;
273
+ this.pending = null;
274
+ if (next && !sameModelSet(next.modelSet, modelSet)) {
275
+ this.startBackgroundTask(next.baseURL, next.composePath, next.modelSet);
276
+ }
277
+ });
190
278
  }
191
279
  /**
192
280
  * Choice lists for model fields. Both are non-strict comboboxes (free text
@@ -0,0 +1,115 @@
1
+ import type { Config } from '../config';
2
+ /** Result of a shell command run, mirroring the parts of `spawnSync` we use. */
3
+ export interface CommandResult {
4
+ status: number | null;
5
+ stdout?: string;
6
+ stderr?: string;
7
+ }
8
+ /** Injection seams for the provisioning primitive. All optional, defaulted to the real implementations. */
9
+ export interface ProvisioningDeps {
10
+ fetchImpl?: typeof fetch;
11
+ /** Runs a shell command (docker). Defaults to `spawnSync` with captured output. */
12
+ runCommand?: (cmd: string, args: string[]) => CommandResult;
13
+ /** Sleep between readiness polls (ms), capped at 5000. Defaults to 2000; tests pass 0. */
14
+ pollIntervalMs?: number;
15
+ /** Readiness wait timeout (ms). Defaults to 1,800,000 (30 min); tests override to run fast. */
16
+ readinessTimeoutMs?: number;
17
+ }
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
+ 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
+ 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
+ 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
+ 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
+ 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
+ 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
+ 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
+ export declare function writeComposeModelSet(composePath: string, modelSet: string[]): void;
64
+ /** True when an NVIDIA GPU + `nvidia-smi` is available on this host. */
65
+ 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
+ export declare function composeFileArgs(composePath: string): string[];
68
+ export type BackendAction = 'served' | 'missing-managed' | 'missing-external' | 'unreachable-managed' | 'unreachable-external';
69
+ export interface BackendEvaluation {
70
+ action: BackendAction;
71
+ managed: boolean;
72
+ /** Required models absent from the served listing; empty for `served` or an unverifiable listing. */
73
+ missingModels: string[];
74
+ /** The full served listing at evaluation time; empty for an unverifiable listing or when unreachable. */
75
+ servedModels: string[];
76
+ }
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
+ 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
+ export declare function manualInstructionMessage(evaluation: BackendEvaluation, base: string, modelSetToWrite: string[]): string;
87
+ export interface EnsureBackendReadyOpts {
88
+ baseURL: string;
89
+ /** Models that must be served for the call to succeed without a recreate/instructions. */
90
+ requiredModels: string[];
91
+ /** Full model set to write into the compose file and bind at startup when a start/recreate is needed. */
92
+ modelSetToWrite: string[];
93
+ composePath: string;
94
+ deps?: ProvisioningDeps;
95
+ }
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
+ 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
+ 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
+ export declare function requiredModelSet(cfg: Config): string[];