codesearch-mcp 0.0.18-alpha → 0.1.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 +23 -0
- package/README.md +75 -6
- package/dist/app.js +36 -0
- package/dist/src/commands/IndexCommand.js +14 -1
- package/dist/src/commands/McpCommand.js +6 -2
- package/dist/src/config-store.d.ts +49 -11
- package/dist/src/config-store.js +79 -19
- package/dist/src/config.d.ts +32 -8
- package/dist/src/config.js +84 -20
- package/dist/src/constants.d.ts +36 -2
- package/dist/src/constants.js +58 -7
- package/dist/src/container.js +5 -3
- package/dist/src/db/index.d.ts +1 -1
- package/dist/src/db/index.js +2 -1
- package/dist/src/db/schema.d.ts +111 -0
- package/dist/src/db/schema.js +15 -1
- package/dist/src/help.js +5 -4
- package/dist/src/interfaces/IAstParser.d.ts +14 -0
- package/dist/src/interfaces/IChunker.d.ts +24 -0
- package/dist/src/interfaces/IChunker.js +2 -0
- package/dist/src/interfaces/IDatabase.d.ts +14 -0
- package/dist/src/interfaces/IEmbedder.d.ts +14 -2
- package/dist/src/interfaces/ISearcher.d.ts +1 -0
- package/dist/src/interfaces/index.d.ts +2 -1
- package/dist/src/owner-path.d.ts +10 -0
- package/dist/src/owner-path.js +27 -0
- package/dist/src/services/AstChunker.d.ts +31 -0
- package/dist/src/services/AstChunker.js +114 -0
- package/dist/src/services/AstParserService.d.ts +6 -0
- package/dist/src/services/AstParserService.js +31 -2
- package/dist/src/services/DatabaseService.d.ts +13 -1
- package/dist/src/services/DatabaseService.js +70 -25
- package/dist/src/services/EmbedderFactory.d.ts +9 -0
- package/dist/src/services/EmbedderFactory.js +29 -0
- package/dist/src/services/IndexerService.d.ts +17 -5
- package/dist/src/services/IndexerService.js +101 -38
- package/dist/src/services/InfinityEmbedder.d.ts +79 -0
- package/dist/src/services/InfinityEmbedder.js +199 -0
- package/dist/src/services/OllamaEmbedder.d.ts +16 -5
- package/dist/src/services/OllamaEmbedder.js +84 -36
- package/dist/src/services/OpenAICompatibleEmbedder.d.ts +25 -0
- package/dist/src/services/OpenAICompatibleEmbedder.js +75 -0
- package/dist/src/services/SearcherService.d.ts +7 -1
- package/dist/src/services/SearcherService.js +35 -16
- package/dist/src/services/VoyageEmbedder.d.ts +22 -0
- package/dist/src/services/VoyageEmbedder.js +72 -0
- package/dist/src/services/WebServerService.d.ts +16 -11
- package/dist/src/services/WebServerService.js +44 -24
- package/dist/src/services/http-retry.d.ts +19 -0
- package/dist/src/services/http-retry.js +54 -0
- package/dist/src/services/reranker.d.ts +15 -0
- package/dist/src/services/reranker.js +44 -0
- package/dist/src/setup.d.ts +6 -0
- package/dist/src/setup.js +243 -25
- package/dist/src/tools/SearchCodebaseTool.js +1 -1
- package/dist/src/tools/format-helpers.js +2 -1
- package/dist/src/web/dashboard.js +5 -0
- package/docker/docker-compose.infinity-gpu.yml +19 -0
- package/docker/docker-compose.yml +25 -0
- package/package.json +21 -2
- package/dist/src/ast.d.ts +0 -2
- package/dist/src/ast.js +0 -110
- package/dist/src/commands/SetupCommand.d.ts +0 -7
- package/dist/src/commands/SetupCommand.js +0 -14
- package/dist/src/db/data-source.d.ts +0 -2
- package/dist/src/db/data-source.js +0 -13
- package/dist/src/db/entities/code-chunk.entity.d.ts +0 -9
- package/dist/src/db/entities/code-chunk.entity.js +0 -54
- package/dist/src/db/entities/code-dependency.entity.d.ts +0 -9
- package/dist/src/db/entities/code-dependency.entity.js +0 -54
- package/dist/src/db/entities/code-symbol.entity.d.ts +0 -10
- package/dist/src/db/entities/code-symbol.entity.js +0 -59
- package/dist/src/db/init.d.ts +0 -2
- package/dist/src/db/init.js +0 -62
- package/dist/src/env-loader.d.ts +0 -1
- package/dist/src/env-loader.js +0 -41
- package/dist/src/indexer.d.ts +0 -17
- package/dist/src/indexer.js +0 -242
- package/dist/src/mcp.d.ts +0 -1
- package/dist/src/mcp.js +0 -403
- package/dist/src/ollama.d.ts +0 -2
- package/dist/src/ollama.js +0 -6
- package/dist/src/searcher.d.ts +0 -74
- package/dist/src/searcher.js +0 -446
- package/dist/vitest.config.d.ts +0 -2
- package/dist/vitest.config.js +0 -9
package/dist/src/setup.js
CHANGED
|
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.dockerPromptDefault = dockerPromptDefault;
|
|
36
37
|
exports.runSetup = runSetup;
|
|
37
38
|
const readline = __importStar(require("readline"));
|
|
38
39
|
const fs = __importStar(require("fs"));
|
|
@@ -42,12 +43,16 @@ const child_process_1 = require("child_process");
|
|
|
42
43
|
const constants_1 = require("./constants");
|
|
43
44
|
const config_store_1 = require("./config-store");
|
|
44
45
|
const config_1 = require("./config");
|
|
46
|
+
const InfinityEmbedder_1 = require("./services/InfinityEmbedder");
|
|
45
47
|
const DEFAULT_LOG_DIR = path.join(config_store_1.CONFIG_DIR, 'logs');
|
|
46
48
|
const _projectRoot = fs.existsSync(path.join(__dirname, '..', 'package.json'))
|
|
47
49
|
? path.join(__dirname, '..')
|
|
48
50
|
: path.join(__dirname, '..', '..');
|
|
49
51
|
const DOCKER_COMPOSE_SRC = path.join(_projectRoot, 'docker', 'docker-compose.yml');
|
|
50
52
|
const DOCKER_COMPOSE_DST = path.join(config_store_1.CONFIG_DIR, 'docker-compose.yml');
|
|
53
|
+
const DOCKER_GPU_OVERRIDE_SRC = path.join(_projectRoot, 'docker', 'docker-compose.infinity-gpu.yml');
|
|
54
|
+
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';
|
|
51
56
|
function ask(rl, question) {
|
|
52
57
|
return new Promise(resolve => rl.question(question, resolve));
|
|
53
58
|
}
|
|
@@ -60,6 +65,79 @@ function expandHome(p) {
|
|
|
60
65
|
return path.join(os.homedir(), p.slice(2));
|
|
61
66
|
return p;
|
|
62
67
|
}
|
|
68
|
+
const PROVIDER_DEFAULTS = {
|
|
69
|
+
'ollama': { baseURL: 'http://localhost:11434', model: constants_1.DEFAULT_EMBED_MODEL, envVar: '' },
|
|
70
|
+
'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' },
|
|
72
|
+
'infinity': { baseURL: 'http://localhost:7997', model: 'jinaai/jina-embeddings-v2-base-code', envVar: '' },
|
|
73
|
+
};
|
|
74
|
+
/** Providers that run a local inference backend we can probe / provision. */
|
|
75
|
+
function isLocalBackend(p) {
|
|
76
|
+
return p === 'ollama' || p === 'infinity';
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Probe whether the embedding backend is already reachable, so the "start
|
|
80
|
+
* Docker?" prompt can default to no (mirrors the DB `checkRemoteDb` flow).
|
|
81
|
+
* Cloud providers have no local backend → always "reachable" (nothing to start).
|
|
82
|
+
*/
|
|
83
|
+
async function checkEmbeddingBackend(provider, baseURL) {
|
|
84
|
+
if (!isLocalBackend(provider))
|
|
85
|
+
return true;
|
|
86
|
+
if (provider === 'infinity')
|
|
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
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Default answer for the "start Docker containers?" prompt: no only when both
|
|
107
|
+
* the database and the embedding backend are already reachable. Pure — exported
|
|
108
|
+
* for unit testing.
|
|
109
|
+
*/
|
|
110
|
+
function dockerPromptDefault(dbReachable, embReachable) {
|
|
111
|
+
return (dbReachable && embReachable) ? 'n' : 'y';
|
|
112
|
+
}
|
|
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
|
+
/**
|
|
123
|
+
* Write (or replace) a single `KEY=VALUE` line in `~/.config/codesearch-mcp/.env`,
|
|
124
|
+
* chmod 600. API keys live here — never in config.json — referenced as `env:KEY`.
|
|
125
|
+
*/
|
|
126
|
+
function writeEnvKey(name, value) {
|
|
127
|
+
fs.mkdirSync(config_store_1.CONFIG_DIR, { recursive: true });
|
|
128
|
+
let existing = '';
|
|
129
|
+
try {
|
|
130
|
+
existing = fs.readFileSync(config_store_1.ENV_FILE, 'utf-8');
|
|
131
|
+
}
|
|
132
|
+
catch { /* absent */ }
|
|
133
|
+
const kept = existing.split(/\r?\n/).filter(l => l.trim() && !l.trim().startsWith(`${name}=`));
|
|
134
|
+
kept.push(`${name}=${value}`);
|
|
135
|
+
fs.writeFileSync(config_store_1.ENV_FILE, kept.join('\n') + '\n', { mode: 0o600 });
|
|
136
|
+
try {
|
|
137
|
+
fs.chmodSync(config_store_1.ENV_FILE, 0o600);
|
|
138
|
+
}
|
|
139
|
+
catch { /* best effort */ }
|
|
140
|
+
}
|
|
63
141
|
async function waitForDb(host, port, user, pass, db, maxWaitSec = 60, logger, steps) {
|
|
64
142
|
const { Client } = await Promise.resolve().then(() => __importStar(require('pg')));
|
|
65
143
|
const deadline = Date.now() + maxWaitSec * 1000;
|
|
@@ -109,6 +187,31 @@ async function waitForOllama(host, maxWaitSec = 60, logger, steps) {
|
|
|
109
187
|
logger?.error('waitForOllama: timeout', { reason, steps: JSON.stringify(steps) });
|
|
110
188
|
process.exit(1);
|
|
111
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
|
+
}
|
|
112
215
|
async function checkRemoteDb(host, port, user, pass, db, logger, steps) {
|
|
113
216
|
try {
|
|
114
217
|
const { Client } = await Promise.resolve().then(() => __importStar(require('pg')));
|
|
@@ -124,7 +227,8 @@ async function checkRemoteDb(host, port, user, pass, db, logger, steps) {
|
|
|
124
227
|
return false;
|
|
125
228
|
}
|
|
126
229
|
}
|
|
127
|
-
async function startDockerContainers(
|
|
230
|
+
async function startDockerContainers(opts) {
|
|
231
|
+
const { provider, ollamaHost, embBaseURL, dbPort, dbUser, dbPass, embModel, rerankModel, startDb, startBackend, logger, steps } = opts;
|
|
128
232
|
console.log('\nWriting docker-compose.yml to', DOCKER_COMPOSE_DST);
|
|
129
233
|
const yamlQuote = (s) => `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
130
234
|
let composeContent = fs.readFileSync(DOCKER_COMPOSE_SRC, 'utf-8');
|
|
@@ -134,33 +238,77 @@ async function startDockerContainers(ollamaHost, dbPort, dbUser, dbPass, logger,
|
|
|
134
238
|
.replace('"5433:5432"', `"${dbPort}:5432"`);
|
|
135
239
|
fs.mkdirSync(config_store_1.CONFIG_DIR, { recursive: true });
|
|
136
240
|
fs.writeFileSync(DOCKER_COMPOSE_DST, composeContent, 'utf-8');
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
241
|
+
// The GPU override is copied verbatim so the managed Infinity path can layer it.
|
|
242
|
+
try {
|
|
243
|
+
fs.copyFileSync(DOCKER_GPU_OVERRIDE_SRC, DOCKER_GPU_OVERRIDE_DST);
|
|
244
|
+
}
|
|
245
|
+
catch { /* absent → CPU only */ }
|
|
246
|
+
const dockerFail = (reason) => {
|
|
141
247
|
console.error(reason);
|
|
142
248
|
logger?.error('startDockerContainers: docker startup failed', { reason, steps: JSON.stringify(steps) });
|
|
143
249
|
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);
|
|
144
258
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
259
|
+
// --- Embedding backend ---
|
|
260
|
+
if (!startBackend) {
|
|
261
|
+
if (isLocalBackend(provider))
|
|
262
|
+
console.log(`${provider} is already reachable — not starting a managed container.`);
|
|
263
|
+
else
|
|
264
|
+
console.log('Cloud embedding provider — no local backend to start.');
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (provider === 'ollama') {
|
|
268
|
+
console.log('\nStarting the Ollama container...');
|
|
269
|
+
const r = (0, child_process_1.spawnSync)('docker', ['compose', '-f', DOCKER_COMPOSE_DST, 'up', '-d', 'ollama'], { stdio: 'inherit' });
|
|
270
|
+
if (r.status !== 0)
|
|
271
|
+
dockerFail('Failed to start the Ollama container. Make sure Docker is running.');
|
|
272
|
+
await waitForOllama(ollamaHost, 60, logger, steps);
|
|
273
|
+
const ollamaId = (0, child_process_1.spawnSync)('docker', ['compose', '-f', DOCKER_COMPOSE_DST, 'ps', '-q', 'ollama'], { encoding: 'utf-8' })
|
|
274
|
+
.stdout?.trim();
|
|
275
|
+
if (!ollamaId) {
|
|
276
|
+
const reason = 'Could not resolve ollama container ID from compose. Pull the model manually:';
|
|
277
|
+
console.error(reason);
|
|
278
|
+
console.error(` docker exec <ollama-container> ollama pull ${embModel}`);
|
|
279
|
+
logger?.error('startDockerContainers: ollama container not found', { reason, steps: JSON.stringify(steps) });
|
|
280
|
+
process.exit(1);
|
|
281
|
+
}
|
|
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
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (provider === 'infinity') {
|
|
290
|
+
// Both models are bound at boot via a ";"-separated INFINITY_MODEL_ID.
|
|
291
|
+
const modelIds = [embModel, ...(rerankModel ? [rerankModel] : [])].join(';');
|
|
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);
|
|
304
|
+
return;
|
|
155
305
|
}
|
|
156
|
-
console.log(`Pulling ${constants_1.DEFAULT_EMBED_MODEL} model (this may take a while)...`);
|
|
157
|
-
(0, child_process_1.spawnSync)('docker', ['exec', ollamaId, 'ollama', 'pull', constants_1.DEFAULT_EMBED_MODEL], { stdio: 'inherit' });
|
|
158
306
|
}
|
|
159
307
|
async function runSetup(logger) {
|
|
160
308
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
161
309
|
const steps = [];
|
|
162
310
|
function logStep(q, raw) {
|
|
163
|
-
const a = /password/i.test(q) ? '***' : raw.trim();
|
|
311
|
+
const a = /password|key|secret|token/i.test(q) ? '***' : raw.trim();
|
|
164
312
|
logger?.debug('setup: prompt', { q, a });
|
|
165
313
|
steps.push({ q, a });
|
|
166
314
|
}
|
|
@@ -191,23 +339,78 @@ async function runSetup(logger) {
|
|
|
191
339
|
console.log(' Path is required.');
|
|
192
340
|
} while (!workspaceInput.trim());
|
|
193
341
|
const workspaceRoot = expandHome(workspaceInput.trim());
|
|
194
|
-
|
|
342
|
+
console.log('\n--- Embedding backend ---');
|
|
343
|
+
console.log(' ollama — local, zero-setup (default)');
|
|
344
|
+
console.log(' infinity — local, one container serves embeddings + reranking (GPU/CPU)');
|
|
345
|
+
console.log(' openai-compatible — OpenAI cloud, or a local Infinity/TEI/vLLM server');
|
|
346
|
+
console.log(' voyage — Voyage AI cloud');
|
|
347
|
+
const validProviders = ['ollama', 'infinity', 'openai-compatible', 'voyage'];
|
|
348
|
+
let embProvider;
|
|
349
|
+
do {
|
|
350
|
+
embProvider = (await loggedPrompt('Provider (ollama/infinity/openai-compatible/voyage)', 'ollama'));
|
|
351
|
+
if (!validProviders.includes(embProvider))
|
|
352
|
+
console.log(` Invalid provider. Choose: ${validProviders.join(', ')}`);
|
|
353
|
+
} while (!validProviders.includes(embProvider));
|
|
354
|
+
const pd = PROVIDER_DEFAULTS[embProvider];
|
|
355
|
+
const embBaseURL = await loggedPrompt('Base URL', pd.baseURL);
|
|
356
|
+
const embModel = await loggedPrompt('Embedding model', pd.model);
|
|
357
|
+
// Infinity serves a reranker from the same container — offer to provision one.
|
|
358
|
+
let rerankModel = '';
|
|
359
|
+
if (embProvider === 'infinity') {
|
|
360
|
+
rerankModel = (await loggedPrompt(`Reranker model (served by the same Infinity container; blank to disable)`, DEFAULT_INFINITY_RERANK_MODEL)).trim();
|
|
361
|
+
}
|
|
362
|
+
// API keys go into .env (chmod 600); config.json only stores the env: reference.
|
|
363
|
+
let apiKeyRef = '';
|
|
364
|
+
if (!isLocalBackend(embProvider)) {
|
|
365
|
+
const key = (await loggedAsk(`API key (stored in ${config_store_1.ENV_FILE}, leave blank to set later): `)).trim();
|
|
366
|
+
if (key) {
|
|
367
|
+
writeEnvKey(pd.envVar, key);
|
|
368
|
+
apiKeyRef = `env:${pd.envVar}`;
|
|
369
|
+
console.log(` Saved API key to ${config_store_1.ENV_FILE} as ${pd.envVar} (config references env:${pd.envVar}).`);
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
apiKeyRef = `env:${pd.envVar}`;
|
|
373
|
+
console.log(` No key entered — set it later: echo '${pd.envVar}=sk-…' >> ${config_store_1.ENV_FILE}`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
// Ollama host used by the Docker helper below (only meaningful for ollama).
|
|
377
|
+
const ollamaHost = embProvider === 'ollama' ? embBaseURL : 'http://localhost:11434';
|
|
195
378
|
console.log('\n--- Database ---');
|
|
196
379
|
const dbHost = await loggedPrompt('DB host', 'localhost');
|
|
197
380
|
const dbPort = await loggedPrompt('DB port', '5433');
|
|
198
381
|
const dbUser = await loggedPrompt('DB user', 'dev');
|
|
199
382
|
const dbPass = await loggedPrompt('DB password', 'dev');
|
|
200
383
|
process.stdout.write('\nTesting connection...');
|
|
201
|
-
const
|
|
202
|
-
if (
|
|
384
|
+
const dbReachable = await checkRemoteDb(dbHost, dbPort, dbUser, dbPass, 'postgres', logger, steps);
|
|
385
|
+
if (dbReachable)
|
|
203
386
|
console.log(' Database is already reachable.');
|
|
204
|
-
|
|
387
|
+
// Symmetric to the DB probe: a reachable embedding backend defaults "start
|
|
388
|
+
// Docker?" to no. Cloud providers have no local backend to start.
|
|
389
|
+
process.stdout.write('Checking embedding backend...');
|
|
390
|
+
const embReachable = await checkEmbeddingBackend(embProvider, embBaseURL);
|
|
391
|
+
if (isLocalBackend(embProvider)) {
|
|
392
|
+
console.log(embReachable ? ` ${embProvider} is already reachable.` : ` ${embProvider} is not reachable — it can be started via Docker.`);
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
console.log(' cloud provider (no local backend to start).');
|
|
396
|
+
}
|
|
397
|
+
const dockerDefault = dockerPromptDefault(dbReachable, embReachable);
|
|
205
398
|
const startDockerRaw = await loggedAsk(`Start Docker containers automatically? (y/n) (${dockerDefault}): `);
|
|
206
399
|
const startDocker = (startDockerRaw.trim().toLowerCase() || dockerDefault) === 'y';
|
|
207
400
|
if (startDocker) {
|
|
208
|
-
await startDockerContainers(
|
|
401
|
+
await startDockerContainers({
|
|
402
|
+
provider: embProvider,
|
|
403
|
+
ollamaHost,
|
|
404
|
+
embBaseURL,
|
|
405
|
+
dbPort, dbUser, dbPass,
|
|
406
|
+
embModel,
|
|
407
|
+
rerankModel: rerankModel || undefined,
|
|
408
|
+
startDb: !dbReachable,
|
|
409
|
+
startBackend: isLocalBackend(embProvider) && !embReachable,
|
|
410
|
+
logger, steps,
|
|
411
|
+
});
|
|
209
412
|
}
|
|
210
|
-
else if (!
|
|
413
|
+
else if (!dbReachable) {
|
|
211
414
|
while (true) {
|
|
212
415
|
console.log('\nTesting connection...');
|
|
213
416
|
const ok = await checkRemoteDb(dbHost, dbPort, dbUser, dbPass, 'postgres', logger, steps);
|
|
@@ -242,10 +445,25 @@ async function runSetup(logger) {
|
|
|
242
445
|
user: dbVars.user,
|
|
243
446
|
password: dbVars.password,
|
|
244
447
|
},
|
|
245
|
-
|
|
448
|
+
embedding: {
|
|
449
|
+
provider: embProvider,
|
|
450
|
+
baseURL: embBaseURL,
|
|
451
|
+
model: embModel,
|
|
452
|
+
...(apiKeyRef ? { apiKey: apiKeyRef } : {}),
|
|
453
|
+
},
|
|
246
454
|
logging: { level: logLevel },
|
|
247
455
|
workspaceRoot,
|
|
248
456
|
};
|
|
457
|
+
// Infinity serves the reranker from the same container — auto-wire reranker.*
|
|
458
|
+
// to that server. `url` derives from provider + baseURL in the Config constructor.
|
|
459
|
+
if (embProvider === 'infinity' && rerankModel) {
|
|
460
|
+
cfg.reranker = {
|
|
461
|
+
enabled: true,
|
|
462
|
+
provider: 'openai-compatible',
|
|
463
|
+
baseURL: embBaseURL,
|
|
464
|
+
model: rerankModel,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
249
467
|
if (logDir)
|
|
250
468
|
cfg.logging.dir = logDir;
|
|
251
469
|
(0, config_store_1.writeConfigJson)(cfg);
|
|
@@ -18,7 +18,7 @@ class SearchCodebaseTool {
|
|
|
18
18
|
definition() {
|
|
19
19
|
return {
|
|
20
20
|
name: 'search_codebase',
|
|
21
|
-
description: 'Search local project codebase by semantic similarity',
|
|
21
|
+
description: 'Search local project codebase by semantic similarity. Each result shows File, Project, an optional Symbol breadcrumb (owning declaration, e.g. `IndexerService.ingestFile`), Line, the code, and Relevance.',
|
|
22
22
|
inputSchema: {
|
|
23
23
|
type: 'object',
|
|
24
24
|
properties: {
|
|
@@ -13,7 +13,8 @@ function errorResponse(text) {
|
|
|
13
13
|
return { isError: true, content: [{ type: 'text', text }] };
|
|
14
14
|
}
|
|
15
15
|
function formatChunk(r, label) {
|
|
16
|
-
|
|
16
|
+
const symbolLine = r.ownerPath ? `**Symbol:** ${r.ownerPath}\n` : '';
|
|
17
|
+
return `${label ? `*(${label})*\n` : ''}**File:** ${r.filePath}\n**Project:** ${r.serviceName}\n${symbolLine}**Line:** ${r.startLine}\n\`\`\`\n${r.content}\n\`\`\``;
|
|
17
18
|
}
|
|
18
19
|
function formatGraphContext(ctx) {
|
|
19
20
|
const parts = [];
|
|
@@ -176,7 +176,9 @@ function renderDashboardHtml() {
|
|
|
176
176
|
const isNum = spec.type === 'number' || spec.type === 'integer';
|
|
177
177
|
inp.dataset.key = key;
|
|
178
178
|
inp.dataset.type = isNum ? 'number' : 'string';
|
|
179
|
+
const isEnvRef = typeof value === 'string' && value.startsWith('env:');
|
|
179
180
|
if (key === 'db.password') inp.type = 'password';
|
|
181
|
+
else if (key.endsWith('.apiKey')) inp.type = isEnvRef ? 'text' : 'password';
|
|
180
182
|
else if (isNum) {
|
|
181
183
|
inp.type = 'number';
|
|
182
184
|
// Only apply small, meaningful bounds (skip zod's safe-integer noise).
|
|
@@ -197,6 +199,9 @@ function renderDashboardHtml() {
|
|
|
197
199
|
const label = document.createElement('label');
|
|
198
200
|
let html = key.split('.').pop();
|
|
199
201
|
if (spec.description) html += '<span class="desc">' + spec.description + '</span>';
|
|
202
|
+
if (key.endsWith('.apiKey') && typeof value === 'string' && value.startsWith('env:')) {
|
|
203
|
+
html += '<span class="defval using">✔ set via ' + value.slice(4) + '</span>';
|
|
204
|
+
}
|
|
200
205
|
if (def !== undefined) {
|
|
201
206
|
html += '<span class="defval' + (isUnset ? ' using' : '') + '">'
|
|
202
207
|
+ (isUnset ? '↪ using default: ' : 'default: ') + fmtDefault(def) + '</span>';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# GPU override for the Infinity service. Layered on top of docker-compose.yml
|
|
2
|
+
# (docker compose -f docker-compose.yml -f docker-compose.infinity-gpu.yml …)
|
|
3
|
+
# only when an NVIDIA GPU + container runtime is detected. Kept separate because
|
|
4
|
+
# a `deploy.reservations.devices` GPU request aborts startup on GPU-less hosts;
|
|
5
|
+
# the base file therefore stays CPU-only and always starts.
|
|
6
|
+
services:
|
|
7
|
+
infinity:
|
|
8
|
+
# Warmup is cheap on GPU (seconds) — re-enable it so the first real request
|
|
9
|
+
# is already fast. On CPU (base file only) it stays off.
|
|
10
|
+
environment:
|
|
11
|
+
INFINITY_MODEL_WARMUP: "true"
|
|
12
|
+
deploy:
|
|
13
|
+
resources:
|
|
14
|
+
reservations:
|
|
15
|
+
devices:
|
|
16
|
+
- driver: nvidia
|
|
17
|
+
count: all
|
|
18
|
+
capabilities:
|
|
19
|
+
- gpu
|
|
@@ -19,6 +19,31 @@ services:
|
|
|
19
19
|
volumes:
|
|
20
20
|
- ollama_data:/root/.ollama
|
|
21
21
|
|
|
22
|
+
# One Infinity container serves both /embeddings and /rerank. The model set is
|
|
23
|
+
# bound at startup via INFINITY_MODEL_ID (";"-separated for multiple models,
|
|
24
|
+
# e.g. "BAAI/bge-m3;BAAI/bge-reranker-v2-m3"). Defaults to CPU so it starts on
|
|
25
|
+
# any host; GPU is opt-in via docker-compose.infinity-gpu.yml (see rag-setup).
|
|
26
|
+
# Behind an "infinity" profile so `docker compose up -d` (ollama path) doesn't
|
|
27
|
+
# pull this multi-GB image — it starts only when explicitly targeted.
|
|
28
|
+
# Model warmup defaults OFF here: on CPU it runs multi-minute benchmark
|
|
29
|
+
# batches at every boot (a pure perf measurement, not required to serve). The
|
|
30
|
+
# GPU override turns it back on, where it costs seconds.
|
|
31
|
+
infinity:
|
|
32
|
+
image: michaelf34/infinity:latest
|
|
33
|
+
restart: unless-stopped
|
|
34
|
+
ports:
|
|
35
|
+
- "7997:7997"
|
|
36
|
+
environment:
|
|
37
|
+
INFINITY_MODEL_ID: ${INFINITY_MODEL_ID:-jinaai/jina-embeddings-v2-base-code}
|
|
38
|
+
INFINITY_MODEL_WARMUP: ${INFINITY_MODEL_WARMUP:-false}
|
|
39
|
+
INFINITY_PORT: "7997"
|
|
40
|
+
command: ["v2"]
|
|
41
|
+
volumes:
|
|
42
|
+
- infinity_cache:/app/.cache
|
|
43
|
+
profiles:
|
|
44
|
+
- infinity
|
|
45
|
+
|
|
22
46
|
volumes:
|
|
23
47
|
pgdata:
|
|
24
48
|
ollama_data:
|
|
49
|
+
infinity_cache:
|
package/package.json
CHANGED
|
@@ -1,8 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codesearch-mcp",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0-alpha",
|
|
4
4
|
"description": "Local semantic codebase search via MCP — indexes projects into pgvector, exposes search to Claude",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"mcp",
|
|
8
|
+
"model-context-protocol",
|
|
9
|
+
"rag",
|
|
10
|
+
"semantic-search",
|
|
11
|
+
"code-search",
|
|
12
|
+
"pgvector",
|
|
13
|
+
"embeddings",
|
|
14
|
+
"claude",
|
|
15
|
+
"ollama",
|
|
16
|
+
"infinity"
|
|
17
|
+
],
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=18"
|
|
20
|
+
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"tag": "alpha"
|
|
23
|
+
},
|
|
6
24
|
"main": "dist/app.js",
|
|
7
25
|
"bin": {
|
|
8
26
|
"rag-index": "dist/app.js",
|
|
@@ -19,9 +37,10 @@
|
|
|
19
37
|
"CHANGELOG.md"
|
|
20
38
|
],
|
|
21
39
|
"scripts": {
|
|
40
|
+
"prebuild": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
22
41
|
"build": "tsc",
|
|
23
42
|
"postbuild": "node -e \"const fs=require('fs');const f='dist/app.js';fs.writeFileSync(f,'#!/usr/bin/env node\\n'+fs.readFileSync(f,'utf8'));fs.chmodSync(f,0o755);\"",
|
|
24
|
-
"prepublishOnly": "npm run build && npm run typecheck",
|
|
43
|
+
"prepublishOnly": "npm run build && npm run typecheck && npm test",
|
|
25
44
|
"start:mcp": "ts-node app.ts mcp",
|
|
26
45
|
"start:web": "ts-node app.ts web",
|
|
27
46
|
"index:workspace": "ts-node app.ts index",
|
package/dist/src/ast.d.ts
DELETED
package/dist/src/ast.js
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.extractImportEdges = extractImportEdges;
|
|
37
|
-
const fs = __importStar(require("fs"));
|
|
38
|
-
const path = __importStar(require("path"));
|
|
39
|
-
const ts_morph_1 = require("ts-morph");
|
|
40
|
-
// Per-project ts-morph instances keyed by projectRoot. Each project gets its own instance so that
|
|
41
|
-
// projects without a tsconfig (key previously shared as '') don't leak files into each other.
|
|
42
|
-
// Loading the tsconfig gives ts-morph the compilerOptions.paths needed to resolve aliases like @core/*.
|
|
43
|
-
const morphProjects = new Map();
|
|
44
|
-
const FALLBACK_OPTS = { skipAddingFilesFromTsConfig: true, skipFileDependencyResolution: false, compilerOptions: { allowJs: true, noEmit: true } };
|
|
45
|
-
function getMorphProject(projectRoot) {
|
|
46
|
-
if (!morphProjects.has(projectRoot)) {
|
|
47
|
-
const tsconfigPath = path.join(projectRoot, 'tsconfig.json');
|
|
48
|
-
const hasTsconfig = fs.existsSync(tsconfigPath);
|
|
49
|
-
let project;
|
|
50
|
-
try {
|
|
51
|
-
const opts = hasTsconfig
|
|
52
|
-
? { tsConfigFilePath: tsconfigPath, skipAddingFilesFromTsConfig: true, skipFileDependencyResolution: false }
|
|
53
|
-
: FALLBACK_OPTS;
|
|
54
|
-
project = new ts_morph_1.Project(opts);
|
|
55
|
-
}
|
|
56
|
-
catch {
|
|
57
|
-
// tsconfig is invalid — fall back so other files in the project can still be processed
|
|
58
|
-
project = new ts_morph_1.Project(FALLBACK_OPTS);
|
|
59
|
-
}
|
|
60
|
-
morphProjects.set(projectRoot, project);
|
|
61
|
-
}
|
|
62
|
-
return morphProjects.get(projectRoot);
|
|
63
|
-
}
|
|
64
|
-
// Extract import edges from a TS/JS file using ts-morph.
|
|
65
|
-
// projectRoot must be the project root (where tsconfig.json lives) for alias resolution to work.
|
|
66
|
-
// Defaults to the file's directory — callers that need alias resolution must pass the project root explicitly.
|
|
67
|
-
// Removes the source file from the morph project after parsing to prevent memory accumulation.
|
|
68
|
-
async function extractImportEdges(filePath, serviceName, projectRoot = path.dirname(filePath)) {
|
|
69
|
-
// Only process JS/TS files; skip declaration files
|
|
70
|
-
if (!/\.(ts|tsx|js|jsx)$/.test(filePath) || /\.d\.ts$/.test(filePath))
|
|
71
|
-
return [];
|
|
72
|
-
const morphProject = getMorphProject(projectRoot);
|
|
73
|
-
// Remove stale cached version before re-adding
|
|
74
|
-
const existing = morphProject.getSourceFile(filePath);
|
|
75
|
-
if (existing)
|
|
76
|
-
morphProject.removeSourceFile(existing);
|
|
77
|
-
let sourceFile;
|
|
78
|
-
try {
|
|
79
|
-
sourceFile = morphProject.addSourceFileAtPath(filePath);
|
|
80
|
-
}
|
|
81
|
-
catch {
|
|
82
|
-
return []; // file disappeared between stat and parse
|
|
83
|
-
}
|
|
84
|
-
const edges = [];
|
|
85
|
-
for (const decl of sourceFile.getImportDeclarations()) {
|
|
86
|
-
const resolved = decl.getModuleSpecifierSourceFile();
|
|
87
|
-
if (!resolved)
|
|
88
|
-
continue; // external package or unresolvable
|
|
89
|
-
const toFile = resolved.getFilePath();
|
|
90
|
-
if (toFile.includes('/node_modules/'))
|
|
91
|
-
continue; // skip package type declarations
|
|
92
|
-
const namedImports = decl.getNamedImports();
|
|
93
|
-
const defaultImport = decl.getDefaultImport();
|
|
94
|
-
const namespaceImport = decl.getNamespaceImport();
|
|
95
|
-
if (namedImports.length > 0) {
|
|
96
|
-
for (const named of namedImports) {
|
|
97
|
-
edges.push({ project: serviceName, fromFile: filePath, fromSymbol: null, toFile, toSymbol: named.getName(), depType: 'import' });
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
else if (defaultImport) {
|
|
101
|
-
edges.push({ project: serviceName, fromFile: filePath, fromSymbol: null, toFile, toSymbol: null, depType: 'import-default' });
|
|
102
|
-
}
|
|
103
|
-
else if (namespaceImport) {
|
|
104
|
-
edges.push({ project: serviceName, fromFile: filePath, fromSymbol: null, toFile, toSymbol: null, depType: 'import-namespace' });
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
// Remove from memory — prevents accumulation across large workspaces
|
|
108
|
-
morphProject.removeSourceFile(sourceFile);
|
|
109
|
-
return edges;
|
|
110
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.SetupCommand = void 0;
|
|
4
|
-
const setup_1 = require("../setup");
|
|
5
|
-
class SetupCommand {
|
|
6
|
-
container;
|
|
7
|
-
constructor(container) {
|
|
8
|
-
this.container = container;
|
|
9
|
-
}
|
|
10
|
-
async execute() {
|
|
11
|
-
await (0, setup_1.runSetup)(this.container.logger);
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
exports.SetupCommand = SetupCommand;
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.db = void 0;
|
|
4
|
-
const typeorm_1 = require("typeorm");
|
|
5
|
-
const config_1 = require("../config");
|
|
6
|
-
const code_chunk_entity_1 = require("./entities/code-chunk.entity");
|
|
7
|
-
const code_dependency_entity_1 = require("./entities/code-dependency.entity");
|
|
8
|
-
exports.db = new typeorm_1.DataSource({
|
|
9
|
-
type: 'postgres',
|
|
10
|
-
...config_1.config.db,
|
|
11
|
-
entities: [code_chunk_entity_1.CodeChunk, code_dependency_entity_1.CodeDependency],
|
|
12
|
-
synchronize: false,
|
|
13
|
-
});
|