viberag 0.5.0 → 0.6.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 (42) hide show
  1. package/README.md +42 -10
  2. package/dist/cli/app.js +6 -6
  3. package/dist/cli/commands/handlers.d.ts +2 -2
  4. package/dist/cli/commands/handlers.js +40 -27
  5. package/dist/cli/commands/useCommands.js +1 -1
  6. package/dist/cli/components/CleanWizard.js +31 -3
  7. package/dist/cli/components/InitWizard.d.ts +3 -3
  8. package/dist/cli/components/InitWizard.js +123 -45
  9. package/dist/cli/components/StatusBar.js +4 -1
  10. package/dist/cli/store/wizard/selectors.d.ts +1 -1
  11. package/dist/cli/store/wizard/selectors.js +1 -1
  12. package/dist/cli/store/wizard/slice.d.ts +38 -22
  13. package/dist/cli/store/wizard/slice.js +3 -3
  14. package/dist/cli/utils/error-handler.d.ts +3 -3
  15. package/dist/cli/utils/error-handler.js +3 -3
  16. package/dist/client/auto-start.js +28 -18
  17. package/dist/common/types.d.ts +9 -1
  18. package/dist/daemon/index.d.ts +5 -2
  19. package/dist/daemon/index.js +15 -9
  20. package/dist/daemon/lib/config.d.ts +13 -4
  21. package/dist/daemon/lib/config.js +48 -5
  22. package/dist/daemon/lib/constants.d.ts +57 -5
  23. package/dist/daemon/lib/constants.js +103 -6
  24. package/dist/daemon/lib/gitignore.d.ts +4 -2
  25. package/dist/daemon/lib/gitignore.js +58 -28
  26. package/dist/daemon/lib/logger.d.ts +5 -5
  27. package/dist/daemon/lib/logger.js +6 -6
  28. package/dist/daemon/lib/merkle/index.d.ts +1 -1
  29. package/dist/daemon/lib/merkle/index.js +2 -2
  30. package/dist/daemon/lib/secrets.d.ts +51 -0
  31. package/dist/daemon/lib/secrets.js +128 -0
  32. package/dist/daemon/owner.js +4 -13
  33. package/dist/daemon/server.js +3 -3
  34. package/dist/daemon/services/v2/indexing.d.ts +1 -0
  35. package/dist/daemon/services/v2/indexing.js +19 -3
  36. package/dist/daemon/services/v2/search/engine.d.ts +1 -0
  37. package/dist/daemon/services/v2/search/engine.js +19 -3
  38. package/dist/daemon/services/watcher.d.ts +1 -1
  39. package/dist/daemon/services/watcher.js +12 -7
  40. package/dist/test-setup.d.ts +1 -0
  41. package/dist/test-setup.js +9 -0
  42. package/package.json +1 -1
package/README.md CHANGED
@@ -51,7 +51,7 @@ When using a coding agent like [Claude Code](https://claude.ai/code), add `use v
51
51
  - **Agent-first search** - Find definitions, entry files, and relevant blocks (not just “chunks”)
52
52
  - **Flexible embeddings** - Local model (offline, free) or cloud providers (Gemini, Mistral, OpenAI)
53
53
  - **MCP server** - Works with Claude Code, Cursor, VS Code Copilot, and more
54
- - **Automatic incremental indexing** - Watches for file changes (respects `.gitignore`) and reindexes only what has changed in real time
54
+ - **Automatic incremental indexing** - Watches for file changes (respects `.gitignore` + `.viberagignore`) and reindexes only what has changed in real time
55
55
  - **Cancelable indexing** - Supports `/cancel` and clear status reporting via `/status`
56
56
  - **Multi-language support** - TypeScript, JavaScript, Python, Go, Rust, and more
57
57
  - **Blazing fast** - The data storage and search functionality is local on your machine, meaning the full power of your machine can churn through massive amounts of data and execute complex search queries in milliseconds.
@@ -510,17 +510,49 @@ VibeRAG includes a CLI for easy execution of initialization, indexing, setup, an
510
510
  | `/clean` | Remove VibeRAG from project |
511
511
  | `/help` | Show all commands |
512
512
 
513
+ ## Data Directory
514
+
515
+ VibeRAG stores all per-project state (config, index, logs) globally under:
516
+
517
+ - `~/.local/share/viberag/projects/<projectId>/` (override via `VIBERAG_HOME`)
518
+
519
+ No files are written into your repo.
520
+
521
+ ## Ignoring Files
522
+
523
+ VibeRAG uses `.gitignore` rules to exclude files and folders from indexing. For
524
+ non-git projects (or for additional ignore patterns), you can create a
525
+ `.viberagignore` file in the project root.
526
+
527
+ - `.viberagignore` uses the exact same pattern syntax as `.gitignore`
528
+ - It is applied in addition to `.gitignore` (if present)
529
+
530
+ Example `.viberagignore`:
531
+
532
+ ```gitignore
533
+ # build outputs
534
+ dist/
535
+ build/
536
+
537
+ # local artifacts
538
+ coverage/
539
+ tmp/
540
+
541
+ # generated bundles
542
+ **/*.min.js
543
+ ```
544
+
513
545
  ## Logs
514
546
 
515
- VibeRAG writes per-service logs to `.viberag/logs/` with hourly rotation:
547
+ VibeRAG writes per-service logs with hourly rotation:
516
548
 
517
- - `.viberag/logs/daemon/` - daemon lifecycle and IPC errors
518
- - `.viberag/logs/indexer/` - indexing progress, retries, and batch failures
519
- - `.viberag/logs/mcp/` - MCP server errors
520
- - `.viberag/logs/cli/` - CLI errors
549
+ - `~/.local/share/viberag/projects/<projectId>/logs/daemon/` - daemon lifecycle and IPC errors
550
+ - `~/.local/share/viberag/projects/<projectId>/logs/indexer/` - indexing progress, retries, and batch failures
551
+ - `~/.local/share/viberag/projects/<projectId>/logs/mcp/` - MCP server errors
552
+ - `~/.local/share/viberag/projects/<projectId>/logs/cli/` - CLI errors
521
553
 
522
554
  If indexing appears slow or retries are happening, check the latest file under
523
- `.viberag/logs/indexer/`.
555
+ `~/.local/share/viberag/projects/<projectId>/logs/indexer/`.
524
556
 
525
557
  ## Embedding Providers
526
558
 
@@ -548,7 +580,7 @@ Choose your embedding provider during `/init`:
548
580
  - **Mistral** - Code-optimized embeddings for technical content
549
581
  - **OpenAI** - Fast and reliable with low cost
550
582
 
551
- API keys are entered during the `/init` wizard and stored securely in `.viberag/config.json` (automatically added to `.gitignore`).
583
+ API keys are entered during the `/init` wizard and stored globally in `~/.local/share/viberag/secrets/secrets.json` (override via `VIBERAG_HOME`). Project configs store only a key id reference (never the raw API key).
552
584
 
553
585
  ## How It Works
554
586
 
@@ -677,9 +709,9 @@ Example sequence:
677
709
 
678
710
  ### Watcher EMFILE (too many open files)
679
711
 
680
- Large repos can exceed OS watch limits. The watcher now honors `.gitignore`, but if you still see EMFILE:
712
+ Large repos can exceed OS watch limits. The watcher honors `.gitignore` and `.viberagignore`, but if you still see EMFILE:
681
713
 
682
- - Add more ignores in `.gitignore` to reduce watched files.
714
+ - Add more ignores in `.gitignore` or `.viberagignore` to reduce watched files.
683
715
  - Increase OS limits:
684
716
  - macOS: raise `kern.maxfiles`, `kern.maxfilesperproc`, and `ulimit -n`
685
717
  - Linux: raise `fs.inotify.max_user_watches`, `fs.inotify.max_user_instances`, and `ulimit -n`
package/dist/cli/app.js CHANGED
@@ -5,7 +5,7 @@ import { Provider } from 'react-redux';
5
5
  import { store } from './store/store.js';
6
6
  import { useAppDispatch, useAppSelector } from './store/hooks.js';
7
7
  import { WizardActions } from './store/wizard/slice.js';
8
- import { selectActiveWizard, selectInitStep, selectMcpStep, selectInitConfig, selectMcpConfig, selectIsReinit, selectShowMcpPrompt, selectExistingApiKey, selectExistingProvider, } from './store/wizard/selectors.js';
8
+ import { selectActiveWizard, selectInitStep, selectMcpStep, selectInitConfig, selectMcpConfig, selectIsReinit, selectShowMcpPrompt, selectExistingApiKeyId, selectExistingProvider, } from './store/wizard/selectors.js';
9
9
  import { AppActions } from './store/app/slice.js';
10
10
  import { selectIsInitialized, selectIndexStats, selectAppStatus, selectOutputItems, selectStartupLoaded, } from './store/app/selectors.js';
11
11
  // Common infrastructure
@@ -63,7 +63,7 @@ function AppContent() {
63
63
  const mcpConfig = useAppSelector(selectMcpConfig);
64
64
  const isReinit = useAppSelector(selectIsReinit);
65
65
  const showMcpPrompt = useAppSelector(selectShowMcpPrompt);
66
- const existingApiKey = useAppSelector(selectExistingApiKey);
66
+ const existingApiKeyId = useAppSelector(selectExistingApiKeyId);
67
67
  const existingProvider = useAppSelector(selectExistingProvider);
68
68
  // Redux app state (migrated from useState)
69
69
  const isInitialized = useAppSelector(selectIsInitialized);
@@ -84,7 +84,7 @@ function AppContent() {
84
84
  if (initialized) {
85
85
  const config = await loadConfig(projectRoot);
86
86
  dispatch(WizardActions.setExistingConfig({
87
- apiKey: config.apiKey,
87
+ apiKeyId: config.apiKeyRef?.keyId,
88
88
  provider: config.embeddingProvider,
89
89
  }));
90
90
  }
@@ -127,10 +127,10 @@ function AppContent() {
127
127
  const startInitWizard = useCallback((isReinit) => {
128
128
  dispatch(WizardActions.startInit({
129
129
  isReinit,
130
- existingApiKey: existingApiKey ?? undefined,
130
+ existingApiKeyId: existingApiKeyId ?? undefined,
131
131
  existingProvider: existingProvider ?? undefined,
132
132
  }));
133
- }, [dispatch, existingApiKey, existingProvider]);
133
+ }, [dispatch, existingApiKeyId, existingProvider]);
134
134
  // Start the MCP setup wizard
135
135
  const startMcpSetupWizard = useCallback((showPrompt = false) => {
136
136
  dispatch(WizardActions.startMcpSetup({ showPrompt }));
@@ -257,7 +257,7 @@ function AppContent() {
257
257
  item.content)) : (React.createElement(Text, null, item.content))));
258
258
  }),
259
259
  React.createElement(StatusBar, { status: appStatus, stats: indexStats }),
260
- activeWizard === 'init' ? (React.createElement(InitWizard, { step: initStep, config: initConfig, isReinit: isReinit, existingApiKey: existingApiKey ?? undefined, existingProvider: existingProvider ?? undefined, onStepChange: handleInitWizardStep, onComplete: handleInitWizardComplete, onCancel: handleWizardCancel })) : activeWizard === 'mcp-setup' ? (React.createElement(McpSetupWizard, { step: mcpStep, config: mcpConfig, projectRoot: projectRoot, showPrompt: showMcpPrompt, onStepChange: handleMcpWizardStep, onComplete: handleMcpWizardComplete, onCancel: handleWizardCancel, addOutput: addOutput })) : activeWizard === 'clean' ? (React.createElement(CleanWizard, { projectRoot: projectRoot, viberagDir: getViberagDir(projectRoot), onComplete: handleCleanWizardComplete, onCancel: handleWizardCancel, addOutput: addOutput })) : (React.createElement(TextInput, { onSubmit: handleSubmit, onCtrlC: handleCtrlC, commands: COMMANDS, navigateHistoryUp: navigateUp, navigateHistoryDown: navigateDown, resetHistoryIndex: resetIndex }))));
260
+ activeWizard === 'init' ? (React.createElement(InitWizard, { step: initStep, config: initConfig, isReinit: isReinit, existingApiKeyId: existingApiKeyId ?? undefined, existingProvider: existingProvider ?? undefined, onStepChange: handleInitWizardStep, onComplete: handleInitWizardComplete, onCancel: handleWizardCancel })) : activeWizard === 'mcp-setup' ? (React.createElement(McpSetupWizard, { step: mcpStep, config: mcpConfig, projectRoot: projectRoot, showPrompt: showMcpPrompt, onStepChange: handleMcpWizardStep, onComplete: handleMcpWizardComplete, onCancel: handleWizardCancel, addOutput: addOutput })) : activeWizard === 'clean' ? (React.createElement(CleanWizard, { projectRoot: projectRoot, viberagDir: getViberagDir(projectRoot), onComplete: handleCleanWizardComplete, onCancel: handleWizardCancel, addOutput: addOutput })) : (React.createElement(TextInput, { onSubmit: handleSubmit, onCtrlC: handleCtrlC, commands: COMMANDS, navigateHistoryUp: navigateUp, navigateHistoryDown: navigateDown, resetHistoryIndex: resetIndex }))));
261
261
  }
262
262
  /**
263
263
  * Main App component with Redux Provider and DaemonStatusProvider.
@@ -13,7 +13,7 @@ export declare function checkInitialized(projectRoot: string): Promise<boolean>;
13
13
  export declare function loadIndexStats(projectRoot: string): Promise<IndexDisplayStats | null>;
14
14
  /**
15
15
  * Initialize a project for Viberag.
16
- * Creates .viberag/ directory with config.json.
16
+ * Creates a global per-project data directory with config.json.
17
17
  * With isReinit=true, shuts down daemon and deletes everything first.
18
18
  * Optionally reports progress for UI status updates.
19
19
  */
@@ -53,7 +53,7 @@ export declare function getStatus(projectRoot: string): Promise<string>;
53
53
  export declare function cancelActivity(projectRoot: string, target?: string): Promise<string>;
54
54
  /**
55
55
  * Clean/uninstall Viberag from a project.
56
- * Shuts down daemon first, then removes the entire .viberag/ directory.
56
+ * Shuts down daemon first, then removes the entire project data directory.
57
57
  */
58
58
  export declare function runClean(projectRoot: string): Promise<string>;
59
59
  /**
@@ -2,11 +2,11 @@
2
2
  * RAG commands for the CLI.
3
3
  */
4
4
  import fs from 'node:fs/promises';
5
- import path from 'node:path';
6
5
  import chalk from 'chalk';
7
6
  import { computeStringHash } from '../../daemon/lib/merkle/hash.js';
8
7
  import { configExists, saveConfig, DEFAULT_CONFIG, PROVIDER_CONFIGS, } from '../../daemon/lib/config.js';
9
- import { getViberagDir } from '../../daemon/lib/constants.js';
8
+ import { getSecretsPath, getRunDir, getServiceLogsDir, getViberagDir, } from '../../daemon/lib/constants.js';
9
+ import { addApiKey } from '../../daemon/lib/secrets.js';
10
10
  import { loadV2Manifest, checkV2IndexCompatibility, v2ManifestExists, } from '../../daemon/services/v2/manifest.js';
11
11
  import { getGrammarSupportSummary } from '../../daemon/lib/chunker/grammars.js';
12
12
  import { checkNpmForUpdate } from '../../daemon/lib/update-check.js';
@@ -36,14 +36,14 @@ export async function loadIndexStats(projectRoot) {
36
36
  }
37
37
  /**
38
38
  * Initialize a project for Viberag.
39
- * Creates .viberag/ directory with config.json.
39
+ * Creates a global per-project data directory with config.json.
40
40
  * With isReinit=true, shuts down daemon and deletes everything first.
41
41
  * Optionally reports progress for UI status updates.
42
42
  */
43
43
  export async function runInit(projectRoot, isReinit = false, wizardConfig, onProgress) {
44
44
  const viberagDir = getViberagDir(projectRoot);
45
45
  const isExisting = await configExists(projectRoot);
46
- // If reinit, shutdown daemon and delete entire .viberag directory first
46
+ // If reinit, shutdown daemon and delete entire project data directory first
47
47
  if (isReinit && isExisting) {
48
48
  onProgress?.('Stopping daemon');
49
49
  const client = new DaemonClient(projectRoot);
@@ -61,11 +61,13 @@ export async function runInit(projectRoot, isReinit = false, wizardConfig, onPro
61
61
  finally {
62
62
  await client.disconnect();
63
63
  }
64
- onProgress?.('Removing .viberag');
64
+ onProgress?.('Removing project data');
65
65
  await fs.rm(viberagDir, { recursive: true, force: true });
66
+ // Also remove runtime files (socket/pid/lock)
67
+ await fs.rm(getRunDir(projectRoot), { recursive: true, force: true });
66
68
  }
67
- // Create .viberag directory
68
- onProgress?.('Creating .viberag');
69
+ // Create global per-project directory
70
+ onProgress?.('Creating project data');
69
71
  await fs.mkdir(viberagDir, { recursive: true });
70
72
  // Build config from wizard choices
71
73
  const provider = wizardConfig?.provider ?? 'gemini';
@@ -83,28 +85,35 @@ export async function runInit(projectRoot, isReinit = false, wizardConfig, onPro
83
85
  embeddingProvider: provider,
84
86
  embeddingModel: model,
85
87
  embeddingDimensions: dimensions,
86
- ...(wizardConfig?.apiKey && { apiKey: wizardConfig.apiKey }),
87
88
  ...(openaiBaseUrl && { openaiBaseUrl }),
88
89
  };
90
+ // Configure API key reference for cloud providers
91
+ if (provider !== 'local') {
92
+ let keyId = wizardConfig?.apiKeyId;
93
+ if (!keyId && wizardConfig?.apiKey) {
94
+ onProgress?.('Saving API key');
95
+ const result = await addApiKey({
96
+ provider,
97
+ apiKey: wizardConfig.apiKey,
98
+ makeDefault: true,
99
+ });
100
+ keyId = result.keyId;
101
+ }
102
+ if (!keyId) {
103
+ throw new Error(`${provider} API key required. Add one via /init (stored at ${getSecretsPath()}).`);
104
+ }
105
+ config.apiKeyRef = { provider, keyId };
106
+ }
89
107
  // Save config
90
108
  onProgress?.('Writing config');
91
109
  await saveConfig(projectRoot, config);
92
- // Add .viberag/ to .gitignore if not present
93
- const gitignorePath = path.join(projectRoot, '.gitignore');
94
- onProgress?.('Updating .gitignore');
95
- try {
96
- const content = await fs.readFile(gitignorePath, 'utf-8');
97
- if (!content.includes('.viberag')) {
98
- await fs.appendFile(gitignorePath, '\n# Viberag index\n.viberag/\n');
99
- }
100
- }
101
- catch {
102
- // .gitignore doesn't exist, create it
103
- await fs.writeFile(gitignorePath, '# Viberag index\n.viberag/\n');
104
- }
105
110
  const action = isExisting ? 'Reinitialized' : 'Initialized';
106
111
  const providerLabel = provider.charAt(0).toUpperCase() + provider.slice(1);
107
- return `${action} Viberag in ${viberagDir}\nProvider: ${providerLabel}\nModel: ${model} (${dimensions}d)\nRun /index to build the code index.`;
112
+ return (`${action} VibeRAG for ${projectRoot}\n` +
113
+ `Data: ${viberagDir}\n` +
114
+ `Provider: ${providerLabel}\n` +
115
+ `Model: ${model} (${dimensions}d)\n` +
116
+ 'Run /index to build the code index.');
108
117
  }
109
118
  /**
110
119
  * Run the indexer and return stats.
@@ -344,7 +353,7 @@ export async function getStatus(projectRoot) {
344
353
  updatePromise,
345
354
  compatibilityPromise,
346
355
  ]);
347
- return formatStatusWithStartupChecks(formatDaemonStatus(daemonStatus), {
356
+ return formatStatusWithStartupChecks(formatDaemonStatus(daemonStatus, projectRoot), {
348
357
  update,
349
358
  compatibility,
350
359
  });
@@ -412,7 +421,7 @@ function formatStatusWithStartupChecks(body, args) {
412
421
  }
413
422
  return `${lines.join('\n')}${body}`;
414
423
  }
415
- function formatDaemonStatus(status) {
424
+ function formatDaemonStatus(status, projectRoot) {
416
425
  const lines = ['Daemon status:'];
417
426
  lines.push(` Initialized: ${status.initialized ? 'yes' : 'no'}`);
418
427
  if (status.indexed) {
@@ -472,7 +481,7 @@ function formatDaemonStatus(status) {
472
481
  lines.push(` Slots: ${slotSummary}`);
473
482
  }
474
483
  if (status.failures.length > 0) {
475
- lines.push(` Failures: ${status.failures.length} batch(es) - see .viberag/logs/indexer/`);
484
+ lines.push(` Failures: ${status.failures.length} batch(es) - see ${getServiceLogsDir(projectRoot, 'indexer')}`);
476
485
  }
477
486
  const watcher = status.watcherStatus;
478
487
  const watcherParts = [
@@ -560,7 +569,7 @@ function normalizeCancelTarget(target) {
560
569
  }
561
570
  /**
562
571
  * Clean/uninstall Viberag from a project.
563
- * Shuts down daemon first, then removes the entire .viberag/ directory.
572
+ * Shuts down daemon first, then removes the entire project data directory.
564
573
  */
565
574
  export async function runClean(projectRoot) {
566
575
  const viberagDir = getViberagDir(projectRoot);
@@ -585,7 +594,11 @@ export async function runClean(projectRoot) {
585
594
  await client.disconnect();
586
595
  }
587
596
  await fs.rm(viberagDir, { recursive: true, force: true });
588
- return `Removed ${viberagDir}\nViberag has been uninstalled from this project.\nRun /init to reinitialize.`;
597
+ await fs.rm(getRunDir(projectRoot), { recursive: true, force: true });
598
+ return (`Removed ${viberagDir}\n` +
599
+ 'VibeRAG has been uninstalled for this project.\n' +
600
+ 'Run /init to reinitialize.\n' +
601
+ `Note: API keys are stored globally at ${getSecretsPath()}.`);
589
602
  }
590
603
  /**
591
604
  * Get MCP setup instructions for Claude Code.
@@ -25,7 +25,7 @@ export function useCommands({ addOutput, addSearchResults, projectRoot, stdout,
25
25
  /status - Show index status
26
26
  /cancel [target] - Cancel indexing or warmup (targets: indexing, warmup)
27
27
  /mcp-setup - Configure MCP server for AI coding tools
28
- /clean - Remove Viberag from project (delete .viberag/)
28
+ /clean - Remove VibeRAG from project (delete project data)
29
29
  /quit - Exit
30
30
 
31
31
  Multi-line input:
@@ -7,8 +7,10 @@
7
7
  import React, { useState, useEffect, useCallback } from 'react';
8
8
  import { Box, Text, useInput } from 'ink';
9
9
  import SelectInput from 'ink-select-input';
10
+ import { DaemonClient } from '../../client/index.js';
10
11
  import { EDITORS, getConfigPath } from '../data/mcp-editors.js';
11
12
  import { findConfiguredEditors, removeViberagConfig, } from '../commands/mcp-setup.js';
13
+ import { getRunDir } from '../../daemon/lib/constants.js';
12
14
  const CONFIRM_ITEMS = [
13
15
  { label: 'Yes, remove everything', value: 'continue' },
14
16
  { label: 'Cancel', value: 'cancel' },
@@ -67,7 +69,26 @@ export function CleanWizard({ projectRoot, viberagDir, onComplete, onCancel, add
67
69
  // Perform the actual cleanup
68
70
  const performCleanup = useCallback(async (cleanProjectMcpArg, cleanGlobalMcp) => {
69
71
  const fs = await import('node:fs/promises');
70
- // Remove .viberag directory
72
+ // Shutdown daemon first (prevents stale DB handles / sockets)
73
+ const client = new DaemonClient({
74
+ projectRoot,
75
+ autoStart: false,
76
+ });
77
+ try {
78
+ if (await client.isRunning()) {
79
+ await client.connect();
80
+ await client.shutdown('clean');
81
+ // Give the daemon a moment to exit
82
+ await new Promise(r => setTimeout(r, 500));
83
+ }
84
+ }
85
+ catch {
86
+ // Ignore errors - daemon may not be running
87
+ }
88
+ finally {
89
+ await client.disconnect();
90
+ }
91
+ // Remove global per-project data directory
71
92
  try {
72
93
  await fs.rm(viberagDir, { recursive: true, force: true });
73
94
  setViberagRemoved(true);
@@ -90,6 +111,13 @@ export function CleanWizard({ projectRoot, viberagDir, onComplete, onCancel, add
90
111
  return;
91
112
  }
92
113
  }
114
+ // Remove runtime files (socket/pid/lock)
115
+ try {
116
+ await fs.rm(getRunDir(projectRoot), { recursive: true, force: true });
117
+ }
118
+ catch {
119
+ // Ignore
120
+ }
93
121
  const results = [];
94
122
  // Clean project MCP configs if requested
95
123
  if (cleanProjectMcpArg) {
@@ -169,7 +197,7 @@ export function CleanWizard({ projectRoot, viberagDir, onComplete, onCancel, add
169
197
  return (React.createElement(Box, { flexDirection: "column", borderStyle: "round", paddingX: 2, paddingY: 1 },
170
198
  React.createElement(Text, { bold: true, color: "yellow" }, "Remove from MCP configs?"),
171
199
  React.createElement(Box, { marginTop: 1, flexDirection: "column" },
172
- React.createElement(Text, null, "Found viberag in these project MCP configs:"),
200
+ React.createElement(Text, null, "Found VibeRAG in these project MCP configs:"),
173
201
  projectScopeConfigs.map(info => (React.createElement(Text, { key: `${info.editor.id}-${info.scope}`, dimColor: true },
174
202
  ' ',
175
203
  "\u2022 ",
@@ -188,7 +216,7 @@ export function CleanWizard({ projectRoot, viberagDir, onComplete, onCancel, add
188
216
  React.createElement(Box, { marginTop: 1 },
189
217
  React.createElement(Text, { color: "yellow" }, "Note: Other projects using this config will need to run /mcp-setup again.")),
190
218
  React.createElement(Box, { marginTop: 1, flexDirection: "column" },
191
- React.createElement(Text, null, "Found viberag in these global configs:"),
219
+ React.createElement(Text, null, "Found VibeRAG in these global configs:"),
192
220
  globalScopeConfigs.map(info => (React.createElement(Text, { key: `${info.editor.id}-${info.scope}`, dimColor: true },
193
221
  ' ',
194
222
  "\u2022 ",
@@ -8,13 +8,13 @@ type Props = {
8
8
  step: number;
9
9
  config: Partial<InitWizardConfig>;
10
10
  isReinit: boolean;
11
- /** Existing API key from previous config (for reinit flow) */
12
- existingApiKey?: string;
11
+ /** Existing API key id from previous config (for reinit flow) */
12
+ existingApiKeyId?: string;
13
13
  /** Existing provider from previous config (for reinit flow) */
14
14
  existingProvider?: EmbeddingProviderType;
15
15
  onStepChange: (step: number, data?: Partial<InitWizardConfig>) => void;
16
16
  onComplete: (config: InitWizardConfig) => void;
17
17
  onCancel: () => void;
18
18
  };
19
- export declare function InitWizard({ step, config, isReinit, existingApiKey, existingProvider, onStepChange, onComplete, onCancel, }: Props): React.ReactElement;
19
+ export declare function InitWizard({ step, config, isReinit, existingApiKeyId, existingProvider, onStepChange, onComplete, onCancel, }: Props): React.ReactElement;
20
20
  export default InitWizard;
@@ -5,6 +5,8 @@
5
5
  import React, { useState, useEffect } from 'react';
6
6
  import { Box, Text, useInput } from 'ink';
7
7
  import SelectInput from 'ink-select-input';
8
+ import { getViberagDir } from '../../daemon/lib/constants.js';
9
+ import { listApiKeys, } from '../../daemon/lib/secrets.js';
8
10
  /**
9
11
  * Cloud providers that require API keys.
10
12
  */
@@ -159,11 +161,6 @@ const CONFIRM_ITEMS = [
159
161
  { label: 'Initialize', value: 'init' },
160
162
  { label: 'Cancel', value: 'cancel' },
161
163
  ];
162
- // API key action options for reinit
163
- const API_KEY_ACTION_ITEMS = [
164
- { label: 'Keep existing API key', value: 'keep' },
165
- { label: 'Enter new API key', value: 'new' },
166
- ];
167
164
  // OpenAI region options for data residency
168
165
  const OPENAI_REGION_ITEMS = [
169
166
  {
@@ -226,15 +223,26 @@ function ApiKeyInputStep({ providerName, apiKeyInput, setApiKeyInput, onSubmit,
226
223
  apiKeyInput.trim() === '' && (React.createElement(Text, { color: "yellow", dimColor: true }, "API key is required")),
227
224
  React.createElement(Text, { dimColor: true }, "Press Enter to continue")));
228
225
  }
229
- export function InitWizard({ step, config, isReinit, existingApiKey, existingProvider, onStepChange, onComplete, onCancel, }) {
226
+ export function InitWizard({ step, config, isReinit, existingApiKeyId, existingProvider, onStepChange, onComplete, onCancel, }) {
230
227
  // State for API key input
231
228
  const [apiKeyInput, setApiKeyInput] = useState('');
232
- const [apiKeyAction, setApiKeyAction] = useState(null);
229
+ const [enteringNewKey, setEnteringNewKey] = useState(false);
230
+ const [availableKeys, setAvailableKeys] = useState(null);
231
+ const [keysLoading, setKeysLoading] = useState(false);
232
+ const [keysError, setKeysError] = useState(null);
233
233
  // State for OpenAI region selection (shown after API key for OpenAI)
234
234
  const [showRegionSelect, setShowRegionSelect] = useState(false);
235
235
  // Handle Escape to cancel
236
236
  useInput((input, key) => {
237
- if (key.escape || (key.ctrl && input === 'c')) {
237
+ if (key.escape) {
238
+ if (enteringNewKey) {
239
+ setEnteringNewKey(false);
240
+ return;
241
+ }
242
+ onCancel();
243
+ return;
244
+ }
245
+ if (key.ctrl && input === 'c') {
238
246
  onCancel();
239
247
  }
240
248
  });
@@ -244,6 +252,8 @@ export function InitWizard({ step, config, isReinit, existingApiKey, existingPro
244
252
  // Check if current provider is a cloud provider
245
253
  const currentProvider = config.provider ?? 'local';
246
254
  const needsApiKey = isCloudProvider(currentProvider);
255
+ // Data directory (global per-project)
256
+ const dataDir = getViberagDir(process.cwd());
247
257
  // Compute effective step (adjusted for non-reinit flow)
248
258
  // Steps: 0=reinit confirm, 1=provider select, 2=api key (cloud only), 3=final confirm
249
259
  const effectiveStep = normalizedIsReinit
@@ -256,7 +266,28 @@ export function InitWizard({ step, config, isReinit, existingApiKey, existingPro
256
266
  }
257
267
  }, [effectiveStep, needsApiKey, normalizedStep, onStepChange]);
258
268
  // Check if we have an existing API key for the same provider
259
- const hasExistingKeyForProvider = existingApiKey && existingProvider === currentProvider;
269
+ const hasExistingKeyForProvider = existingApiKeyId && existingProvider === currentProvider;
270
+ // Load available keys when on the API key step for a cloud provider
271
+ useEffect(() => {
272
+ if (effectiveStep !== 2 || !needsApiKey)
273
+ return;
274
+ setKeysLoading(true);
275
+ setKeysError(null);
276
+ setAvailableKeys(null);
277
+ const provider = currentProvider;
278
+ listApiKeys(provider)
279
+ .then(keys => {
280
+ setAvailableKeys(keys);
281
+ })
282
+ .catch(error => {
283
+ const message = error instanceof Error ? error.message : String(error);
284
+ setKeysError(message);
285
+ setAvailableKeys([]);
286
+ })
287
+ .finally(() => {
288
+ setKeysLoading(false);
289
+ });
290
+ }, [effectiveStep, needsApiKey, currentProvider]);
260
291
  // Step 0 (reinit only): Confirmation
261
292
  if (normalizedIsReinit && normalizedStep === 0) {
262
293
  return (React.createElement(Box, { flexDirection: "column", borderStyle: "round", paddingX: 2, paddingY: 1 },
@@ -285,10 +316,17 @@ export function InitWizard({ step, config, isReinit, existingApiKey, existingPro
285
316
  React.createElement(SelectInput, { items: PROVIDER_ITEMS, onSelect: item => {
286
317
  // Reset API key and region state when provider changes
287
318
  setApiKeyInput('');
288
- setApiKeyAction(null);
319
+ setEnteringNewKey(false);
320
+ setAvailableKeys(null);
321
+ setKeysError(null);
289
322
  setShowRegionSelect(false);
290
323
  // Use relative increment: step + 1
291
- onStepChange(normalizedStep + 1, { provider: item.value });
324
+ onStepChange(normalizedStep + 1, {
325
+ provider: item.value,
326
+ apiKey: undefined,
327
+ apiKeyId: undefined,
328
+ openaiRegion: undefined,
329
+ });
292
330
  } })),
293
331
  React.createElement(Box, { marginTop: 1 },
294
332
  React.createElement(Text, { dimColor: true }, "\u2191/\u2193 navigate, Enter select, Esc cancel"))));
@@ -321,7 +359,7 @@ export function InitWizard({ step, config, isReinit, existingApiKey, existingPro
321
359
  }
322
360
  return (React.createElement(Box, { flexDirection: "column", borderStyle: "round", paddingX: 2, paddingY: 1 },
323
361
  React.createElement(Text, { bold: true },
324
- "Configure ",
362
+ "Select ",
325
363
  info.name,
326
364
  " API Key"),
327
365
  React.createElement(Box, { marginTop: 1, flexDirection: "column" },
@@ -329,52 +367,86 @@ export function InitWizard({ step, config, isReinit, existingApiKey, existingPro
329
367
  "Get your API key:",
330
368
  ' ',
331
369
  React.createElement(Text, { color: "cyan", underline: true }, apiKeyUrl))),
332
- hasExistingKeyForProvider && apiKeyAction === null ? (React.createElement(Box, { marginTop: 1, flexDirection: "column" },
333
- React.createElement(Text, { color: "green" },
334
- "An API key is already configured for ",
335
- info.name,
336
- "."),
337
- React.createElement(Box, { marginTop: 1 },
338
- React.createElement(SelectInput, { items: API_KEY_ACTION_ITEMS, onSelect: item => {
339
- if (item.value === 'keep') {
340
- // Keep existing key
341
- onStepChange(normalizedStep, { apiKey: existingApiKey });
342
- if (isOpenAI) {
343
- // Show region selection for OpenAI
344
- setShowRegionSelect(true);
345
- }
346
- else {
347
- // Advance to confirmation for other providers
348
- onStepChange(normalizedStep + 1, {
349
- apiKey: existingApiKey,
350
- });
351
- }
352
- }
353
- else {
354
- // Show text input for new key
355
- setApiKeyAction('new');
356
- }
357
- } })))) : (React.createElement(ApiKeyInputStep, { providerName: info.name, apiKeyInput: apiKeyInput, setApiKeyInput: setApiKeyInput, onSubmit: key => {
370
+ keysError && (React.createElement(Box, { marginTop: 1 },
371
+ React.createElement(Text, { color: "yellow", dimColor: true },
372
+ "Failed to load existing keys: ",
373
+ keysError))),
374
+ enteringNewKey ? (React.createElement(ApiKeyInputStep, { providerName: info.name, apiKeyInput: apiKeyInput, setApiKeyInput: setApiKeyInput, onSubmit: key => {
358
375
  if (key.trim()) {
359
- onStepChange(normalizedStep, { apiKey: key.trim() });
376
+ onStepChange(normalizedStep, {
377
+ apiKey: key.trim(),
378
+ apiKeyId: undefined,
379
+ });
360
380
  if (isOpenAI) {
361
381
  // Show region selection for OpenAI
362
382
  setShowRegionSelect(true);
363
383
  }
364
384
  else {
365
385
  // Advance to confirmation for other providers
366
- onStepChange(normalizedStep + 1, { apiKey: key.trim() });
386
+ onStepChange(normalizedStep + 1, {
387
+ apiKey: key.trim(),
388
+ apiKeyId: undefined,
389
+ });
367
390
  }
368
391
  }
369
- } })),
392
+ } })) : (React.createElement(Box, { marginTop: 1, flexDirection: "column" },
393
+ keysLoading && React.createElement(Text, { dimColor: true }, "Loading saved keys..."),
394
+ React.createElement(Box, { marginTop: 1 },
395
+ React.createElement(SelectInput, { items: (() => {
396
+ const items = [];
397
+ const seen = new Set();
398
+ if (hasExistingKeyForProvider && existingApiKeyId) {
399
+ items.push({
400
+ label: 'Keep current key',
401
+ value: { kind: 'existing', keyId: existingApiKeyId },
402
+ });
403
+ seen.add(existingApiKeyId);
404
+ }
405
+ for (const key of availableKeys ?? []) {
406
+ if (seen.has(key.id))
407
+ continue;
408
+ items.push({
409
+ label: `${key.label} (${key.preview})`,
410
+ value: { kind: 'existing', keyId: key.id },
411
+ });
412
+ seen.add(key.id);
413
+ }
414
+ items.push({
415
+ label: 'Add new API key',
416
+ value: { kind: 'new' },
417
+ });
418
+ return items;
419
+ })(), onSelect: item => {
420
+ if (item.value.kind === 'new') {
421
+ setEnteringNewKey(true);
422
+ setApiKeyInput('');
423
+ return;
424
+ }
425
+ const keyId = item.value.keyId;
426
+ if (isOpenAI) {
427
+ onStepChange(normalizedStep, {
428
+ apiKeyId: keyId,
429
+ apiKey: undefined,
430
+ });
431
+ setShowRegionSelect(true);
432
+ }
433
+ else {
434
+ onStepChange(normalizedStep + 1, {
435
+ apiKeyId: keyId,
436
+ apiKey: undefined,
437
+ });
438
+ }
439
+ } })))),
370
440
  React.createElement(Box, { marginTop: 1 },
371
- React.createElement(Text, { dimColor: true }, "Esc to cancel"))));
441
+ React.createElement(Text, { dimColor: true }, enteringNewKey
442
+ ? 'Esc to go back · Ctrl+C to cancel'
443
+ : 'Esc to cancel'))));
372
444
  }
373
445
  // Step 3: Confirmation
374
446
  if (effectiveStep === 3) {
375
447
  const provider = config.provider ?? 'gemini';
376
448
  const info = PROVIDER_CONFIG[provider];
377
- const hasApiKey = !!config.apiKey;
449
+ const hasApiKey = !!config.apiKeyId || !!config.apiKey;
378
450
  return (React.createElement(Box, { flexDirection: "column", borderStyle: "round", paddingX: 2, paddingY: 1 },
379
451
  React.createElement(Text, { bold: true }, "Ready to Initialize"),
380
452
  React.createElement(Box, { marginTop: 1, flexDirection: "column" },
@@ -401,11 +473,17 @@ export function InitWizard({ step, config, isReinit, existingApiKey, existingPro
401
473
  hasApiKey ? (React.createElement(Text, { color: "green" }, "Configured")) : (React.createElement(Text, { color: "red" }, "Missing")))),
402
474
  React.createElement(Text, null,
403
475
  React.createElement(Text, { dimColor: true }, "Directory:"),
404
- " .viberag/")),
476
+ " ",
477
+ dataDir)),
405
478
  React.createElement(Box, { marginTop: 1 },
406
479
  React.createElement(SelectInput, { items: CONFIRM_ITEMS, onSelect: item => {
407
480
  if (item.value === 'init') {
408
- onComplete({ provider, apiKey: config.apiKey });
481
+ onComplete({
482
+ provider,
483
+ apiKeyId: config.apiKeyId,
484
+ apiKey: config.apiKey,
485
+ openaiRegion: config.openaiRegion,
486
+ });
409
487
  }
410
488
  else {
411
489
  onCancel();