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.
- package/CHANGELOG.md +24 -1
- package/README.md +141 -39
- package/dist/app.js +2 -2
- package/dist/src/commands/IndexCommand.js +42 -1
- package/dist/src/config-error.d.ts +7 -0
- package/dist/src/config-error.js +14 -0
- package/dist/src/config-store.d.ts +30 -12
- package/dist/src/config-store.js +81 -26
- package/dist/src/config.d.ts +5 -4
- package/dist/src/config.js +14 -16
- package/dist/src/constants.d.ts +8 -2
- package/dist/src/constants.js +8 -5
- package/dist/src/help.js +1 -1
- package/dist/src/interfaces/IDatabase.d.ts +17 -0
- package/dist/src/interfaces/ILogger.d.ts +7 -0
- package/dist/src/services/AstChunker.d.ts +11 -0
- package/dist/src/services/AstChunker.js +44 -0
- package/dist/src/services/DatabaseService.d.ts +4 -0
- package/dist/src/services/DatabaseService.js +46 -0
- package/dist/src/services/EmbedderFactory.d.ts +1 -1
- package/dist/src/services/EmbedderFactory.js +2 -5
- package/dist/src/services/IndexerService.d.ts +4 -0
- package/dist/src/services/IndexerService.js +27 -7
- package/dist/src/services/InfinityEmbedder.d.ts +14 -55
- package/dist/src/services/InfinityEmbedder.js +30 -141
- package/dist/src/services/LoggerService.d.ts +3 -1
- package/dist/src/services/LoggerService.js +17 -3
- package/dist/src/services/SearcherService.d.ts +15 -1
- package/dist/src/services/SearcherService.js +105 -23
- package/dist/src/services/WebServerService.d.ts +41 -3
- package/dist/src/services/WebServerService.js +90 -2
- package/dist/src/services/infinity-provisioning.d.ts +115 -0
- package/dist/src/services/infinity-provisioning.js +516 -0
- package/dist/src/services/legacy-ollama-index.d.ts +24 -0
- package/dist/src/services/legacy-ollama-index.js +61 -0
- package/dist/src/services/reranker.d.ts +3 -3
- package/dist/src/services/reranker.js +3 -3
- package/dist/src/setup.d.ts +37 -0
- package/dist/src/setup.js +111 -157
- package/docker/docker-compose.yml +5 -15
- package/docs/web-dashboard.md +36 -17
- package/package.json +1 -5
- package/dist/src/services/OllamaEmbedder.d.ts +0 -25
- package/dist/src/services/OllamaEmbedder.js +0 -111
package/dist/src/setup.js
CHANGED
|
@@ -33,17 +33,19 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.EMBEDDING_PROVIDER_CHOICES = void 0;
|
|
36
37
|
exports.dockerPromptDefault = dockerPromptDefault;
|
|
38
|
+
exports.prepareComposeAndStartDb = prepareComposeAndStartDb;
|
|
39
|
+
exports.provisionInfinity = provisionInfinity;
|
|
37
40
|
exports.runSetup = runSetup;
|
|
38
41
|
const readline = __importStar(require("readline"));
|
|
39
42
|
const fs = __importStar(require("fs"));
|
|
40
43
|
const path = __importStar(require("path"));
|
|
41
44
|
const os = __importStar(require("os"));
|
|
42
45
|
const child_process_1 = require("child_process");
|
|
43
|
-
const constants_1 = require("./constants");
|
|
44
46
|
const config_store_1 = require("./config-store");
|
|
45
47
|
const config_1 = require("./config");
|
|
46
|
-
const
|
|
48
|
+
const infinity_provisioning_1 = require("./services/infinity-provisioning");
|
|
47
49
|
const DEFAULT_LOG_DIR = path.join(config_store_1.CONFIG_DIR, 'logs');
|
|
48
50
|
const _projectRoot = fs.existsSync(path.join(__dirname, '..', 'package.json'))
|
|
49
51
|
? path.join(__dirname, '..')
|
|
@@ -52,7 +54,6 @@ const DOCKER_COMPOSE_SRC = path.join(_projectRoot, 'docker', 'docker-compose.yml
|
|
|
52
54
|
const DOCKER_COMPOSE_DST = path.join(config_store_1.CONFIG_DIR, 'docker-compose.yml');
|
|
53
55
|
const DOCKER_GPU_OVERRIDE_SRC = path.join(_projectRoot, 'docker', 'docker-compose.infinity-gpu.yml');
|
|
54
56
|
const DOCKER_GPU_OVERRIDE_DST = path.join(config_store_1.CONFIG_DIR, 'docker-compose.infinity-gpu.yml');
|
|
55
|
-
const DEFAULT_INFINITY_RERANK_MODEL = 'Alibaba-NLP/gte-reranker-modernbert-base';
|
|
56
57
|
function ask(rl, question) {
|
|
57
58
|
return new Promise(resolve => rl.question(question, resolve));
|
|
58
59
|
}
|
|
@@ -66,14 +67,15 @@ function expandHome(p) {
|
|
|
66
67
|
return p;
|
|
67
68
|
}
|
|
68
69
|
const PROVIDER_DEFAULTS = {
|
|
69
|
-
'ollama': { baseURL: 'http://localhost:11434', model: constants_1.DEFAULT_EMBED_MODEL, envVar: '' },
|
|
70
70
|
'openai-compatible': { baseURL: 'https://api.openai.com/v1', model: 'text-embedding-3-small', envVar: 'OPENAI_API_KEY' },
|
|
71
71
|
'voyage': { baseURL: 'https://api.voyageai.com/v1', model: 'voyage-code-3', envVar: 'VOYAGE_API_KEY' },
|
|
72
72
|
'infinity': { baseURL: 'http://localhost:7997', model: 'jinaai/jina-embeddings-v2-base-code', envVar: '' },
|
|
73
73
|
};
|
|
74
|
+
/** Providers the wizard offers, in prompt order; the first is the preselected default. Exported for unit testing. */
|
|
75
|
+
exports.EMBEDDING_PROVIDER_CHOICES = ['infinity', 'openai-compatible', 'voyage'];
|
|
74
76
|
/** Providers that run a local inference backend we can probe / provision. */
|
|
75
77
|
function isLocalBackend(p) {
|
|
76
|
-
return p === '
|
|
78
|
+
return p === 'infinity';
|
|
77
79
|
}
|
|
78
80
|
/**
|
|
79
81
|
* Probe whether the embedding backend is already reachable, so the "start
|
|
@@ -83,24 +85,7 @@ function isLocalBackend(p) {
|
|
|
83
85
|
async function checkEmbeddingBackend(provider, baseURL) {
|
|
84
86
|
if (!isLocalBackend(provider))
|
|
85
87
|
return true;
|
|
86
|
-
|
|
87
|
-
return (0, InfinityEmbedder_1.probeInfinityHealth)(baseURL);
|
|
88
|
-
// ollama — probe /api/tags
|
|
89
|
-
try {
|
|
90
|
-
const http = await Promise.resolve().then(() => __importStar(require('http')));
|
|
91
|
-
await new Promise((resolve, reject) => {
|
|
92
|
-
const req = http.get(`${baseURL.replace(/\/+$/, '')}/api/tags`, res => {
|
|
93
|
-
res.resume();
|
|
94
|
-
res.statusCode === 200 ? resolve() : reject(new Error(`status ${res.statusCode}`));
|
|
95
|
-
});
|
|
96
|
-
req.on('error', reject);
|
|
97
|
-
req.setTimeout(2000, () => { req.destroy(); reject(new Error('timeout')); });
|
|
98
|
-
});
|
|
99
|
-
return true;
|
|
100
|
-
}
|
|
101
|
-
catch {
|
|
102
|
-
return false;
|
|
103
|
-
}
|
|
88
|
+
return (0, infinity_provisioning_1.probeInfinityHealth)(baseURL);
|
|
104
89
|
}
|
|
105
90
|
/**
|
|
106
91
|
* Default answer for the "start Docker containers?" prompt: no only when both
|
|
@@ -110,15 +95,6 @@ async function checkEmbeddingBackend(provider, baseURL) {
|
|
|
110
95
|
function dockerPromptDefault(dbReachable, embReachable) {
|
|
111
96
|
return (dbReachable && embReachable) ? 'n' : 'y';
|
|
112
97
|
}
|
|
113
|
-
/** True when an NVIDIA GPU + `nvidia-smi` is available on this host. */
|
|
114
|
-
function hasNvidiaGpu() {
|
|
115
|
-
try {
|
|
116
|
-
return (0, child_process_1.spawnSync)('nvidia-smi', ['-L'], { stdio: 'ignore' }).status === 0;
|
|
117
|
-
}
|
|
118
|
-
catch {
|
|
119
|
-
return false;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
98
|
/**
|
|
123
99
|
* Write (or replace) a single `KEY=VALUE` line in `~/.config/codesearch-mcp/.env`,
|
|
124
100
|
* chmod 600. API keys live here — never in config.json — referenced as `env:KEY`.
|
|
@@ -160,58 +136,6 @@ async function waitForDb(host, port, user, pass, db, maxWaitSec = 60, logger, st
|
|
|
160
136
|
logger?.error('waitForDb: timeout', { reason, steps: JSON.stringify(steps) });
|
|
161
137
|
process.exit(1);
|
|
162
138
|
}
|
|
163
|
-
async function waitForOllama(host, maxWaitSec = 60, logger, steps) {
|
|
164
|
-
const deadline = Date.now() + maxWaitSec * 1000;
|
|
165
|
-
process.stdout.write('Waiting for Ollama to be ready');
|
|
166
|
-
while (Date.now() < deadline) {
|
|
167
|
-
try {
|
|
168
|
-
const http = await Promise.resolve().then(() => __importStar(require('http')));
|
|
169
|
-
await new Promise((resolve, reject) => {
|
|
170
|
-
const req = http.get(`${host}/api/tags`, res => {
|
|
171
|
-
res.resume();
|
|
172
|
-
res.statusCode === 200 ? resolve() : reject(new Error(`status ${res.statusCode}`));
|
|
173
|
-
});
|
|
174
|
-
req.on('error', reject);
|
|
175
|
-
req.setTimeout(2000, () => { req.destroy(); reject(new Error('timeout')); });
|
|
176
|
-
});
|
|
177
|
-
console.log(' ready.');
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
catch {
|
|
181
|
-
process.stdout.write('.');
|
|
182
|
-
await new Promise(r => setTimeout(r, 2000));
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
const reason = 'Ollama did not become ready in time.';
|
|
186
|
-
console.error(`\n${reason}`);
|
|
187
|
-
logger?.error('waitForOllama: timeout', { reason, steps: JSON.stringify(steps) });
|
|
188
|
-
process.exit(1);
|
|
189
|
-
}
|
|
190
|
-
/**
|
|
191
|
-
* Poll Infinity's `/health` until ready. Unlike the DB/Ollama waiters this is
|
|
192
|
-
* NON-fatal: on CPU the first boot downloads the model set AND runs a lengthy
|
|
193
|
-
* per-model warmup (a reranker warmup batch alone can take minutes), so a
|
|
194
|
-
* timeout here does not mean failure — the container is usually just still
|
|
195
|
-
* warming up. Returns whether it became ready; the caller keeps going and
|
|
196
|
-
* writes the (valid) config either way. Generous default (30 min) for CPU.
|
|
197
|
-
*/
|
|
198
|
-
async function waitForInfinity(baseURL, maxWaitSec = 1800, logger, steps) {
|
|
199
|
-
const deadline = Date.now() + maxWaitSec * 1000;
|
|
200
|
-
process.stdout.write('Waiting for Infinity to be ready (first boot downloads + warms up the model set, this can take several minutes on CPU)');
|
|
201
|
-
while (Date.now() < deadline) {
|
|
202
|
-
if (await (0, InfinityEmbedder_1.probeInfinityHealth)(baseURL)) {
|
|
203
|
-
console.log(' ready.');
|
|
204
|
-
return true;
|
|
205
|
-
}
|
|
206
|
-
process.stdout.write('.');
|
|
207
|
-
await new Promise(r => setTimeout(r, 3000));
|
|
208
|
-
}
|
|
209
|
-
const reason = 'Infinity did not answer /health in time — it is likely still warming up.';
|
|
210
|
-
console.warn(`\n${reason}`);
|
|
211
|
-
console.warn(` The container is running; check "docker logs codesearch-mcp-infinity-1" and, once /health responds, run: rag-index`);
|
|
212
|
-
logger?.warn('waitForInfinity: timeout (non-fatal)', { reason, steps: JSON.stringify(steps) });
|
|
213
|
-
return false;
|
|
214
|
-
}
|
|
215
139
|
async function checkRemoteDb(host, port, user, pass, db, logger, steps) {
|
|
216
140
|
try {
|
|
217
141
|
const { Client } = await Promise.resolve().then(() => __importStar(require('pg')));
|
|
@@ -227,8 +151,16 @@ async function checkRemoteDb(host, port, user, pass, db, logger, steps) {
|
|
|
227
151
|
return false;
|
|
228
152
|
}
|
|
229
153
|
}
|
|
230
|
-
|
|
231
|
-
|
|
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
|
+
async function prepareComposeAndStartDb(opts) {
|
|
163
|
+
const { dbPort, dbUser, dbPass, startDb, logger, steps } = opts;
|
|
232
164
|
console.log('\nWriting docker-compose.yml to', DOCKER_COMPOSE_DST);
|
|
233
165
|
const yamlQuote = (s) => `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
234
166
|
let composeContent = fs.readFileSync(DOCKER_COMPOSE_SRC, 'utf-8');
|
|
@@ -236,72 +168,99 @@ async function startDockerContainers(opts) {
|
|
|
236
168
|
.replace('POSTGRES_USER: dev', `POSTGRES_USER: ${yamlQuote(dbUser)}`)
|
|
237
169
|
.replace('POSTGRES_PASSWORD: dev', `POSTGRES_PASSWORD: ${yamlQuote(dbPass)}`)
|
|
238
170
|
.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
|
+
let existingModelId = null;
|
|
175
|
+
try {
|
|
176
|
+
existingModelId = (0, infinity_provisioning_1.extractModelIdValue)(fs.readFileSync(DOCKER_COMPOSE_DST, 'utf-8'));
|
|
177
|
+
}
|
|
178
|
+
catch { /* no prior file */ }
|
|
179
|
+
if (existingModelId) {
|
|
180
|
+
composeContent = composeContent.replace(/INFINITY_MODEL_ID:\s*"[^"]*"/, `INFINITY_MODEL_ID: "${existingModelId}"`);
|
|
181
|
+
}
|
|
239
182
|
fs.mkdirSync(config_store_1.CONFIG_DIR, { recursive: true });
|
|
240
|
-
|
|
183
|
+
let existingMode = 0o600;
|
|
184
|
+
try {
|
|
185
|
+
existingMode = fs.statSync(DOCKER_COMPOSE_DST).mode;
|
|
186
|
+
}
|
|
187
|
+
catch { /* first-ever write */ }
|
|
188
|
+
(0, infinity_provisioning_1.atomicWriteFile)(DOCKER_COMPOSE_DST, composeContent, existingMode);
|
|
241
189
|
// The GPU override is copied verbatim so the managed Infinity path can layer it.
|
|
242
190
|
try {
|
|
243
191
|
fs.copyFileSync(DOCKER_GPU_OVERRIDE_SRC, DOCKER_GPU_OVERRIDE_DST);
|
|
244
192
|
}
|
|
245
193
|
catch { /* absent → CPU only */ }
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
194
|
+
if (!startDb)
|
|
195
|
+
return;
|
|
196
|
+
console.log('\nStarting the database container...');
|
|
197
|
+
const r = (0, child_process_1.spawnSync)('docker', ['compose', '-f', DOCKER_COMPOSE_DST, 'up', '-d', 'db'], { stdio: 'inherit' });
|
|
198
|
+
if (r.status !== 0) {
|
|
199
|
+
console.error('Failed to start the database container. Make sure Docker is running.');
|
|
200
|
+
logger?.error('prepareComposeAndStartDb: docker startup failed', { steps: JSON.stringify(steps) });
|
|
249
201
|
process.exit(1);
|
|
250
|
-
};
|
|
251
|
-
// --- Database ---
|
|
252
|
-
if (startDb) {
|
|
253
|
-
console.log('\nStarting the database container...');
|
|
254
|
-
const r = (0, child_process_1.spawnSync)('docker', ['compose', '-f', DOCKER_COMPOSE_DST, 'up', '-d', 'db'], { stdio: 'inherit' });
|
|
255
|
-
if (r.status !== 0)
|
|
256
|
-
dockerFail('Failed to start the database container. Make sure Docker is running.');
|
|
257
|
-
await waitForDb('localhost', parseInt(dbPort), dbUser, dbPass, 'postgres', 60, logger, steps);
|
|
258
202
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
203
|
+
await waitForDb('localhost', parseInt(dbPort), dbUser, dbPass, 'postgres', 60, logger, steps);
|
|
204
|
+
}
|
|
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
|
+
async function provisionInfinity(savedConfig, startDocker, logger, deps = {}, composePathOverride) {
|
|
214
|
+
const composePath = composePathOverride ?? DOCKER_COMPOSE_DST;
|
|
215
|
+
const baseURL = savedConfig.embedding.baseURL;
|
|
216
|
+
const base = (0, infinity_provisioning_1.serverRoot)(baseURL);
|
|
217
|
+
const set = (0, infinity_provisioning_1.requiredModelSet)(savedConfig);
|
|
218
|
+
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
|
+
if (managed) {
|
|
223
|
+
try {
|
|
224
|
+
(0, infinity_provisioning_1.writeComposeModelSet)(composePath, set);
|
|
225
|
+
}
|
|
226
|
+
catch (err) {
|
|
227
|
+
console.error(`\nFailed to write the managed compose file: ${err.message}`);
|
|
228
|
+
logger?.error('provisionInfinity: compose write failed', { reason: err.message });
|
|
229
|
+
process.exitCode = 1;
|
|
230
|
+
}
|
|
266
231
|
}
|
|
267
|
-
if (
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
232
|
+
if (startDocker) {
|
|
233
|
+
try {
|
|
234
|
+
await (0, infinity_provisioning_1.ensureModelSetServed)(baseURL, composePath, set, deps);
|
|
235
|
+
console.log(`\nInfinity is serving the configured model set: ${set.join(';')}`);
|
|
236
|
+
}
|
|
237
|
+
catch (err) {
|
|
238
|
+
const msg = err.message;
|
|
239
|
+
if (/did not become ready within/.test(msg)) {
|
|
240
|
+
console.warn(`\nWarning: ${msg}`);
|
|
241
|
+
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 });
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
console.error(`\nInfinity provisioning failed: ${msg}`);
|
|
246
|
+
logger?.error('provisionInfinity: failed', { reason: msg });
|
|
247
|
+
process.exitCode = 1;
|
|
248
|
+
}
|
|
281
249
|
}
|
|
282
|
-
console.log(`Pulling ${embModel} model (this may take a while)...`);
|
|
283
|
-
// `-t` allocates a pseudo-TTY so `ollama pull` renders its live progress bar;
|
|
284
|
-
// only when our stdout is itself a TTY (docker refuses `-t` without one).
|
|
285
|
-
const ttyFlag = process.stdout.isTTY ? ['-t'] : [];
|
|
286
|
-
(0, child_process_1.spawnSync)('docker', ['exec', ...ttyFlag, ollamaId, 'ollama', 'pull', embModel], { stdio: 'inherit' });
|
|
287
250
|
return;
|
|
288
251
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
// Only layer the GPU override when it actually exists on disk — otherwise a
|
|
293
|
-
// GPU host whose override copy failed would get `-f <missing>` → hard failure
|
|
294
|
-
// instead of the intended CPU fallback.
|
|
295
|
-
const gpu = hasNvidiaGpu() && fs.existsSync(DOCKER_GPU_OVERRIDE_DST);
|
|
296
|
-
const files = ['-f', DOCKER_COMPOSE_DST, ...(gpu ? ['-f', DOCKER_GPU_OVERRIDE_DST] : [])];
|
|
297
|
-
console.log(`\nStarting the Infinity container (${gpu ? 'GPU' : 'CPU'}) serving: ${modelIds}`);
|
|
298
|
-
// `--force-recreate` so a changed model set recreates the container (D7) —
|
|
299
|
-
// Infinity binds its models at startup and won't pick up a new set otherwise.
|
|
300
|
-
const r = (0, child_process_1.spawnSync)('docker', ['compose', ...files, 'up', '-d', '--force-recreate', 'infinity'], { stdio: 'inherit', env: { ...process.env, INFINITY_MODEL_ID: modelIds } });
|
|
301
|
-
if (r.status !== 0)
|
|
302
|
-
dockerFail('Failed to start the Infinity container. Make sure Docker is running.');
|
|
303
|
-
await waitForInfinity(embBaseURL, 1800, logger, steps);
|
|
252
|
+
// Docker declined — no Docker command; print what would apply the set.
|
|
253
|
+
const evaluation = await (0, infinity_provisioning_1.evaluateBackend)(baseURL, composePath, set, deps);
|
|
254
|
+
if (evaluation.action === 'served')
|
|
304
255
|
return;
|
|
256
|
+
if (evaluation.managed) {
|
|
257
|
+
const files = (0, infinity_provisioning_1.composeFileArgs)(composePath);
|
|
258
|
+
console.log(`\nInfinity does not yet serve the required model set (${set.join(';')}).`);
|
|
259
|
+
console.log('Run this to apply it:');
|
|
260
|
+
console.log(` docker compose ${files.join(' ')} up -d --force-recreate infinity`);
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
console.log(`\n${(0, infinity_provisioning_1.manualInstructionMessage)(evaluation, base, set)}`);
|
|
305
264
|
}
|
|
306
265
|
}
|
|
307
266
|
async function runSetup(logger) {
|
|
@@ -340,24 +299,22 @@ async function runSetup(logger) {
|
|
|
340
299
|
} while (!workspaceInput.trim());
|
|
341
300
|
const workspaceRoot = expandHome(workspaceInput.trim());
|
|
342
301
|
console.log('\n--- Embedding backend ---');
|
|
343
|
-
console.log('
|
|
344
|
-
console.log('
|
|
345
|
-
console.log(' openai-compatible — OpenAI cloud, or a local Infinity/TEI/vLLM server');
|
|
302
|
+
console.log(' infinity — local, one container serves embeddings + reranking (GPU/CPU) (default)');
|
|
303
|
+
console.log(' openai-compatible — OpenAI cloud, or a local Infinity/TEI/vLLM/Ollama (via its /v1 endpoint) server');
|
|
346
304
|
console.log(' voyage — Voyage AI cloud');
|
|
347
|
-
const validProviders = ['ollama', 'infinity', 'openai-compatible', 'voyage'];
|
|
348
305
|
let embProvider;
|
|
349
306
|
do {
|
|
350
|
-
embProvider = (await loggedPrompt('Provider (
|
|
351
|
-
if (!
|
|
352
|
-
console.log(` Invalid provider. Choose: ${
|
|
353
|
-
} while (!
|
|
307
|
+
embProvider = (await loggedPrompt('Provider (infinity/openai-compatible/voyage)', exports.EMBEDDING_PROVIDER_CHOICES[0]));
|
|
308
|
+
if (!exports.EMBEDDING_PROVIDER_CHOICES.includes(embProvider))
|
|
309
|
+
console.log(` Invalid provider. Choose: ${exports.EMBEDDING_PROVIDER_CHOICES.join(', ')}`);
|
|
310
|
+
} while (!exports.EMBEDDING_PROVIDER_CHOICES.includes(embProvider));
|
|
354
311
|
const pd = PROVIDER_DEFAULTS[embProvider];
|
|
355
312
|
const embBaseURL = await loggedPrompt('Base URL', pd.baseURL);
|
|
356
313
|
const embModel = await loggedPrompt('Embedding model', pd.model);
|
|
357
314
|
// Infinity serves a reranker from the same container — offer to provision one.
|
|
358
315
|
let rerankModel = '';
|
|
359
316
|
if (embProvider === 'infinity') {
|
|
360
|
-
rerankModel = (await loggedPrompt(`Reranker model (served by the same Infinity container; blank to disable)`,
|
|
317
|
+
rerankModel = (await loggedPrompt(`Reranker model (served by the same Infinity container; blank to disable)`, config_store_1.CONFIG_DEFAULTS['reranker.model'])).trim();
|
|
361
318
|
}
|
|
362
319
|
// API keys go into .env (chmod 600); config.json only stores the env: reference.
|
|
363
320
|
let apiKeyRef = '';
|
|
@@ -373,8 +330,6 @@ async function runSetup(logger) {
|
|
|
373
330
|
console.log(` No key entered — set it later: echo '${pd.envVar}=sk-…' >> ${config_store_1.ENV_FILE}`);
|
|
374
331
|
}
|
|
375
332
|
}
|
|
376
|
-
// Ollama host used by the Docker helper below (only meaningful for ollama).
|
|
377
|
-
const ollamaHost = embProvider === 'ollama' ? embBaseURL : 'http://localhost:11434';
|
|
378
333
|
console.log('\n--- Database ---');
|
|
379
334
|
const dbHost = await loggedPrompt('DB host', 'localhost');
|
|
380
335
|
const dbPort = await loggedPrompt('DB port', '5433');
|
|
@@ -398,15 +353,9 @@ async function runSetup(logger) {
|
|
|
398
353
|
const startDockerRaw = await loggedAsk(`Start Docker containers automatically? (y/n) (${dockerDefault}): `);
|
|
399
354
|
const startDocker = (startDockerRaw.trim().toLowerCase() || dockerDefault) === 'y';
|
|
400
355
|
if (startDocker) {
|
|
401
|
-
await
|
|
402
|
-
provider: embProvider,
|
|
403
|
-
ollamaHost,
|
|
404
|
-
embBaseURL,
|
|
356
|
+
await prepareComposeAndStartDb({
|
|
405
357
|
dbPort, dbUser, dbPass,
|
|
406
|
-
embModel,
|
|
407
|
-
rerankModel: rerankModel || undefined,
|
|
408
358
|
startDb: !dbReachable,
|
|
409
|
-
startBackend: isLocalBackend(embProvider) && !embReachable,
|
|
410
359
|
logger, steps,
|
|
411
360
|
});
|
|
412
361
|
}
|
|
@@ -469,6 +418,11 @@ async function runSetup(logger) {
|
|
|
469
418
|
(0, config_store_1.writeConfigJson)(cfg);
|
|
470
419
|
console.log(`\nConfig saved to ${config_store_1.CONFIG_FILE}`);
|
|
471
420
|
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
|
+
if (embProvider === 'infinity') {
|
|
424
|
+
await provisionInfinity(new config_1.Config(), startDocker, logger);
|
|
425
|
+
}
|
|
472
426
|
console.log('\nInitializing database schema...');
|
|
473
427
|
const { DatabaseService } = await Promise.resolve().then(() => __importStar(require('./services/DatabaseService')));
|
|
474
428
|
const dbService = new DatabaseService(new config_1.Config());
|
|
@@ -12,19 +12,12 @@ services:
|
|
|
12
12
|
volumes:
|
|
13
13
|
- pgdata:/var/lib/postgresql/data
|
|
14
14
|
|
|
15
|
-
ollama:
|
|
16
|
-
image: ollama/ollama
|
|
17
|
-
ports:
|
|
18
|
-
- "11434:11434"
|
|
19
|
-
volumes:
|
|
20
|
-
- ollama_data:/root/.ollama
|
|
21
|
-
|
|
22
15
|
# One Infinity container serves both /embeddings and /rerank. The model set is
|
|
23
16
|
# bound at startup via INFINITY_MODEL_ID (";"-separated for multiple models,
|
|
24
|
-
# e.g. "BAAI/bge-m3;BAAI/bge-reranker-v2-m3")
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
17
|
+
# e.g. "BAAI/bge-m3;BAAI/bge-reranker-v2-m3") and rewritten in place by the app
|
|
18
|
+
# (a literal, not an env interpolation) so a plain `docker compose up -d`
|
|
19
|
+
# always serves the configured set. Defaults to CPU so it starts on any host;
|
|
20
|
+
# GPU is opt-in via docker-compose.infinity-gpu.yml (see rag-setup).
|
|
28
21
|
# Model warmup defaults OFF here: on CPU it runs multi-minute benchmark
|
|
29
22
|
# batches at every boot (a pure perf measurement, not required to serve). The
|
|
30
23
|
# GPU override turns it back on, where it costs seconds.
|
|
@@ -34,16 +27,13 @@ services:
|
|
|
34
27
|
ports:
|
|
35
28
|
- "7997:7997"
|
|
36
29
|
environment:
|
|
37
|
-
INFINITY_MODEL_ID:
|
|
30
|
+
INFINITY_MODEL_ID: "jinaai/jina-embeddings-v2-base-code;mixedbread-ai/mxbai-rerank-xsmall-v1"
|
|
38
31
|
INFINITY_MODEL_WARMUP: ${INFINITY_MODEL_WARMUP:-false}
|
|
39
32
|
INFINITY_PORT: "7997"
|
|
40
33
|
command: ["v2"]
|
|
41
34
|
volumes:
|
|
42
35
|
- infinity_cache:/app/.cache
|
|
43
|
-
profiles:
|
|
44
|
-
- infinity
|
|
45
36
|
|
|
46
37
|
volumes:
|
|
47
38
|
pgdata:
|
|
48
|
-
ollama_data:
|
|
49
39
|
infinity_cache:
|
package/docs/web-dashboard.md
CHANGED
|
@@ -49,7 +49,7 @@ can only reach the dashboard while the process is alive.
|
|
|
49
49
|
`rag-config edit` (CLI) instead.
|
|
50
50
|
- A failed **DB** connection is non-fatal: the config editor still works; the
|
|
51
51
|
Projects and Graph tabs show an in-page error.
|
|
52
|
-
- A failed **
|
|
52
|
+
- A failed **embedding backend** connection only affects model suggestions (see
|
|
53
53
|
*Field options*); everything else works.
|
|
54
54
|
|
|
55
55
|
---
|
|
@@ -105,16 +105,15 @@ Some settings have a constrained set of valid values, so the form offers a list
|
|
|
105
105
|
rather than free text. `GET /api/field-options` returns
|
|
106
106
|
`{ [dottedKey]: { values: string[], strict: boolean } }`:
|
|
107
107
|
|
|
108
|
-
- **`
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
text.
|
|
108
|
+
- **`embedding.model`, `reranker.model` — non-strict.** `embedding.model`
|
|
109
|
+
suggests `MODEL_REGISTRY` (`src/constants.ts`) entries plus any model the
|
|
110
|
+
backend currently serves; `reranker.model` suggests only served models. Both
|
|
111
|
+
allow free text — a model outside the registry still works (its dimension is
|
|
112
|
+
probed at index time). Served models come from `IEmbedder.listModels()`,
|
|
113
|
+
implemented only by `InfinityEmbedder` (2-second-limited `/models` listing);
|
|
114
|
+
if the backend is unreachable, or the provider doesn't implement `listModels`
|
|
115
|
+
(`openai-compatible`, `voyage`), the served-model list is empty and the field
|
|
116
|
+
degrades to registry suggestions only (or plain text for `reranker.model`).
|
|
118
117
|
|
|
119
118
|
A strict `<select>` preserves an unknown current value as an extra option, so
|
|
120
119
|
saving never silently drops a value that isn't in the list.
|
|
@@ -132,19 +131,39 @@ All routes are served from `127.0.0.1:<port>`.
|
|
|
132
131
|
| `GET` | `/api/defaults` | `CONFIG_DEFAULTS` — dotted key → default value |
|
|
133
132
|
| `GET` | `/api/field-options` | dotted key → `{ values, strict }` |
|
|
134
133
|
| `GET` | `/api/config` | Current config, `db.password` masked |
|
|
135
|
-
| `PUT` | `/api/config` | Save. Body = config JSON. `200 {ok:true}` or `400 {error}` |
|
|
134
|
+
| `PUT` | `/api/config` | Save. Body = config JSON. `200 {ok:true, backend:{action,message}}` or `400 {error}`. For `embedding.provider: infinity`, `backend.action` reports whether the Infinity container needed a background recreate (`started`/`already-running`/`not-needed`/`manual-required`/`failed`) — see *Infinity backend recreate* below |
|
|
135
|
+
| `GET` | `/api/backend-status` | `{ state, message, startedAt, finishedAt }` — progress of the most recent background Infinity recreate this server process started |
|
|
136
136
|
| `GET` | `/api/projects` | `SearcherService.listProjects()` |
|
|
137
137
|
| `GET` | `/api/graph` | `{ nodes, edges }` JSON (`buildGraphData()`) |
|
|
138
138
|
| `GET` | `/graph` | Full interactive graph page (`buildHtml()`), embedded by the Graph tab's iframe |
|
|
139
139
|
|
|
140
140
|
---
|
|
141
141
|
|
|
142
|
+
## Infinity backend recreate
|
|
143
|
+
|
|
144
|
+
When a save's `embedding.provider` is `infinity`, the server computes the
|
|
145
|
+
required model set from the **saved** config (freshly re-read from disk, not
|
|
146
|
+
the embedder the process started with) and compares it against what the
|
|
147
|
+
backend currently serves. If a managed container needs to be started or
|
|
148
|
+
recreated to serve the new set, that runs as an in-process background task —
|
|
149
|
+
the `PUT` responds immediately (`action: "started"`) without waiting for it.
|
|
150
|
+
Poll `GET /api/backend-status` for progress (`state: "recreating"` →
|
|
151
|
+
`"ready"`/`"failed"`). Only one `docker compose` run is active at a time; a
|
|
152
|
+
save that arrives while a recreate is already in flight for a managed backend
|
|
153
|
+
responds `already-running`, and at most one follow-up run applies the latest
|
|
154
|
+
such save once the in-flight one finishes. An external (unmanaged) backend
|
|
155
|
+
missing a required model never triggers Docker — the response's `message`
|
|
156
|
+
carries manual `--model-id`/launch instructions instead.
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
142
160
|
## Architecture
|
|
143
161
|
|
|
144
162
|
| File | Responsibility |
|
|
145
163
|
|---|---|
|
|
146
164
|
| `src/commands/WebCommand.ts` | CLI entry. `db.initDb()`, starts the server, keeps `execute()` pending until `SIGINT`/`SIGTERM` (mirrors `McpCommand`), then `server.stop()`. Parses `--port` (default `4319`). |
|
|
147
|
-
| `src/services/WebServerService.ts` | `WebServer(searcher, config, logger)` — `node:http` server, routing, and the
|
|
165
|
+
| `src/services/WebServerService.ts` | `WebServer(searcher, config, logger, embedder, deps?)` — `node:http` server, routing, the API handlers, and the background Infinity recreate + `/api/backend-status` state. Exports the pure `mergeConfigForSave()` and the `FieldOption` type. |
|
|
166
|
+
| `src/services/infinity-provisioning.ts` | The readiness/recreate primitive the background task delegates to (shared with `InfinityEmbedder` and `rag-setup`) — classification, compose-file rewrite, `docker compose` invocation, readiness polling. |
|
|
148
167
|
| `src/web/dashboard.ts` | `renderDashboardHtml()` — the entire page (HTML + CSS + JS) as one self-contained string, mirroring `graph.ts:buildHtml`. |
|
|
149
168
|
|
|
150
169
|
Reused primitives (no duplication):
|
|
@@ -153,7 +172,8 @@ Reused primitives (no duplication):
|
|
|
153
172
|
`CONFIG_DEFAULTS` — `src/config-store.ts`
|
|
154
173
|
- `buildGraphData` — `src/graph.ts`
|
|
155
174
|
- `listProjects` — `src/services/SearcherService.ts`
|
|
156
|
-
- `
|
|
175
|
+
- `MODEL_REGISTRY` — `src/constants.ts`
|
|
176
|
+
- `evaluateBackend` / `ensureModelSetServed` / `requiredModelSet` — `src/services/infinity-provisioning.ts`
|
|
157
177
|
|
|
158
178
|
Wiring: `app.ts` detects the `web` mode and dispatches
|
|
159
179
|
`new WebCommand(container, effectiveArgs)`. The command uses the shared
|
|
@@ -182,6 +202,5 @@ changes were needed.
|
|
|
182
202
|
`.default(...)` calls, so the runtime default and the dashboard-displayed
|
|
183
203
|
default cannot diverge — no manual sync needed. (A unit test still asserts
|
|
184
204
|
every key is a valid config key.)
|
|
185
|
-
- `
|
|
186
|
-
|
|
187
|
-
manual invariant, guarded only at the membership level by a unit test.
|
|
205
|
+
- `MODEL_REGISTRY` (`constants.ts`) is advisory, not exhaustive — a model
|
|
206
|
+
outside it still works via the runtime dimension probe at index time.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codesearch-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0-alpha",
|
|
4
4
|
"description": "Local semantic codebase search via MCP — indexes projects into pgvector, exposes search to Claude",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -18,9 +18,6 @@
|
|
|
18
18
|
"engines": {
|
|
19
19
|
"node": ">=18"
|
|
20
20
|
},
|
|
21
|
-
"publishConfig": {
|
|
22
|
-
"tag": "alpha"
|
|
23
|
-
},
|
|
24
21
|
"main": "dist/app.js",
|
|
25
22
|
"bin": {
|
|
26
23
|
"rag-index": "dist/app.js",
|
|
@@ -58,7 +55,6 @@
|
|
|
58
55
|
"cli-progress": "^3.12.0",
|
|
59
56
|
"drizzle-orm": "^0.45.2",
|
|
60
57
|
"ignore": "^7.0.5",
|
|
61
|
-
"ollama": "^0.5.0",
|
|
62
58
|
"pg": "^8.13.0",
|
|
63
59
|
"pino": "^10.3.1",
|
|
64
60
|
"pino-roll": "^4.0.0",
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import type { Config } from '../config';
|
|
2
|
-
import type { IEmbedder, InputType } from '../interfaces';
|
|
3
|
-
import type { ILogger } from '../interfaces';
|
|
4
|
-
import type { EmbedderDeps } from './OpenAICompatibleEmbedder';
|
|
5
|
-
export declare class OllamaEmbedder implements IEmbedder {
|
|
6
|
-
private config;
|
|
7
|
-
private logger;
|
|
8
|
-
readonly dimensions: number;
|
|
9
|
-
private client;
|
|
10
|
-
rerank?: (query: string, documents: string[], topN: number) => Promise<number[]>;
|
|
11
|
-
constructor(config: Config, logger?: ILogger, deps?: EmbedderDeps);
|
|
12
|
-
embed(text: string, _inputType?: InputType): Promise<number[]>;
|
|
13
|
-
embedBatch(texts: string[], _inputType?: InputType): Promise<number[][]>;
|
|
14
|
-
listModels(): Promise<string[]>;
|
|
15
|
-
/**
|
|
16
|
-
* Pull the configured model if it is not already present locally, so a plain
|
|
17
|
-
* `rag-index` works after switching to (or back to) a local model that was
|
|
18
|
-
* never pulled — `rag-setup` is no longer the only place that fetches models.
|
|
19
|
-
* A tagless model name (`bge-m3`) matches its `:latest`/tagged install.
|
|
20
|
-
* Streams pull progress to the console: a live single-line bar on a TTY, or
|
|
21
|
-
* de-duplicated status lines otherwise.
|
|
22
|
-
*/
|
|
23
|
-
ensureModelAvailable(): Promise<void>;
|
|
24
|
-
private static formatPullProgress;
|
|
25
|
-
}
|