viberag 0.2.0 → 0.3.0
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/README.md +1 -1
- package/dist/cli/commands/mcp-setup.d.ts +1 -1
- package/dist/cli/commands/mcp-setup.js +23 -3
- package/dist/cli/components/CleanWizard.js +16 -1
- package/dist/cli/components/InitWizard.js +37 -15
- package/dist/common/types.d.ts +2 -2
- package/dist/mcp/index.js +5 -1
- package/dist/mcp/warmup.d.ts +5 -0
- package/dist/mcp/warmup.js +7 -0
- package/dist/rag/config/index.d.ts +4 -0
- package/dist/rag/config/index.js +37 -13
- package/dist/rag/embeddings/gemini.js +34 -7
- package/dist/rag/embeddings/index.d.ts +1 -0
- package/dist/rag/embeddings/index.js +1 -0
- package/dist/rag/embeddings/mistral.d.ts +2 -2
- package/dist/rag/embeddings/mistral.js +18 -5
- package/dist/rag/embeddings/openai.js +22 -3
- package/dist/rag/embeddings/validate.d.ts +22 -0
- package/dist/rag/embeddings/validate.js +148 -0
- package/dist/rag/index.d.ts +1 -1
- package/dist/rag/index.js +1 -1
- package/dist/rag/indexer/chunker.js +31 -19
- package/dist/rag/indexer/indexer.d.ts +10 -0
- package/dist/rag/indexer/indexer.js +88 -53
- package/dist/rag/search/index.d.ts +6 -0
- package/dist/rag/search/index.js +35 -9
- package/dist/rag/storage/index.d.ts +15 -1
- package/dist/rag/storage/index.js +108 -21
- package/package.json +33 -4
- package/dist/cli/__tests__/mcp-setup-comprehensive.test.d.ts +0 -10
- package/dist/cli/__tests__/mcp-setup-comprehensive.test.js +0 -515
- package/dist/cli/__tests__/mcp-setup-global.test.d.ts +0 -7
- package/dist/cli/__tests__/mcp-setup-global.test.js +0 -577
- package/dist/cli/__tests__/mcp-setup.test.d.ts +0 -6
- package/dist/cli/__tests__/mcp-setup.test.js +0 -704
- package/dist/rag/__tests__/grammar-smoke.test.d.ts +0 -9
- package/dist/rag/__tests__/grammar-smoke.test.js +0 -161
- package/dist/rag/__tests__/helpers.d.ts +0 -30
- package/dist/rag/__tests__/helpers.js +0 -67
- package/dist/rag/__tests__/merkle.test.d.ts +0 -5
- package/dist/rag/__tests__/merkle.test.js +0 -161
- package/dist/rag/__tests__/metadata-extraction.test.d.ts +0 -10
- package/dist/rag/__tests__/metadata-extraction.test.js +0 -202
- package/dist/rag/__tests__/multi-language.test.d.ts +0 -13
- package/dist/rag/__tests__/multi-language.test.js +0 -535
- package/dist/rag/__tests__/rag.test.d.ts +0 -10
- package/dist/rag/__tests__/rag.test.js +0 -311
- package/dist/rag/__tests__/search-exhaustive.test.d.ts +0 -9
- package/dist/rag/__tests__/search-exhaustive.test.js +0 -87
- package/dist/rag/__tests__/search-filters.test.d.ts +0 -10
- package/dist/rag/__tests__/search-filters.test.js +0 -250
- package/dist/rag/__tests__/search-modes.test.d.ts +0 -8
- package/dist/rag/__tests__/search-modes.test.js +0 -133
package/README.md
CHANGED
|
@@ -580,7 +580,7 @@ Choose your embedding provider during `/init`:
|
|
|
580
580
|
| Provider | Model | Dims | Cost | Get API Key |
|
|
581
581
|
| -------- | ---------------------- | ---- | --------- | ------------------------------------------------------- |
|
|
582
582
|
| Gemini | gemini-embedding-001 | 1536 | Free tier | [Google AI Studio](https://aistudio.google.com) |
|
|
583
|
-
| Mistral | codestral-embed |
|
|
583
|
+
| Mistral | codestral-embed | 1536 | $0.10/1M | [Mistral Console](https://console.mistral.ai/api-keys/) |
|
|
584
584
|
| OpenAI | text-embedding-3-small | 1536 | $0.02/1M | [OpenAI Platform](https://platform.openai.com/api-keys) |
|
|
585
585
|
|
|
586
586
|
- **Gemini** - Free tier available, great for getting started
|
|
@@ -47,7 +47,7 @@ export declare function generateMcpConfig(editor: EditorConfig): object;
|
|
|
47
47
|
export declare function generateTomlConfig(): string;
|
|
48
48
|
/**
|
|
49
49
|
* Read existing TOML config file.
|
|
50
|
-
* Returns the raw content string.
|
|
50
|
+
* Returns the raw content string, or null if file doesn't exist or can't be read.
|
|
51
51
|
*/
|
|
52
52
|
export declare function readTomlConfig(configPath: string): Promise<string | null>;
|
|
53
53
|
/**
|
|
@@ -150,13 +150,20 @@ args = ["-y", "viberag-mcp"]
|
|
|
150
150
|
}
|
|
151
151
|
/**
|
|
152
152
|
* Read existing TOML config file.
|
|
153
|
-
* Returns the raw content string.
|
|
153
|
+
* Returns the raw content string, or null if file doesn't exist or can't be read.
|
|
154
154
|
*/
|
|
155
155
|
export async function readTomlConfig(configPath) {
|
|
156
156
|
try {
|
|
157
157
|
return await fs.readFile(configPath, 'utf-8');
|
|
158
158
|
}
|
|
159
|
-
catch {
|
|
159
|
+
catch (error) {
|
|
160
|
+
// Log non-ENOENT errors to help with debugging
|
|
161
|
+
if (error instanceof Error && 'code' in error) {
|
|
162
|
+
const code = error.code;
|
|
163
|
+
if (code !== 'ENOENT') {
|
|
164
|
+
console.warn(`[mcp-setup] Failed to read ${configPath}: ${code} - ${error.message}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
160
167
|
return null;
|
|
161
168
|
}
|
|
162
169
|
}
|
|
@@ -242,7 +249,20 @@ export async function readJsonConfig(configPath) {
|
|
|
242
249
|
const stripped = stripJsonComments(content);
|
|
243
250
|
return JSON.parse(stripped);
|
|
244
251
|
}
|
|
245
|
-
catch {
|
|
252
|
+
catch (error) {
|
|
253
|
+
// Log non-ENOENT errors to help with debugging
|
|
254
|
+
if (error instanceof Error) {
|
|
255
|
+
if ('code' in error) {
|
|
256
|
+
const code = error.code;
|
|
257
|
+
if (code !== 'ENOENT') {
|
|
258
|
+
console.warn(`[mcp-setup] Failed to read ${configPath}: ${code} - ${error.message}`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
else if (error instanceof SyntaxError) {
|
|
262
|
+
// JSON parse error
|
|
263
|
+
console.warn(`[mcp-setup] Failed to parse ${configPath}: ${error.message}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
246
266
|
return null;
|
|
247
267
|
}
|
|
248
268
|
}
|
|
@@ -73,7 +73,22 @@ export function CleanWizard({ projectRoot, viberagDir, onComplete, onCancel, add
|
|
|
73
73
|
setViberagRemoved(true);
|
|
74
74
|
}
|
|
75
75
|
catch (error) {
|
|
76
|
-
|
|
76
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
77
|
+
// Check if directory just doesn't exist (not critical)
|
|
78
|
+
const isNotFound = error instanceof Error &&
|
|
79
|
+
'code' in error &&
|
|
80
|
+
error.code === 'ENOENT';
|
|
81
|
+
if (isNotFound) {
|
|
82
|
+
// Directory doesn't exist - that's fine, consider it removed
|
|
83
|
+
setViberagRemoved(true);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
// Critical failure (permission denied, etc.) - stop cleanup
|
|
87
|
+
addOutput('system', `Failed to remove ${viberagDir}: ${message}`);
|
|
88
|
+
addOutput('system', 'Stopping cleanup due to critical failure.');
|
|
89
|
+
setStep('summary');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
77
92
|
}
|
|
78
93
|
const results = [];
|
|
79
94
|
// Clean project MCP configs if requested
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Multi-step initialization wizard component.
|
|
3
3
|
* Guides users through embedding provider selection and API key configuration.
|
|
4
4
|
*/
|
|
5
|
-
import React, { useState } from 'react';
|
|
5
|
+
import React, { useState, useEffect } from 'react';
|
|
6
6
|
import { Box, Text, useInput } from 'ink';
|
|
7
7
|
import SelectInput from 'ink-select-input';
|
|
8
8
|
/**
|
|
@@ -48,7 +48,7 @@ const PROVIDER_CONFIG = {
|
|
|
48
48
|
name: 'Gemini',
|
|
49
49
|
model: 'gemini-embedding-001',
|
|
50
50
|
modelFull: 'gemini-embedding-001',
|
|
51
|
-
dims: '
|
|
51
|
+
dims: '1536',
|
|
52
52
|
context: '2K',
|
|
53
53
|
cost: 'Free tier',
|
|
54
54
|
note: 'API key required',
|
|
@@ -58,7 +58,7 @@ const PROVIDER_CONFIG = {
|
|
|
58
58
|
name: 'Mistral',
|
|
59
59
|
model: 'codestral-embed',
|
|
60
60
|
modelFull: 'codestral-embed',
|
|
61
|
-
dims: '
|
|
61
|
+
dims: '1536',
|
|
62
62
|
context: '8K',
|
|
63
63
|
cost: '$0.10/1M',
|
|
64
64
|
note: 'API key required',
|
|
@@ -98,13 +98,13 @@ const FRONTIER_MODELS_DATA = [
|
|
|
98
98
|
{
|
|
99
99
|
Provider: 'Gemini',
|
|
100
100
|
Model: 'gemini-embedding-001',
|
|
101
|
-
Dims: '
|
|
101
|
+
Dims: '1536',
|
|
102
102
|
Cost: 'Free tier',
|
|
103
103
|
},
|
|
104
104
|
{
|
|
105
105
|
Provider: 'Mistral',
|
|
106
106
|
Model: 'codestral-embed',
|
|
107
|
-
Dims: '
|
|
107
|
+
Dims: '1536',
|
|
108
108
|
Cost: '$0.10/1M',
|
|
109
109
|
},
|
|
110
110
|
{
|
|
@@ -178,21 +178,35 @@ const API_KEY_ACTION_ITEMS = [
|
|
|
178
178
|
];
|
|
179
179
|
/**
|
|
180
180
|
* Simple text input component for API key entry.
|
|
181
|
+
* Uses a ref to accumulate input, which handles paste better than
|
|
182
|
+
* relying on React state updates between rapid useInput calls.
|
|
181
183
|
*/
|
|
182
184
|
function ApiKeyInputStep({ providerName, apiKeyInput, setApiKeyInput, onSubmit, }) {
|
|
185
|
+
// Use a ref to accumulate input - avoids closure stale state issues during rapid paste
|
|
186
|
+
const inputRef = React.useRef(apiKeyInput);
|
|
187
|
+
inputRef.current = apiKeyInput;
|
|
183
188
|
// Handle text input (supports paste)
|
|
184
189
|
useInput((input, key) => {
|
|
185
190
|
if (key.return) {
|
|
186
|
-
onSubmit(
|
|
191
|
+
onSubmit(inputRef.current);
|
|
187
192
|
}
|
|
188
193
|
else if (key.backspace || key.delete) {
|
|
189
|
-
setApiKeyInput(
|
|
194
|
+
setApiKeyInput(inputRef.current.slice(0, -1));
|
|
190
195
|
}
|
|
191
196
|
else if (!key.ctrl && !key.meta && input) {
|
|
192
197
|
// Add printable characters (supports multi-char paste)
|
|
193
|
-
|
|
198
|
+
// Filter out control characters that might slip through
|
|
199
|
+
// eslint-disable-next-line no-control-regex
|
|
200
|
+
const printable = input.replace(/[\x00-\x1F\x7F]/g, '');
|
|
201
|
+
if (printable) {
|
|
202
|
+
setApiKeyInput(inputRef.current + printable);
|
|
203
|
+
}
|
|
194
204
|
}
|
|
195
205
|
});
|
|
206
|
+
// Mask API key display (show first 7 and last 4 chars)
|
|
207
|
+
const maskedKey = apiKeyInput.length > 15
|
|
208
|
+
? `${apiKeyInput.slice(0, 7)}${'•'.repeat(Math.min(apiKeyInput.length - 11, 20))}${apiKeyInput.slice(-4)}`
|
|
209
|
+
: apiKeyInput;
|
|
196
210
|
return (React.createElement(Box, { marginTop: 1, flexDirection: "column" },
|
|
197
211
|
React.createElement(Text, null,
|
|
198
212
|
"Enter your ",
|
|
@@ -200,8 +214,12 @@ function ApiKeyInputStep({ providerName, apiKeyInput, setApiKeyInput, onSubmit,
|
|
|
200
214
|
" API key:"),
|
|
201
215
|
React.createElement(Box, { marginTop: 1 },
|
|
202
216
|
React.createElement(Text, { color: "blue" }, "> "),
|
|
203
|
-
React.createElement(Text, null,
|
|
217
|
+
React.createElement(Text, null, maskedKey),
|
|
204
218
|
React.createElement(Text, { color: "gray" }, "\u2588")),
|
|
219
|
+
apiKeyInput.length > 0 && (React.createElement(Text, { dimColor: true },
|
|
220
|
+
"Length: ",
|
|
221
|
+
apiKeyInput.length,
|
|
222
|
+
" characters")),
|
|
205
223
|
apiKeyInput.trim() === '' && (React.createElement(Text, { color: "yellow", dimColor: true }, "API key is required")),
|
|
206
224
|
React.createElement(Text, { dimColor: true }, "Press Enter to continue")));
|
|
207
225
|
}
|
|
@@ -221,13 +239,19 @@ export function InitWizard({ step, config, isReinit, existingApiKey, existingPro
|
|
|
221
239
|
// Check if current provider is a cloud provider
|
|
222
240
|
const currentProvider = config.provider ?? 'local';
|
|
223
241
|
const needsApiKey = isCloudProvider(currentProvider);
|
|
224
|
-
// Check if we have an existing API key for the same provider
|
|
225
|
-
const hasExistingKeyForProvider = existingApiKey && existingProvider === currentProvider;
|
|
226
242
|
// Compute effective step (adjusted for non-reinit flow)
|
|
227
243
|
// Steps: 0=reinit confirm, 1=provider select, 2=api key (cloud only), 3=final confirm
|
|
228
244
|
const effectiveStep = normalizedIsReinit
|
|
229
245
|
? normalizedStep
|
|
230
246
|
: normalizedStep + 1;
|
|
247
|
+
// Auto-advance past API key step for local providers (must be in useEffect, not render)
|
|
248
|
+
useEffect(() => {
|
|
249
|
+
if (effectiveStep === 2 && !needsApiKey) {
|
|
250
|
+
onStepChange(normalizedStep + 1);
|
|
251
|
+
}
|
|
252
|
+
}, [effectiveStep, needsApiKey, normalizedStep, onStepChange]);
|
|
253
|
+
// Check if we have an existing API key for the same provider
|
|
254
|
+
const hasExistingKeyForProvider = existingApiKey && existingProvider === currentProvider;
|
|
231
255
|
// Step 0 (reinit only): Confirmation
|
|
232
256
|
if (normalizedIsReinit && normalizedStep === 0) {
|
|
233
257
|
return (React.createElement(Box, { flexDirection: "column", borderStyle: "round", paddingX: 2, paddingY: 1 },
|
|
@@ -264,12 +288,10 @@ export function InitWizard({ step, config, isReinit, existingApiKey, existingPro
|
|
|
264
288
|
React.createElement(Text, { dimColor: true }, "\u2191/\u2193 navigate, Enter select, Esc cancel"))));
|
|
265
289
|
}
|
|
266
290
|
// Step 2: API Key input (cloud providers only)
|
|
267
|
-
// For local providers, skip to step 3 (confirmation)
|
|
291
|
+
// For local providers, skip to step 3 (confirmation) - handled by useEffect above
|
|
268
292
|
if (effectiveStep === 2) {
|
|
269
|
-
//
|
|
293
|
+
// Show loading while useEffect auto-advances for local providers
|
|
270
294
|
if (!needsApiKey) {
|
|
271
|
-
// Auto-advance to confirmation
|
|
272
|
-
onStepChange(normalizedStep + 1);
|
|
273
295
|
return (React.createElement(Box, null,
|
|
274
296
|
React.createElement(Text, { dimColor: true }, "Loading...")));
|
|
275
297
|
}
|
package/dist/common/types.d.ts
CHANGED
|
@@ -82,8 +82,8 @@ export type IndexDisplayStats = {
|
|
|
82
82
|
* - local-4b: Qwen3-Embedding-4B FP32 (2560d) - ~8GB download, ~8GB RAM
|
|
83
83
|
*
|
|
84
84
|
* API providers:
|
|
85
|
-
* - gemini: gemini-embedding-001 (
|
|
86
|
-
* - mistral: codestral-embed (
|
|
85
|
+
* - gemini: gemini-embedding-001 (1536d) - Free tier
|
|
86
|
+
* - mistral: codestral-embed (1536d) - Code-optimized
|
|
87
87
|
* - openai: text-embedding-3-small (1536d) - Fast API
|
|
88
88
|
*/
|
|
89
89
|
export type EmbeddingProviderType = 'local' | 'local-4b' | 'gemini' | 'mistral' | 'openai';
|
package/dist/mcp/index.js
CHANGED
|
@@ -13,7 +13,7 @@ import { createMcpServer } from './server.js';
|
|
|
13
13
|
import { configExists, Indexer } from '../rag/index.js';
|
|
14
14
|
// Use current working directory as project root (same behavior as CLI)
|
|
15
15
|
const projectRoot = process.cwd();
|
|
16
|
-
const { server, startWatcher, stopWatcher, startWarmup } = createMcpServer(projectRoot);
|
|
16
|
+
const { server, startWatcher, stopWatcher, startWarmup, warmupManager } = createMcpServer(projectRoot);
|
|
17
17
|
// Handle shutdown signals
|
|
18
18
|
async function shutdown(signal) {
|
|
19
19
|
console.error(`[viberag-mcp] Received ${signal}, shutting down...`);
|
|
@@ -35,6 +35,10 @@ setImmediate(async () => {
|
|
|
35
35
|
if (await configExists(projectRoot)) {
|
|
36
36
|
startWarmup();
|
|
37
37
|
console.error('[viberag-mcp] Warmup started');
|
|
38
|
+
// Monitor warmup completion for logging (non-blocking)
|
|
39
|
+
warmupManager.getWarmupPromise()?.catch(error => {
|
|
40
|
+
console.error('[viberag-mcp] Warmup failed:', error instanceof Error ? error.message : error);
|
|
41
|
+
});
|
|
38
42
|
}
|
|
39
43
|
}
|
|
40
44
|
catch (error) {
|
package/dist/mcp/warmup.d.ts
CHANGED
|
@@ -59,6 +59,11 @@ export declare class WarmupManager {
|
|
|
59
59
|
* Check if warmup is in progress.
|
|
60
60
|
*/
|
|
61
61
|
isInitializing(): boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Get the warmup promise for external error monitoring.
|
|
64
|
+
* Returns null if warmup hasn't started.
|
|
65
|
+
*/
|
|
66
|
+
getWarmupPromise(): Promise<SearchEngine> | null;
|
|
62
67
|
/**
|
|
63
68
|
* Start warmup if not already started.
|
|
64
69
|
* Returns immediately - doesn't wait for completion.
|
package/dist/mcp/warmup.js
CHANGED
|
@@ -65,6 +65,13 @@ export class WarmupManager {
|
|
|
65
65
|
isInitializing() {
|
|
66
66
|
return this.state.status === 'initializing';
|
|
67
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Get the warmup promise for external error monitoring.
|
|
70
|
+
* Returns null if warmup hasn't started.
|
|
71
|
+
*/
|
|
72
|
+
getWarmupPromise() {
|
|
73
|
+
return this.warmupPromise;
|
|
74
|
+
}
|
|
68
75
|
/**
|
|
69
76
|
* Start warmup if not already started.
|
|
70
77
|
* Returns immediately - doesn't wait for completion.
|
|
@@ -48,6 +48,10 @@ export declare const DEFAULT_CONFIG: ViberagConfig;
|
|
|
48
48
|
* Load config from disk, merging with defaults.
|
|
49
49
|
* Returns DEFAULT_CONFIG if no config file exists.
|
|
50
50
|
* Handles nested watch config merge for backward compatibility.
|
|
51
|
+
*
|
|
52
|
+
* IMPORTANT: If the config file exists but can't be read/parsed,
|
|
53
|
+
* this throws an error instead of silently falling back to defaults.
|
|
54
|
+
* This prevents dimension mismatches when switching providers.
|
|
51
55
|
*/
|
|
52
56
|
export declare function loadConfig(projectRoot: string): Promise<ViberagConfig>;
|
|
53
57
|
/**
|
package/dist/rag/config/index.js
CHANGED
|
@@ -18,7 +18,7 @@ export const PROVIDER_CONFIGS = {
|
|
|
18
18
|
},
|
|
19
19
|
mistral: {
|
|
20
20
|
model: 'codestral-embed',
|
|
21
|
-
dimensions:
|
|
21
|
+
dimensions: 1536,
|
|
22
22
|
},
|
|
23
23
|
openai: {
|
|
24
24
|
model: 'text-embedding-3-small',
|
|
@@ -65,26 +65,50 @@ export const DEFAULT_CONFIG = {
|
|
|
65
65
|
* Load config from disk, merging with defaults.
|
|
66
66
|
* Returns DEFAULT_CONFIG if no config file exists.
|
|
67
67
|
* Handles nested watch config merge for backward compatibility.
|
|
68
|
+
*
|
|
69
|
+
* IMPORTANT: If the config file exists but can't be read/parsed,
|
|
70
|
+
* this throws an error instead of silently falling back to defaults.
|
|
71
|
+
* This prevents dimension mismatches when switching providers.
|
|
68
72
|
*/
|
|
69
73
|
export async function loadConfig(projectRoot) {
|
|
70
74
|
const configPath = getConfigPath(projectRoot);
|
|
75
|
+
// First check if the file exists
|
|
71
76
|
try {
|
|
72
|
-
|
|
73
|
-
const loaded = JSON.parse(content);
|
|
74
|
-
// Deep merge watch config with defaults
|
|
75
|
-
const watchConfig = {
|
|
76
|
-
...DEFAULT_WATCH_CONFIG,
|
|
77
|
-
...(loaded.watch ?? {}),
|
|
78
|
-
};
|
|
79
|
-
return {
|
|
80
|
-
...DEFAULT_CONFIG,
|
|
81
|
-
...loaded,
|
|
82
|
-
watch: watchConfig,
|
|
83
|
-
};
|
|
77
|
+
await fs.access(configPath);
|
|
84
78
|
}
|
|
85
79
|
catch {
|
|
80
|
+
// Config doesn't exist - return defaults (expected for first run)
|
|
86
81
|
return { ...DEFAULT_CONFIG };
|
|
87
82
|
}
|
|
83
|
+
// File exists - must be readable and valid
|
|
84
|
+
// Don't silently fall back to defaults as this could cause dimension mismatches
|
|
85
|
+
const content = await fs.readFile(configPath, 'utf-8');
|
|
86
|
+
let loaded;
|
|
87
|
+
try {
|
|
88
|
+
loaded = JSON.parse(content);
|
|
89
|
+
}
|
|
90
|
+
catch (parseError) {
|
|
91
|
+
throw new Error(`Invalid config.json at ${configPath}: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
|
|
92
|
+
}
|
|
93
|
+
// Validate embedding dimensions match provider
|
|
94
|
+
const provider = loaded.embeddingProvider ?? DEFAULT_CONFIG.embeddingProvider;
|
|
95
|
+
const expectedDimensions = PROVIDER_CONFIGS[provider]?.dimensions;
|
|
96
|
+
if (expectedDimensions && loaded.embeddingDimensions !== expectedDimensions) {
|
|
97
|
+
// Dimensions mismatch - this can happen after provider change
|
|
98
|
+
// Auto-correct to prevent search failures
|
|
99
|
+
loaded.embeddingDimensions = expectedDimensions;
|
|
100
|
+
loaded.embeddingModel = PROVIDER_CONFIGS[provider].model;
|
|
101
|
+
}
|
|
102
|
+
// Deep merge watch config with defaults
|
|
103
|
+
const watchConfig = {
|
|
104
|
+
...DEFAULT_WATCH_CONFIG,
|
|
105
|
+
...(loaded.watch ?? {}),
|
|
106
|
+
};
|
|
107
|
+
return {
|
|
108
|
+
...DEFAULT_CONFIG,
|
|
109
|
+
...loaded,
|
|
110
|
+
watch: watchConfig,
|
|
111
|
+
};
|
|
88
112
|
}
|
|
89
113
|
/**
|
|
90
114
|
* Save config to disk.
|
|
@@ -35,7 +35,8 @@ export class GeminiEmbeddingProvider {
|
|
|
35
35
|
writable: true,
|
|
36
36
|
value: false
|
|
37
37
|
});
|
|
38
|
-
|
|
38
|
+
// Trim the key to remove any accidental whitespace
|
|
39
|
+
this.apiKey = (apiKey ?? '').trim();
|
|
39
40
|
}
|
|
40
41
|
async initialize(_onProgress) {
|
|
41
42
|
if (!this.apiKey) {
|
|
@@ -60,11 +61,12 @@ export class GeminiEmbeddingProvider {
|
|
|
60
61
|
return results;
|
|
61
62
|
}
|
|
62
63
|
async embedBatch(texts) {
|
|
63
|
-
const url = `${GEMINI_API_BASE}/${MODEL}:batchEmbedContents
|
|
64
|
+
const url = `${GEMINI_API_BASE}/${MODEL}:batchEmbedContents`;
|
|
64
65
|
const response = await fetch(url, {
|
|
65
66
|
method: 'POST',
|
|
66
67
|
headers: {
|
|
67
68
|
'Content-Type': 'application/json',
|
|
69
|
+
'x-goog-api-key': this.apiKey,
|
|
68
70
|
},
|
|
69
71
|
body: JSON.stringify({
|
|
70
72
|
requests: texts.map(text => ({
|
|
@@ -78,8 +80,20 @@ export class GeminiEmbeddingProvider {
|
|
|
78
80
|
}),
|
|
79
81
|
});
|
|
80
82
|
if (!response.ok) {
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
+
const errorText = await response.text();
|
|
84
|
+
let errorMessage;
|
|
85
|
+
try {
|
|
86
|
+
const errorJson = JSON.parse(errorText);
|
|
87
|
+
errorMessage = errorJson.error?.message || errorText;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
errorMessage = errorText;
|
|
91
|
+
}
|
|
92
|
+
if (response.status === 400 || response.status === 403) {
|
|
93
|
+
throw new Error(`Gemini API authentication failed (${response.status}). ` +
|
|
94
|
+
`Verify your API key at https://aistudio.google.com/apikey. Error: ${errorMessage}`);
|
|
95
|
+
}
|
|
96
|
+
throw new Error(`Gemini API error (${response.status}): ${errorMessage}`);
|
|
83
97
|
}
|
|
84
98
|
const data = (await response.json());
|
|
85
99
|
return data.embeddings.map(e => e.values);
|
|
@@ -88,11 +102,12 @@ export class GeminiEmbeddingProvider {
|
|
|
88
102
|
if (!this.initialized) {
|
|
89
103
|
await this.initialize();
|
|
90
104
|
}
|
|
91
|
-
const url = `${GEMINI_API_BASE}/${MODEL}:embedContent
|
|
105
|
+
const url = `${GEMINI_API_BASE}/${MODEL}:embedContent`;
|
|
92
106
|
const response = await fetch(url, {
|
|
93
107
|
method: 'POST',
|
|
94
108
|
headers: {
|
|
95
109
|
'Content-Type': 'application/json',
|
|
110
|
+
'x-goog-api-key': this.apiKey,
|
|
96
111
|
},
|
|
97
112
|
body: JSON.stringify({
|
|
98
113
|
model: `models/${MODEL}`,
|
|
@@ -104,8 +119,20 @@ export class GeminiEmbeddingProvider {
|
|
|
104
119
|
}),
|
|
105
120
|
});
|
|
106
121
|
if (!response.ok) {
|
|
107
|
-
const
|
|
108
|
-
|
|
122
|
+
const errorText = await response.text();
|
|
123
|
+
let errorMessage;
|
|
124
|
+
try {
|
|
125
|
+
const errorJson = JSON.parse(errorText);
|
|
126
|
+
errorMessage = errorJson.error?.message || errorText;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
errorMessage = errorText;
|
|
130
|
+
}
|
|
131
|
+
if (response.status === 400 || response.status === 403) {
|
|
132
|
+
throw new Error(`Gemini API authentication failed (${response.status}). ` +
|
|
133
|
+
`Verify your API key at https://aistudio.google.com/apikey. Error: ${errorMessage}`);
|
|
134
|
+
}
|
|
135
|
+
throw new Error(`Gemini API error (${response.status}): ${errorMessage}`);
|
|
109
136
|
}
|
|
110
137
|
const data = (await response.json());
|
|
111
138
|
return data.embedding.values;
|
|
@@ -7,4 +7,5 @@ export { Local4BEmbeddingProvider } from './local-4b.js';
|
|
|
7
7
|
export { LocalEmbeddingProvider } from './local.js';
|
|
8
8
|
export { MistralEmbeddingProvider } from './mistral.js';
|
|
9
9
|
export { OpenAIEmbeddingProvider } from './openai.js';
|
|
10
|
+
export { validateApiKey, type ValidationResult } from './validate.js';
|
|
10
11
|
export type { EmbeddingProvider, ModelProgressCallback } from './types.js';
|
|
@@ -7,3 +7,4 @@ export { Local4BEmbeddingProvider } from './local-4b.js';
|
|
|
7
7
|
export { LocalEmbeddingProvider } from './local.js';
|
|
8
8
|
export { MistralEmbeddingProvider } from './mistral.js';
|
|
9
9
|
export { OpenAIEmbeddingProvider } from './openai.js';
|
|
10
|
+
export { validateApiKey } from './validate.js';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Mistral embedding provider using Mistral AI API.
|
|
3
3
|
*
|
|
4
|
-
* Uses codestral-embed model (
|
|
4
|
+
* Uses codestral-embed model (1536 dimensions).
|
|
5
5
|
* Optimized for code and technical content.
|
|
6
6
|
*/
|
|
7
7
|
import type { EmbeddingProvider, ModelProgressCallback } from './types.js';
|
|
@@ -10,7 +10,7 @@ import type { EmbeddingProvider, ModelProgressCallback } from './types.js';
|
|
|
10
10
|
* Uses codestral-embed model via Mistral AI API.
|
|
11
11
|
*/
|
|
12
12
|
export declare class MistralEmbeddingProvider implements EmbeddingProvider {
|
|
13
|
-
readonly dimensions =
|
|
13
|
+
readonly dimensions = 1536;
|
|
14
14
|
private apiKey;
|
|
15
15
|
private initialized;
|
|
16
16
|
constructor(apiKey?: string);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Mistral embedding provider using Mistral AI API.
|
|
3
3
|
*
|
|
4
|
-
* Uses codestral-embed model (
|
|
4
|
+
* Uses codestral-embed model (1536 dimensions).
|
|
5
5
|
* Optimized for code and technical content.
|
|
6
6
|
*/
|
|
7
7
|
const MISTRAL_API_BASE = 'https://api.mistral.ai/v1';
|
|
@@ -17,7 +17,7 @@ export class MistralEmbeddingProvider {
|
|
|
17
17
|
enumerable: true,
|
|
18
18
|
configurable: true,
|
|
19
19
|
writable: true,
|
|
20
|
-
value:
|
|
20
|
+
value: 1536
|
|
21
21
|
});
|
|
22
22
|
Object.defineProperty(this, "apiKey", {
|
|
23
23
|
enumerable: true,
|
|
@@ -31,7 +31,8 @@ export class MistralEmbeddingProvider {
|
|
|
31
31
|
writable: true,
|
|
32
32
|
value: false
|
|
33
33
|
});
|
|
34
|
-
|
|
34
|
+
// Trim the key to remove any accidental whitespace
|
|
35
|
+
this.apiKey = (apiKey ?? '').trim();
|
|
35
36
|
}
|
|
36
37
|
async initialize(_onProgress) {
|
|
37
38
|
if (!this.apiKey) {
|
|
@@ -68,8 +69,20 @@ export class MistralEmbeddingProvider {
|
|
|
68
69
|
}),
|
|
69
70
|
});
|
|
70
71
|
if (!response.ok) {
|
|
71
|
-
const
|
|
72
|
-
|
|
72
|
+
const errorText = await response.text();
|
|
73
|
+
let errorMessage;
|
|
74
|
+
try {
|
|
75
|
+
const errorJson = JSON.parse(errorText);
|
|
76
|
+
errorMessage = errorJson.message || errorJson.detail || errorText;
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
errorMessage = errorText;
|
|
80
|
+
}
|
|
81
|
+
if (response.status === 401) {
|
|
82
|
+
throw new Error(`Mistral API authentication failed (401). ` +
|
|
83
|
+
`Verify your API key at https://console.mistral.ai/api-keys. Error: ${errorMessage}`);
|
|
84
|
+
}
|
|
85
|
+
throw new Error(`Mistral API error (${response.status}): ${errorMessage}`);
|
|
73
86
|
}
|
|
74
87
|
const data = (await response.json());
|
|
75
88
|
// Sort by index to ensure correct order
|
|
@@ -31,12 +31,17 @@ export class OpenAIEmbeddingProvider {
|
|
|
31
31
|
writable: true,
|
|
32
32
|
value: false
|
|
33
33
|
});
|
|
34
|
-
|
|
34
|
+
// Trim the key to remove any accidental whitespace
|
|
35
|
+
this.apiKey = (apiKey ?? '').trim();
|
|
35
36
|
}
|
|
36
37
|
async initialize(_onProgress) {
|
|
37
38
|
if (!this.apiKey) {
|
|
38
39
|
throw new Error('OpenAI API key required. Run /init to configure your API key.');
|
|
39
40
|
}
|
|
41
|
+
// Validate key format (should start with sk-)
|
|
42
|
+
if (!this.apiKey.startsWith('sk-')) {
|
|
43
|
+
throw new Error(`Invalid OpenAI API key format. Key should start with "sk-" but got "${this.apiKey.slice(0, 3)}..."`);
|
|
44
|
+
}
|
|
40
45
|
this.initialized = true;
|
|
41
46
|
}
|
|
42
47
|
async embed(texts) {
|
|
@@ -68,8 +73,22 @@ export class OpenAIEmbeddingProvider {
|
|
|
68
73
|
}),
|
|
69
74
|
});
|
|
70
75
|
if (!response.ok) {
|
|
71
|
-
const
|
|
72
|
-
|
|
76
|
+
const errorText = await response.text();
|
|
77
|
+
let errorMessage;
|
|
78
|
+
try {
|
|
79
|
+
const errorJson = JSON.parse(errorText);
|
|
80
|
+
errorMessage = errorJson.error?.message || errorText;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
errorMessage = errorText;
|
|
84
|
+
}
|
|
85
|
+
// Provide helpful context for common errors
|
|
86
|
+
if (response.status === 401) {
|
|
87
|
+
const keyPreview = `${this.apiKey.slice(0, 7)}...${this.apiKey.slice(-4)}`;
|
|
88
|
+
throw new Error(`OpenAI API authentication failed (401). Key format: ${keyPreview}. ` +
|
|
89
|
+
`Verify your API key at https://platform.openai.com/api-keys. Error: ${errorMessage}`);
|
|
90
|
+
}
|
|
91
|
+
throw new Error(`OpenAI API error (${response.status}): ${errorMessage}`);
|
|
73
92
|
}
|
|
74
93
|
const data = (await response.json());
|
|
75
94
|
// Sort by index to ensure correct order
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API key validation for cloud embedding providers.
|
|
3
|
+
*
|
|
4
|
+
* Makes a minimal test embedding call to verify the API key is valid
|
|
5
|
+
* before proceeding with indexing.
|
|
6
|
+
*/
|
|
7
|
+
import type { EmbeddingProviderType } from '../../common/types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Result of API key validation.
|
|
10
|
+
*/
|
|
11
|
+
export interface ValidationResult {
|
|
12
|
+
valid: boolean;
|
|
13
|
+
error?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Validate an API key by making a minimal test embedding call.
|
|
17
|
+
*
|
|
18
|
+
* @param provider - The embedding provider type
|
|
19
|
+
* @param apiKey - The API key to validate
|
|
20
|
+
* @returns Validation result with error message if invalid
|
|
21
|
+
*/
|
|
22
|
+
export declare function validateApiKey(provider: EmbeddingProviderType, apiKey: string): Promise<ValidationResult>;
|