codesearch-mcp 0.1.0-alpha → 0.2.1-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 +39 -0
- package/README.md +146 -41
- package/dist/app.js +4 -19
- package/dist/src/commands/IndexCommand.js +68 -7
- 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/db/index.d.ts +1 -1
- package/dist/src/db/index.js +2 -1
- package/dist/src/db/schema.d.ts +75 -0
- package/dist/src/db/schema.js +10 -1
- package/dist/src/enriched-prompt.d.ts +8 -0
- package/dist/src/enriched-prompt.js +87 -0
- package/dist/src/help.js +1 -1
- package/dist/src/interfaces/IAstParser.d.ts +18 -2
- package/dist/src/interfaces/IDatabase.d.ts +31 -0
- package/dist/src/interfaces/ILogger.d.ts +7 -0
- package/dist/src/interfaces/ISearcher.d.ts +3 -0
- package/dist/src/services/AstChunker.d.ts +11 -0
- package/dist/src/services/AstChunker.js +44 -0
- package/dist/src/services/AstParserService.d.ts +39 -7
- package/dist/src/services/AstParserService.js +504 -104
- package/dist/src/services/DatabaseService.d.ts +17 -0
- package/dist/src/services/DatabaseService.js +130 -2
- 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 -14
- package/dist/src/services/IndexerService.js +54 -156
- 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/McpServer.js +3 -3
- package/dist/src/services/SearcherService.d.ts +39 -4
- package/dist/src/services/SearcherService.js +254 -81
- 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/dist/src/symbol-name.d.ts +44 -0
- package/dist/src/symbol-name.js +68 -0
- package/dist/src/symbol-rows.d.ts +12 -0
- package/dist/src/symbol-rows.js +38 -0
- package/dist/src/tools/FindSymbolTool.js +30 -7
- package/dist/src/tools/GetCallersTool.js +20 -5
- package/docker/docker-compose.yml +5 -15
- package/docs/architecture.svg +103 -0
- package/docs/web-dashboard.md +36 -17
- package/package.json +1 -6
- package/dist/src/commands/BackfillCommand.d.ts +0 -7
- package/dist/src/commands/BackfillCommand.js +0 -37
- 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());
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export declare const BARE_NAME_RE: RegExp;
|
|
2
|
+
export declare const QUALIFIED_NAME_RE: RegExp;
|
|
3
|
+
export type FindSymbolQuery = {
|
|
4
|
+
form: 'bare';
|
|
5
|
+
name: string;
|
|
6
|
+
} | {
|
|
7
|
+
form: 'qualified';
|
|
8
|
+
container: string;
|
|
9
|
+
name: string;
|
|
10
|
+
} | {
|
|
11
|
+
form: 'quoted';
|
|
12
|
+
container: string | null;
|
|
13
|
+
name: string;
|
|
14
|
+
} | {
|
|
15
|
+
form: 'computed';
|
|
16
|
+
container: string | null;
|
|
17
|
+
name: string;
|
|
18
|
+
};
|
|
19
|
+
/** Split `P.n` into container `P` and last segment `n`. Requires a qualified name. */
|
|
20
|
+
export declare function splitQualified(input: string): {
|
|
21
|
+
container: string;
|
|
22
|
+
name: string;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Parse a `find_symbol` name into one of the four accepted forms (bare,
|
|
26
|
+
* qualified, quoted, computed), or null when it matches none — the caller
|
|
27
|
+
* should reject with `Invalid symbol name` without querying anything.
|
|
28
|
+
*/
|
|
29
|
+
export declare function parseFindSymbolName(input: string): FindSymbolQuery | null;
|
|
30
|
+
/**
|
|
31
|
+
* Parse a `get_callers` symbol into bare or qualified form only — the quoted
|
|
32
|
+
* and computed forms of `find_symbol` are meaningless here (calls through
|
|
33
|
+
* element access are never recorded in the call graph).
|
|
34
|
+
*/
|
|
35
|
+
export declare function parseCallerSymbol(input: string): {
|
|
36
|
+
form: 'bare';
|
|
37
|
+
name: string;
|
|
38
|
+
} | {
|
|
39
|
+
form: 'qualified';
|
|
40
|
+
container: string;
|
|
41
|
+
name: string;
|
|
42
|
+
} | null;
|
|
43
|
+
/** Escape `%`, `_` and `\` for a `LIKE ... ESCAPE '\'` pattern. */
|
|
44
|
+
export declare function escapeLike(s: string): string;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.QUALIFIED_NAME_RE = exports.BARE_NAME_RE = void 0;
|
|
4
|
+
exports.splitQualified = splitQualified;
|
|
5
|
+
exports.parseFindSymbolName = parseFindSymbolName;
|
|
6
|
+
exports.parseCallerSymbol = parseCallerSymbol;
|
|
7
|
+
exports.escapeLike = escapeLike;
|
|
8
|
+
exports.BARE_NAME_RE = /^#?[\w$]+$/;
|
|
9
|
+
exports.QUALIFIED_NAME_RE = /^#?[\w$]+(\.#?[\w$]+)+$/;
|
|
10
|
+
const PFX_SRC = '(?:#?[\\w$]+(?:\\.#?[\\w$]+)*\\.)?';
|
|
11
|
+
const QUOTED_RE = new RegExp(`^(${PFX_SRC})('[^'\\r\\n]+'|"[^"\\r\\n]+")$`);
|
|
12
|
+
const COMPUTED_RE = new RegExp(`^(${PFX_SRC})(\\[[^\\]\\r\\n]+\\])$`);
|
|
13
|
+
const MAX_QUOTED_COMPUTED_LEN = 256;
|
|
14
|
+
/** Strip a qualified name's trailing `.` container prefix, if present. */
|
|
15
|
+
function stripTrailingDot(prefix) {
|
|
16
|
+
if (!prefix)
|
|
17
|
+
return null;
|
|
18
|
+
return prefix.slice(0, -1);
|
|
19
|
+
}
|
|
20
|
+
/** Split `P.n` into container `P` and last segment `n`. Requires a qualified name. */
|
|
21
|
+
function splitQualified(input) {
|
|
22
|
+
const idx = input.lastIndexOf('.');
|
|
23
|
+
return { container: input.slice(0, idx), name: input.slice(idx + 1) };
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Parse a `find_symbol` name into one of the four accepted forms (bare,
|
|
27
|
+
* qualified, quoted, computed), or null when it matches none — the caller
|
|
28
|
+
* should reject with `Invalid symbol name` without querying anything.
|
|
29
|
+
*/
|
|
30
|
+
function parseFindSymbolName(input) {
|
|
31
|
+
if (input.length <= MAX_QUOTED_COMPUTED_LEN) {
|
|
32
|
+
const quoted = input.match(QUOTED_RE);
|
|
33
|
+
if (quoted) {
|
|
34
|
+
return { form: 'quoted', container: stripTrailingDot(quoted[1]), name: quoted[2].slice(1, -1) };
|
|
35
|
+
}
|
|
36
|
+
const computed = input.match(COMPUTED_RE);
|
|
37
|
+
if (computed) {
|
|
38
|
+
return { form: 'computed', container: stripTrailingDot(computed[1]), name: computed[2].replace(/\s+/g, '') };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (exports.QUALIFIED_NAME_RE.test(input)) {
|
|
42
|
+
const { container, name } = splitQualified(input);
|
|
43
|
+
return { form: 'qualified', container, name };
|
|
44
|
+
}
|
|
45
|
+
if (exports.BARE_NAME_RE.test(input)) {
|
|
46
|
+
return { form: 'bare', name: input };
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Parse a `get_callers` symbol into bare or qualified form only — the quoted
|
|
52
|
+
* and computed forms of `find_symbol` are meaningless here (calls through
|
|
53
|
+
* element access are never recorded in the call graph).
|
|
54
|
+
*/
|
|
55
|
+
function parseCallerSymbol(input) {
|
|
56
|
+
if (exports.QUALIFIED_NAME_RE.test(input)) {
|
|
57
|
+
const { container, name } = splitQualified(input);
|
|
58
|
+
return { form: 'qualified', container, name };
|
|
59
|
+
}
|
|
60
|
+
if (exports.BARE_NAME_RE.test(input)) {
|
|
61
|
+
return { form: 'bare', name: input };
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
/** Escape `%`, `_` and `\` for a `LIKE ... ESCAPE '\'` pattern. */
|
|
66
|
+
function escapeLike(s) {
|
|
67
|
+
return s.replace(/\\/g, '\\\\').replace(/%/g, '\\%').replace(/_/g, '\\_');
|
|
68
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { SymbolEntry } from './interfaces/IAstParser';
|
|
2
|
+
export type CodeSymbolRow = Pick<SymbolEntry, 'project' | 'filePath' | 'name' | 'kind' | 'signature' | 'line' | 'col' | 'container' | 'isStatic'>;
|
|
3
|
+
/**
|
|
4
|
+
* Combine top-level symbols and members into the rows written to `code_symbol`.
|
|
5
|
+
* Overload signatures of one declaration — same `(container, name, kind,
|
|
6
|
+
* isStatic)` — collapse to a single representative row: the one with a body
|
|
7
|
+
* (the implementation) when present, otherwise the earliest by line (the first
|
|
8
|
+
* signature in source order). Static and instance members sharing a name are
|
|
9
|
+
* never collapsed into each other (`isStatic` is part of the group key). A
|
|
10
|
+
* final dedup by `(container, name, kind, line)` matches the DB's unique index.
|
|
11
|
+
*/
|
|
12
|
+
export declare function buildSymbolRows(symbols: SymbolEntry[], members: SymbolEntry[]): CodeSymbolRow[];
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildSymbolRows = buildSymbolRows;
|
|
4
|
+
/**
|
|
5
|
+
* Combine top-level symbols and members into the rows written to `code_symbol`.
|
|
6
|
+
* Overload signatures of one declaration — same `(container, name, kind,
|
|
7
|
+
* isStatic)` — collapse to a single representative row: the one with a body
|
|
8
|
+
* (the implementation) when present, otherwise the earliest by line (the first
|
|
9
|
+
* signature in source order). Static and instance members sharing a name are
|
|
10
|
+
* never collapsed into each other (`isStatic` is part of the group key). A
|
|
11
|
+
* final dedup by `(container, name, kind, line)` matches the DB's unique index.
|
|
12
|
+
*/
|
|
13
|
+
function buildSymbolRows(symbols, members) {
|
|
14
|
+
const groups = new Map();
|
|
15
|
+
for (const entry of [...symbols, ...members]) {
|
|
16
|
+
const key = JSON.stringify([entry.container, entry.name, entry.kind, entry.isStatic]);
|
|
17
|
+
const group = groups.get(key);
|
|
18
|
+
if (group)
|
|
19
|
+
group.push(entry);
|
|
20
|
+
else
|
|
21
|
+
groups.set(key, [entry]);
|
|
22
|
+
}
|
|
23
|
+
const collapsed = [];
|
|
24
|
+
for (const group of groups.values()) {
|
|
25
|
+
if (group.length === 1) {
|
|
26
|
+
collapsed.push(group[0]);
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const implementation = group.find(e => e.hasBody);
|
|
30
|
+
collapsed.push(implementation ?? group.reduce((a, b) => ((a.line ?? 0) <= (b.line ?? 0) ? a : b)));
|
|
31
|
+
}
|
|
32
|
+
const dedup = new Map();
|
|
33
|
+
for (const entry of collapsed) {
|
|
34
|
+
const key = JSON.stringify([entry.container, entry.name, entry.kind, entry.line]);
|
|
35
|
+
dedup.set(key, entry);
|
|
36
|
+
}
|
|
37
|
+
return [...dedup.values()].map(({ project, filePath, name, kind, signature, line, col, container, isStatic }) => ({ project, filePath, name, kind, signature, line, col, container, isStatic }));
|
|
38
|
+
}
|
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.FindSymbolTool = void 0;
|
|
4
4
|
const format_helpers_1 = require("./format-helpers");
|
|
5
|
-
const VALID_KINDS = [
|
|
5
|
+
const VALID_KINDS = [
|
|
6
|
+
'function', 'class', 'type', 'interface', 'variable',
|
|
7
|
+
'method', 'get_accessor', 'set_accessor', 'constructor', 'function_property', 'method_signature',
|
|
8
|
+
'all',
|
|
9
|
+
];
|
|
6
10
|
class FindSymbolTool {
|
|
7
11
|
searcher;
|
|
8
12
|
constructor(searcher) {
|
|
@@ -11,11 +15,16 @@ class FindSymbolTool {
|
|
|
11
15
|
definition() {
|
|
12
16
|
return {
|
|
13
17
|
name: 'find_symbol',
|
|
14
|
-
description: 'Find TypeScript/JavaScript symbol definitions
|
|
18
|
+
description: 'Find TypeScript/JavaScript symbol definitions by exact name: top-level declarations (functions, classes, types, ' +
|
|
19
|
+
'interfaces, variables), class/object-literal members (methods, get/set accessors, constructors, function ' +
|
|
20
|
+
'properties), interface and type-literal method signatures, and namespace-level declarations. Accepts a bare name ' +
|
|
21
|
+
'(any container), a qualified name (`Class.method`, `NS.Class.method`), a quoted name for string-literal members ' +
|
|
22
|
+
"(`Router.'kebab-name'`), or a computed name (`Stack.[Symbol.iterator]`). Use when you know the symbol name and " +
|
|
23
|
+
'want its definition location fast.',
|
|
15
24
|
inputSchema: {
|
|
16
25
|
type: 'object',
|
|
17
26
|
properties: {
|
|
18
|
-
name: { type: 'string', description: '
|
|
27
|
+
name: { type: 'string', description: 'Bare, qualified (Class.method), quoted, or computed symbol name (case-sensitive)' },
|
|
19
28
|
project: { type: 'string', description: 'Limit search to this project (optional). Use list_projects to find names.' },
|
|
20
29
|
kind: { type: 'string', enum: VALID_KINDS, description: 'Symbol kind filter. Default: all.' },
|
|
21
30
|
},
|
|
@@ -28,15 +37,29 @@ class FindSymbolTool {
|
|
|
28
37
|
const project = args.project ? String(args.project) : null;
|
|
29
38
|
const rawKind = args.kind;
|
|
30
39
|
const kind = VALID_KINDS.includes(rawKind) ? rawKind : 'all';
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
40
|
+
let parsed;
|
|
41
|
+
try {
|
|
42
|
+
parsed = await this.searcher.findSymbol(name, project, kind);
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
return (0, format_helpers_1.errorResponse)(err.message);
|
|
34
46
|
}
|
|
47
|
+
const { hits, truncated, grepFailed } = parsed;
|
|
48
|
+
const notice = await this.searcher.outdatedNotice(project);
|
|
35
49
|
const lines = [];
|
|
50
|
+
if (notice)
|
|
51
|
+
lines.push(notice + '\n');
|
|
52
|
+
if (hits.length === 0) {
|
|
53
|
+
const suffix = grepFailed ? ' (grep fallback failed — see log)' : '';
|
|
54
|
+
lines.push(`No definitions found for symbol: ${name}${suffix}`);
|
|
55
|
+
return (0, format_helpers_1.textResponse)(lines.join('\n'));
|
|
56
|
+
}
|
|
36
57
|
if (truncated)
|
|
37
58
|
lines.push(`*(truncated to ${hits.length} results)*\n`);
|
|
38
59
|
for (const h of hits) {
|
|
39
|
-
|
|
60
|
+
const staticTag = h.isStatic ? ' static' : '';
|
|
61
|
+
const grepTag = h.source === 'grep' ? ' (grep — not in index; may be stale or imprecise)' : '';
|
|
62
|
+
lines.push(`**${h.kind}** \`${h.qualified}\`${staticTag} \`${h.filePath}\` line ${h.line}, col ${h.column}${grepTag}`);
|
|
40
63
|
lines.push(`\`\`\`\n${h.signature}\n\`\`\``);
|
|
41
64
|
}
|
|
42
65
|
return (0, format_helpers_1.textResponse)(lines.join('\n'));
|