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.
- package/README.md +42 -10
- package/dist/cli/app.js +6 -6
- package/dist/cli/commands/handlers.d.ts +2 -2
- package/dist/cli/commands/handlers.js +40 -27
- package/dist/cli/commands/useCommands.js +1 -1
- package/dist/cli/components/CleanWizard.js +31 -3
- package/dist/cli/components/InitWizard.d.ts +3 -3
- package/dist/cli/components/InitWizard.js +123 -45
- package/dist/cli/components/StatusBar.js +4 -1
- package/dist/cli/store/wizard/selectors.d.ts +1 -1
- package/dist/cli/store/wizard/selectors.js +1 -1
- package/dist/cli/store/wizard/slice.d.ts +38 -22
- package/dist/cli/store/wizard/slice.js +3 -3
- package/dist/cli/utils/error-handler.d.ts +3 -3
- package/dist/cli/utils/error-handler.js +3 -3
- package/dist/client/auto-start.js +28 -18
- package/dist/common/types.d.ts +9 -1
- package/dist/daemon/index.d.ts +5 -2
- package/dist/daemon/index.js +15 -9
- package/dist/daemon/lib/config.d.ts +13 -4
- package/dist/daemon/lib/config.js +48 -5
- package/dist/daemon/lib/constants.d.ts +57 -5
- package/dist/daemon/lib/constants.js +103 -6
- package/dist/daemon/lib/gitignore.d.ts +4 -2
- package/dist/daemon/lib/gitignore.js +58 -28
- package/dist/daemon/lib/logger.d.ts +5 -5
- package/dist/daemon/lib/logger.js +6 -6
- package/dist/daemon/lib/merkle/index.d.ts +1 -1
- package/dist/daemon/lib/merkle/index.js +2 -2
- package/dist/daemon/lib/secrets.d.ts +51 -0
- package/dist/daemon/lib/secrets.js +128 -0
- package/dist/daemon/owner.js +4 -13
- package/dist/daemon/server.js +3 -3
- package/dist/daemon/services/v2/indexing.d.ts +1 -0
- package/dist/daemon/services/v2/indexing.js +19 -3
- package/dist/daemon/services/v2/search/engine.d.ts +1 -0
- package/dist/daemon/services/v2/search/engine.js +19 -3
- package/dist/daemon/services/watcher.d.ts +1 -1
- package/dist/daemon/services/watcher.js +12 -7
- package/dist/test-setup.d.ts +1 -0
- package/dist/test-setup.js +9 -0
- 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
|
|
547
|
+
VibeRAG writes per-service logs with hourly rotation:
|
|
516
548
|
|
|
517
|
-
-
|
|
518
|
-
-
|
|
519
|
-
-
|
|
520
|
-
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
130
|
+
existingApiKeyId: existingApiKeyId ?? undefined,
|
|
131
131
|
existingProvider: existingProvider ?? undefined,
|
|
132
132
|
}));
|
|
133
|
-
}, [dispatch,
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
68
|
-
onProgress?.('Creating
|
|
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}
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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 [
|
|
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
|
|
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 =
|
|
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
|
-
|
|
319
|
+
setEnteringNewKey(false);
|
|
320
|
+
setAvailableKeys(null);
|
|
321
|
+
setKeysError(null);
|
|
289
322
|
setShowRegionSelect(false);
|
|
290
323
|
// Use relative increment: step + 1
|
|
291
|
-
onStepChange(normalizedStep + 1, {
|
|
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
|
-
"
|
|
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
|
-
|
|
333
|
-
React.createElement(Text, { color: "
|
|
334
|
-
"
|
|
335
|
-
|
|
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, {
|
|
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, {
|
|
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 },
|
|
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
|
-
"
|
|
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({
|
|
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();
|