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.
Files changed (53) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/commands/mcp-setup.d.ts +1 -1
  3. package/dist/cli/commands/mcp-setup.js +23 -3
  4. package/dist/cli/components/CleanWizard.js +16 -1
  5. package/dist/cli/components/InitWizard.js +37 -15
  6. package/dist/common/types.d.ts +2 -2
  7. package/dist/mcp/index.js +5 -1
  8. package/dist/mcp/warmup.d.ts +5 -0
  9. package/dist/mcp/warmup.js +7 -0
  10. package/dist/rag/config/index.d.ts +4 -0
  11. package/dist/rag/config/index.js +37 -13
  12. package/dist/rag/embeddings/gemini.js +34 -7
  13. package/dist/rag/embeddings/index.d.ts +1 -0
  14. package/dist/rag/embeddings/index.js +1 -0
  15. package/dist/rag/embeddings/mistral.d.ts +2 -2
  16. package/dist/rag/embeddings/mistral.js +18 -5
  17. package/dist/rag/embeddings/openai.js +22 -3
  18. package/dist/rag/embeddings/validate.d.ts +22 -0
  19. package/dist/rag/embeddings/validate.js +148 -0
  20. package/dist/rag/index.d.ts +1 -1
  21. package/dist/rag/index.js +1 -1
  22. package/dist/rag/indexer/chunker.js +31 -19
  23. package/dist/rag/indexer/indexer.d.ts +10 -0
  24. package/dist/rag/indexer/indexer.js +88 -53
  25. package/dist/rag/search/index.d.ts +6 -0
  26. package/dist/rag/search/index.js +35 -9
  27. package/dist/rag/storage/index.d.ts +15 -1
  28. package/dist/rag/storage/index.js +108 -21
  29. package/package.json +33 -4
  30. package/dist/cli/__tests__/mcp-setup-comprehensive.test.d.ts +0 -10
  31. package/dist/cli/__tests__/mcp-setup-comprehensive.test.js +0 -515
  32. package/dist/cli/__tests__/mcp-setup-global.test.d.ts +0 -7
  33. package/dist/cli/__tests__/mcp-setup-global.test.js +0 -577
  34. package/dist/cli/__tests__/mcp-setup.test.d.ts +0 -6
  35. package/dist/cli/__tests__/mcp-setup.test.js +0 -704
  36. package/dist/rag/__tests__/grammar-smoke.test.d.ts +0 -9
  37. package/dist/rag/__tests__/grammar-smoke.test.js +0 -161
  38. package/dist/rag/__tests__/helpers.d.ts +0 -30
  39. package/dist/rag/__tests__/helpers.js +0 -67
  40. package/dist/rag/__tests__/merkle.test.d.ts +0 -5
  41. package/dist/rag/__tests__/merkle.test.js +0 -161
  42. package/dist/rag/__tests__/metadata-extraction.test.d.ts +0 -10
  43. package/dist/rag/__tests__/metadata-extraction.test.js +0 -202
  44. package/dist/rag/__tests__/multi-language.test.d.ts +0 -13
  45. package/dist/rag/__tests__/multi-language.test.js +0 -535
  46. package/dist/rag/__tests__/rag.test.d.ts +0 -10
  47. package/dist/rag/__tests__/rag.test.js +0 -311
  48. package/dist/rag/__tests__/search-exhaustive.test.d.ts +0 -9
  49. package/dist/rag/__tests__/search-exhaustive.test.js +0 -87
  50. package/dist/rag/__tests__/search-filters.test.d.ts +0 -10
  51. package/dist/rag/__tests__/search-filters.test.js +0 -250
  52. package/dist/rag/__tests__/search-modes.test.d.ts +0 -8
  53. 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 | 1024 | $0.10/1M | [Mistral Console](https://console.mistral.ai/api-keys/) |
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
- addOutput('system', `Failed to remove ${viberagDir}: ${error instanceof Error ? error.message : String(error)}`);
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: '768',
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: '1024',
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: '768',
101
+ Dims: '1536',
102
102
  Cost: 'Free tier',
103
103
  },
104
104
  {
105
105
  Provider: 'Mistral',
106
106
  Model: 'codestral-embed',
107
- Dims: '1024',
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(apiKeyInput);
191
+ onSubmit(inputRef.current);
187
192
  }
188
193
  else if (key.backspace || key.delete) {
189
- setApiKeyInput(apiKeyInput.slice(0, -1));
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
- setApiKeyInput(apiKeyInput + input);
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, apiKeyInput),
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
- // Skip API key step for local providers
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
  }
@@ -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 (768d) - Free tier
86
- * - mistral: codestral-embed (1024d) - Code-optimized
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) {
@@ -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.
@@ -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
  /**
@@ -18,7 +18,7 @@ export const PROVIDER_CONFIGS = {
18
18
  },
19
19
  mistral: {
20
20
  model: 'codestral-embed',
21
- dimensions: 1024,
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
- const content = await fs.readFile(configPath, 'utf-8');
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
- this.apiKey = apiKey ?? '';
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?key=${this.apiKey}`;
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 error = await response.text();
82
- throw new Error(`Gemini API error: ${response.status} - ${error}`);
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?key=${this.apiKey}`;
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 error = await response.text();
108
- throw new Error(`Gemini API error: ${response.status} - ${error}`);
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 (1024 dimensions).
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 = 1024;
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 (1024 dimensions).
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: 1024
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
- this.apiKey = apiKey ?? '';
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 error = await response.text();
72
- throw new Error(`Mistral API error: ${response.status} - ${error}`);
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
- this.apiKey = apiKey ?? '';
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 error = await response.text();
72
- throw new Error(`OpenAI API error: ${response.status} - ${error}`);
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>;