codesearch-mcp 0.0.18-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 +47 -1
- package/README.md +194 -23
- package/dist/app.js +38 -2
- package/dist/src/commands/IndexCommand.js +55 -1
- package/dist/src/commands/McpCommand.js +6 -2
- package/dist/src/config-error.d.ts +7 -0
- package/dist/src/config-error.js +14 -0
- package/dist/src/config-store.d.ts +73 -17
- package/dist/src/config-store.js +142 -27
- package/dist/src/config.d.ts +36 -11
- package/dist/src/config.js +89 -27
- package/dist/src/constants.d.ts +42 -2
- package/dist/src/constants.js +61 -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 +6 -5
- 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 +31 -0
- package/dist/src/interfaces/IEmbedder.d.ts +14 -2
- package/dist/src/interfaces/ILogger.d.ts +7 -0
- 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 +42 -0
- package/dist/src/services/AstChunker.js +158 -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 +17 -1
- package/dist/src/services/DatabaseService.js +116 -25
- package/dist/src/services/EmbedderFactory.d.ts +9 -0
- package/dist/src/services/EmbedderFactory.js +26 -0
- package/dist/src/services/IndexerService.d.ts +21 -5
- package/dist/src/services/IndexerService.js +127 -44
- package/dist/src/services/InfinityEmbedder.d.ts +38 -0
- package/dist/src/services/InfinityEmbedder.js +88 -0
- package/dist/src/services/LoggerService.d.ts +3 -1
- package/dist/src/services/LoggerService.js +17 -3
- package/dist/src/services/OpenAICompatibleEmbedder.d.ts +25 -0
- package/dist/src/services/OpenAICompatibleEmbedder.js +75 -0
- package/dist/src/services/SearcherService.d.ts +21 -1
- package/dist/src/services/SearcherService.js +129 -28
- package/dist/src/services/VoyageEmbedder.d.ts +22 -0
- package/dist/src/services/VoyageEmbedder.js +72 -0
- package/dist/src/services/WebServerService.d.ts +57 -14
- package/dist/src/services/WebServerService.js +134 -26
- package/dist/src/services/http-retry.d.ts +19 -0
- package/dist/src/services/http-retry.js +54 -0
- 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 +15 -0
- package/dist/src/services/reranker.js +44 -0
- package/dist/src/setup.d.ts +43 -0
- package/dist/src/setup.js +227 -55
- 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 +20 -5
- package/docs/web-dashboard.md +36 -17
- package/package.json +18 -3
- 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/src/services/OllamaEmbedder.d.ts +0 -14
- package/dist/src/services/OllamaEmbedder.js +0 -63
- package/dist/vitest.config.d.ts +0 -2
- package/dist/vitest.config.js +0 -9
package/dist/src/setup.d.ts
CHANGED
|
@@ -1,2 +1,45 @@
|
|
|
1
|
+
import { type EmbeddingProvider } from './constants';
|
|
2
|
+
import { Config } from './config';
|
|
3
|
+
import { type ProvisioningDeps } from './services/infinity-provisioning';
|
|
1
4
|
import type { LoggerService } from './services/LoggerService';
|
|
5
|
+
type SetupStep = {
|
|
6
|
+
q: string;
|
|
7
|
+
a: string;
|
|
8
|
+
};
|
|
9
|
+
/** Providers the wizard offers, in prompt order; the first is the preselected default. Exported for unit testing. */
|
|
10
|
+
export declare const EMBEDDING_PROVIDER_CHOICES: readonly EmbeddingProvider[];
|
|
11
|
+
/**
|
|
12
|
+
* Default answer for the "start Docker containers?" prompt: no only when both
|
|
13
|
+
* the database and the embedding backend are already reachable. Pure — exported
|
|
14
|
+
* for unit testing.
|
|
15
|
+
*/
|
|
16
|
+
export declare function dockerPromptDefault(dbReachable: boolean, embReachable: boolean): 'y' | 'n';
|
|
17
|
+
interface DbDockerOpts {
|
|
18
|
+
dbPort: string;
|
|
19
|
+
dbUser: string;
|
|
20
|
+
dbPass: string;
|
|
21
|
+
/** Bring up the local db container (skip when a remote db is already reachable). */
|
|
22
|
+
startDb: boolean;
|
|
23
|
+
logger?: LoggerService;
|
|
24
|
+
steps?: SetupStep[];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Regenerate `CONFIG_DIR/docker-compose.yml` from the shipped template
|
|
28
|
+
* (carrying over an existing literal `INFINITY_MODEL_ID`, written atomically),
|
|
29
|
+
* copy the GPU override, and start the DB container when requested. Runs
|
|
30
|
+
* BEFORE the config file is written — its failure aborts setup unsaved,
|
|
31
|
+
* matching the existing "abort without saving" behavior of the DB step. Does
|
|
32
|
+
* NOT touch Infinity provisioning (moved after `writeConfigJson` — see D6a).
|
|
33
|
+
*/
|
|
34
|
+
export declare function prepareComposeAndStartDb(opts: DbDockerOpts): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Infinity provisioning (D6a) — runs AFTER the config file is written, so no
|
|
37
|
+
* outcome here can leave the config unsaved. `savedConfig` is a fresh read of
|
|
38
|
+
* the just-written file (never the process's own startup config). Exit-status
|
|
39
|
+
* rules: 0 for served / timeout (warning) / external instructions / declined
|
|
40
|
+
* Docker; 1 for any other provisioning failure — the cause is printed and the
|
|
41
|
+
* remaining setup steps (DB schema init) still run.
|
|
42
|
+
*/
|
|
43
|
+
export declare function provisionInfinity(savedConfig: Config, startDocker: boolean, logger?: LoggerService, deps?: ProvisioningDeps, composePathOverride?: string): Promise<void>;
|
|
2
44
|
export declare function runSetup(logger?: LoggerService): Promise<void>;
|
|
45
|
+
export {};
|
package/dist/src/setup.js
CHANGED
|
@@ -33,21 +33,27 @@ 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;
|
|
37
|
+
exports.dockerPromptDefault = dockerPromptDefault;
|
|
38
|
+
exports.prepareComposeAndStartDb = prepareComposeAndStartDb;
|
|
39
|
+
exports.provisionInfinity = provisionInfinity;
|
|
36
40
|
exports.runSetup = runSetup;
|
|
37
41
|
const readline = __importStar(require("readline"));
|
|
38
42
|
const fs = __importStar(require("fs"));
|
|
39
43
|
const path = __importStar(require("path"));
|
|
40
44
|
const os = __importStar(require("os"));
|
|
41
45
|
const child_process_1 = require("child_process");
|
|
42
|
-
const constants_1 = require("./constants");
|
|
43
46
|
const config_store_1 = require("./config-store");
|
|
44
47
|
const config_1 = require("./config");
|
|
48
|
+
const infinity_provisioning_1 = require("./services/infinity-provisioning");
|
|
45
49
|
const DEFAULT_LOG_DIR = path.join(config_store_1.CONFIG_DIR, 'logs');
|
|
46
50
|
const _projectRoot = fs.existsSync(path.join(__dirname, '..', 'package.json'))
|
|
47
51
|
? path.join(__dirname, '..')
|
|
48
52
|
: path.join(__dirname, '..', '..');
|
|
49
53
|
const DOCKER_COMPOSE_SRC = path.join(_projectRoot, 'docker', 'docker-compose.yml');
|
|
50
54
|
const DOCKER_COMPOSE_DST = path.join(config_store_1.CONFIG_DIR, 'docker-compose.yml');
|
|
55
|
+
const DOCKER_GPU_OVERRIDE_SRC = path.join(_projectRoot, 'docker', 'docker-compose.infinity-gpu.yml');
|
|
56
|
+
const DOCKER_GPU_OVERRIDE_DST = path.join(config_store_1.CONFIG_DIR, 'docker-compose.infinity-gpu.yml');
|
|
51
57
|
function ask(rl, question) {
|
|
52
58
|
return new Promise(resolve => rl.question(question, resolve));
|
|
53
59
|
}
|
|
@@ -60,6 +66,54 @@ function expandHome(p) {
|
|
|
60
66
|
return path.join(os.homedir(), p.slice(2));
|
|
61
67
|
return p;
|
|
62
68
|
}
|
|
69
|
+
const PROVIDER_DEFAULTS = {
|
|
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 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'];
|
|
76
|
+
/** Providers that run a local inference backend we can probe / provision. */
|
|
77
|
+
function isLocalBackend(p) {
|
|
78
|
+
return p === 'infinity';
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Probe whether the embedding backend is already reachable, so the "start
|
|
82
|
+
* Docker?" prompt can default to no (mirrors the DB `checkRemoteDb` flow).
|
|
83
|
+
* Cloud providers have no local backend → always "reachable" (nothing to start).
|
|
84
|
+
*/
|
|
85
|
+
async function checkEmbeddingBackend(provider, baseURL) {
|
|
86
|
+
if (!isLocalBackend(provider))
|
|
87
|
+
return true;
|
|
88
|
+
return (0, infinity_provisioning_1.probeInfinityHealth)(baseURL);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Default answer for the "start Docker containers?" prompt: no only when both
|
|
92
|
+
* the database and the embedding backend are already reachable. Pure — exported
|
|
93
|
+
* for unit testing.
|
|
94
|
+
*/
|
|
95
|
+
function dockerPromptDefault(dbReachable, embReachable) {
|
|
96
|
+
return (dbReachable && embReachable) ? 'n' : 'y';
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Write (or replace) a single `KEY=VALUE` line in `~/.config/codesearch-mcp/.env`,
|
|
100
|
+
* chmod 600. API keys live here — never in config.json — referenced as `env:KEY`.
|
|
101
|
+
*/
|
|
102
|
+
function writeEnvKey(name, value) {
|
|
103
|
+
fs.mkdirSync(config_store_1.CONFIG_DIR, { recursive: true });
|
|
104
|
+
let existing = '';
|
|
105
|
+
try {
|
|
106
|
+
existing = fs.readFileSync(config_store_1.ENV_FILE, 'utf-8');
|
|
107
|
+
}
|
|
108
|
+
catch { /* absent */ }
|
|
109
|
+
const kept = existing.split(/\r?\n/).filter(l => l.trim() && !l.trim().startsWith(`${name}=`));
|
|
110
|
+
kept.push(`${name}=${value}`);
|
|
111
|
+
fs.writeFileSync(config_store_1.ENV_FILE, kept.join('\n') + '\n', { mode: 0o600 });
|
|
112
|
+
try {
|
|
113
|
+
fs.chmodSync(config_store_1.ENV_FILE, 0o600);
|
|
114
|
+
}
|
|
115
|
+
catch { /* best effort */ }
|
|
116
|
+
}
|
|
63
117
|
async function waitForDb(host, port, user, pass, db, maxWaitSec = 60, logger, steps) {
|
|
64
118
|
const { Client } = await Promise.resolve().then(() => __importStar(require('pg')));
|
|
65
119
|
const deadline = Date.now() + maxWaitSec * 1000;
|
|
@@ -82,33 +136,6 @@ async function waitForDb(host, port, user, pass, db, maxWaitSec = 60, logger, st
|
|
|
82
136
|
logger?.error('waitForDb: timeout', { reason, steps: JSON.stringify(steps) });
|
|
83
137
|
process.exit(1);
|
|
84
138
|
}
|
|
85
|
-
async function waitForOllama(host, maxWaitSec = 60, logger, steps) {
|
|
86
|
-
const deadline = Date.now() + maxWaitSec * 1000;
|
|
87
|
-
process.stdout.write('Waiting for Ollama to be ready');
|
|
88
|
-
while (Date.now() < deadline) {
|
|
89
|
-
try {
|
|
90
|
-
const http = await Promise.resolve().then(() => __importStar(require('http')));
|
|
91
|
-
await new Promise((resolve, reject) => {
|
|
92
|
-
const req = http.get(`${host}/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
|
-
console.log(' ready.');
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
catch {
|
|
103
|
-
process.stdout.write('.');
|
|
104
|
-
await new Promise(r => setTimeout(r, 2000));
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
const reason = 'Ollama did not become ready in time.';
|
|
108
|
-
console.error(`\n${reason}`);
|
|
109
|
-
logger?.error('waitForOllama: timeout', { reason, steps: JSON.stringify(steps) });
|
|
110
|
-
process.exit(1);
|
|
111
|
-
}
|
|
112
139
|
async function checkRemoteDb(host, port, user, pass, db, logger, steps) {
|
|
113
140
|
try {
|
|
114
141
|
const { Client } = await Promise.resolve().then(() => __importStar(require('pg')));
|
|
@@ -124,7 +151,16 @@ async function checkRemoteDb(host, port, user, pass, db, logger, steps) {
|
|
|
124
151
|
return false;
|
|
125
152
|
}
|
|
126
153
|
}
|
|
127
|
-
|
|
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;
|
|
128
164
|
console.log('\nWriting docker-compose.yml to', DOCKER_COMPOSE_DST);
|
|
129
165
|
const yamlQuote = (s) => `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
130
166
|
let composeContent = fs.readFileSync(DOCKER_COMPOSE_SRC, 'utf-8');
|
|
@@ -132,35 +168,106 @@ async function startDockerContainers(ollamaHost, dbPort, dbUser, dbPass, logger,
|
|
|
132
168
|
.replace('POSTGRES_USER: dev', `POSTGRES_USER: ${yamlQuote(dbUser)}`)
|
|
133
169
|
.replace('POSTGRES_PASSWORD: dev', `POSTGRES_PASSWORD: ${yamlQuote(dbPass)}`)
|
|
134
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
|
+
}
|
|
135
182
|
fs.mkdirSync(config_store_1.CONFIG_DIR, { recursive: true });
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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);
|
|
189
|
+
// The GPU override is copied verbatim so the managed Infinity path can layer it.
|
|
190
|
+
try {
|
|
191
|
+
fs.copyFileSync(DOCKER_GPU_OVERRIDE_SRC, DOCKER_GPU_OVERRIDE_DST);
|
|
192
|
+
}
|
|
193
|
+
catch { /* absent → CPU only */ }
|
|
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) });
|
|
143
201
|
process.exit(1);
|
|
144
202
|
}
|
|
145
203
|
await waitForDb('localhost', parseInt(dbPort), dbUser, dbPass, 'postgres', 60, logger, steps);
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
+
}
|
|
231
|
+
}
|
|
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
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
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')
|
|
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)}`);
|
|
155
264
|
}
|
|
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
265
|
}
|
|
159
266
|
async function runSetup(logger) {
|
|
160
267
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
161
268
|
const steps = [];
|
|
162
269
|
function logStep(q, raw) {
|
|
163
|
-
const a = /password/i.test(q) ? '***' : raw.trim();
|
|
270
|
+
const a = /password|key|secret|token/i.test(q) ? '***' : raw.trim();
|
|
164
271
|
logger?.debug('setup: prompt', { q, a });
|
|
165
272
|
steps.push({ q, a });
|
|
166
273
|
}
|
|
@@ -191,23 +298,68 @@ async function runSetup(logger) {
|
|
|
191
298
|
console.log(' Path is required.');
|
|
192
299
|
} while (!workspaceInput.trim());
|
|
193
300
|
const workspaceRoot = expandHome(workspaceInput.trim());
|
|
194
|
-
|
|
301
|
+
console.log('\n--- Embedding backend ---');
|
|
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');
|
|
304
|
+
console.log(' voyage — Voyage AI cloud');
|
|
305
|
+
let embProvider;
|
|
306
|
+
do {
|
|
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));
|
|
311
|
+
const pd = PROVIDER_DEFAULTS[embProvider];
|
|
312
|
+
const embBaseURL = await loggedPrompt('Base URL', pd.baseURL);
|
|
313
|
+
const embModel = await loggedPrompt('Embedding model', pd.model);
|
|
314
|
+
// Infinity serves a reranker from the same container — offer to provision one.
|
|
315
|
+
let rerankModel = '';
|
|
316
|
+
if (embProvider === 'infinity') {
|
|
317
|
+
rerankModel = (await loggedPrompt(`Reranker model (served by the same Infinity container; blank to disable)`, config_store_1.CONFIG_DEFAULTS['reranker.model'])).trim();
|
|
318
|
+
}
|
|
319
|
+
// API keys go into .env (chmod 600); config.json only stores the env: reference.
|
|
320
|
+
let apiKeyRef = '';
|
|
321
|
+
if (!isLocalBackend(embProvider)) {
|
|
322
|
+
const key = (await loggedAsk(`API key (stored in ${config_store_1.ENV_FILE}, leave blank to set later): `)).trim();
|
|
323
|
+
if (key) {
|
|
324
|
+
writeEnvKey(pd.envVar, key);
|
|
325
|
+
apiKeyRef = `env:${pd.envVar}`;
|
|
326
|
+
console.log(` Saved API key to ${config_store_1.ENV_FILE} as ${pd.envVar} (config references env:${pd.envVar}).`);
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
apiKeyRef = `env:${pd.envVar}`;
|
|
330
|
+
console.log(` No key entered — set it later: echo '${pd.envVar}=sk-…' >> ${config_store_1.ENV_FILE}`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
195
333
|
console.log('\n--- Database ---');
|
|
196
334
|
const dbHost = await loggedPrompt('DB host', 'localhost');
|
|
197
335
|
const dbPort = await loggedPrompt('DB port', '5433');
|
|
198
336
|
const dbUser = await loggedPrompt('DB user', 'dev');
|
|
199
337
|
const dbPass = await loggedPrompt('DB password', 'dev');
|
|
200
338
|
process.stdout.write('\nTesting connection...');
|
|
201
|
-
const
|
|
202
|
-
if (
|
|
339
|
+
const dbReachable = await checkRemoteDb(dbHost, dbPort, dbUser, dbPass, 'postgres', logger, steps);
|
|
340
|
+
if (dbReachable)
|
|
203
341
|
console.log(' Database is already reachable.');
|
|
204
|
-
|
|
342
|
+
// Symmetric to the DB probe: a reachable embedding backend defaults "start
|
|
343
|
+
// Docker?" to no. Cloud providers have no local backend to start.
|
|
344
|
+
process.stdout.write('Checking embedding backend...');
|
|
345
|
+
const embReachable = await checkEmbeddingBackend(embProvider, embBaseURL);
|
|
346
|
+
if (isLocalBackend(embProvider)) {
|
|
347
|
+
console.log(embReachable ? ` ${embProvider} is already reachable.` : ` ${embProvider} is not reachable — it can be started via Docker.`);
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
console.log(' cloud provider (no local backend to start).');
|
|
351
|
+
}
|
|
352
|
+
const dockerDefault = dockerPromptDefault(dbReachable, embReachable);
|
|
205
353
|
const startDockerRaw = await loggedAsk(`Start Docker containers automatically? (y/n) (${dockerDefault}): `);
|
|
206
354
|
const startDocker = (startDockerRaw.trim().toLowerCase() || dockerDefault) === 'y';
|
|
207
355
|
if (startDocker) {
|
|
208
|
-
await
|
|
356
|
+
await prepareComposeAndStartDb({
|
|
357
|
+
dbPort, dbUser, dbPass,
|
|
358
|
+
startDb: !dbReachable,
|
|
359
|
+
logger, steps,
|
|
360
|
+
});
|
|
209
361
|
}
|
|
210
|
-
else if (!
|
|
362
|
+
else if (!dbReachable) {
|
|
211
363
|
while (true) {
|
|
212
364
|
console.log('\nTesting connection...');
|
|
213
365
|
const ok = await checkRemoteDb(dbHost, dbPort, dbUser, dbPass, 'postgres', logger, steps);
|
|
@@ -242,15 +394,35 @@ async function runSetup(logger) {
|
|
|
242
394
|
user: dbVars.user,
|
|
243
395
|
password: dbVars.password,
|
|
244
396
|
},
|
|
245
|
-
|
|
397
|
+
embedding: {
|
|
398
|
+
provider: embProvider,
|
|
399
|
+
baseURL: embBaseURL,
|
|
400
|
+
model: embModel,
|
|
401
|
+
...(apiKeyRef ? { apiKey: apiKeyRef } : {}),
|
|
402
|
+
},
|
|
246
403
|
logging: { level: logLevel },
|
|
247
404
|
workspaceRoot,
|
|
248
405
|
};
|
|
406
|
+
// Infinity serves the reranker from the same container — auto-wire reranker.*
|
|
407
|
+
// to that server. `url` derives from provider + baseURL in the Config constructor.
|
|
408
|
+
if (embProvider === 'infinity' && rerankModel) {
|
|
409
|
+
cfg.reranker = {
|
|
410
|
+
enabled: true,
|
|
411
|
+
provider: 'openai-compatible',
|
|
412
|
+
baseURL: embBaseURL,
|
|
413
|
+
model: rerankModel,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
249
416
|
if (logDir)
|
|
250
417
|
cfg.logging.dir = logDir;
|
|
251
418
|
(0, config_store_1.writeConfigJson)(cfg);
|
|
252
419
|
console.log(`\nConfig saved to ${config_store_1.CONFIG_FILE}`);
|
|
253
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
|
+
}
|
|
254
426
|
console.log('\nInitializing database schema...');
|
|
255
427
|
const { DatabaseService } = await Promise.resolve().then(() => __importStar(require('./services/DatabaseService')));
|
|
256
428
|
const dbService = new DatabaseService(new config_1.Config());
|
|
@@ -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
|
|
@@ -12,13 +12,28 @@ services:
|
|
|
12
12
|
volumes:
|
|
13
13
|
- pgdata:/var/lib/postgresql/data
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
# One Infinity container serves both /embeddings and /rerank. The model set is
|
|
16
|
+
# bound at startup via INFINITY_MODEL_ID (";"-separated for multiple models,
|
|
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).
|
|
21
|
+
# Model warmup defaults OFF here: on CPU it runs multi-minute benchmark
|
|
22
|
+
# batches at every boot (a pure perf measurement, not required to serve). The
|
|
23
|
+
# GPU override turns it back on, where it costs seconds.
|
|
24
|
+
infinity:
|
|
25
|
+
image: michaelf34/infinity:latest
|
|
26
|
+
restart: unless-stopped
|
|
17
27
|
ports:
|
|
18
|
-
- "
|
|
28
|
+
- "7997:7997"
|
|
29
|
+
environment:
|
|
30
|
+
INFINITY_MODEL_ID: "jinaai/jina-embeddings-v2-base-code;mixedbread-ai/mxbai-rerank-xsmall-v1"
|
|
31
|
+
INFINITY_MODEL_WARMUP: ${INFINITY_MODEL_WARMUP:-false}
|
|
32
|
+
INFINITY_PORT: "7997"
|
|
33
|
+
command: ["v2"]
|
|
19
34
|
volumes:
|
|
20
|
-
-
|
|
35
|
+
- infinity_cache:/app/.cache
|
|
21
36
|
|
|
22
37
|
volumes:
|
|
23
38
|
pgdata:
|
|
24
|
-
|
|
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.
|