viberag 0.1.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 (151) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +219 -0
  3. package/dist/cli/__tests__/mcp-setup.test.d.ts +6 -0
  4. package/dist/cli/__tests__/mcp-setup.test.js +597 -0
  5. package/dist/cli/app.d.ts +2 -0
  6. package/dist/cli/app.js +238 -0
  7. package/dist/cli/commands/handlers.d.ts +57 -0
  8. package/dist/cli/commands/handlers.js +231 -0
  9. package/dist/cli/commands/index.d.ts +2 -0
  10. package/dist/cli/commands/index.js +2 -0
  11. package/dist/cli/commands/mcp-setup.d.ts +107 -0
  12. package/dist/cli/commands/mcp-setup.js +509 -0
  13. package/dist/cli/commands/useRagCommands.d.ts +23 -0
  14. package/dist/cli/commands/useRagCommands.js +180 -0
  15. package/dist/cli/components/CleanWizard.d.ts +17 -0
  16. package/dist/cli/components/CleanWizard.js +169 -0
  17. package/dist/cli/components/InitWizard.d.ts +20 -0
  18. package/dist/cli/components/InitWizard.js +370 -0
  19. package/dist/cli/components/McpSetupWizard.d.ts +37 -0
  20. package/dist/cli/components/McpSetupWizard.js +387 -0
  21. package/dist/cli/components/SearchResultsDisplay.d.ts +13 -0
  22. package/dist/cli/components/SearchResultsDisplay.js +130 -0
  23. package/dist/cli/components/WelcomeBanner.d.ts +10 -0
  24. package/dist/cli/components/WelcomeBanner.js +26 -0
  25. package/dist/cli/components/index.d.ts +1 -0
  26. package/dist/cli/components/index.js +1 -0
  27. package/dist/cli/data/mcp-editors.d.ts +80 -0
  28. package/dist/cli/data/mcp-editors.js +270 -0
  29. package/dist/cli/index.d.ts +2 -0
  30. package/dist/cli/index.js +26 -0
  31. package/dist/cli-bundle.cjs +5269 -0
  32. package/dist/common/commands/terminalSetup.d.ts +2 -0
  33. package/dist/common/commands/terminalSetup.js +144 -0
  34. package/dist/common/components/CommandSuggestions.d.ts +9 -0
  35. package/dist/common/components/CommandSuggestions.js +20 -0
  36. package/dist/common/components/StaticWithResize.d.ts +23 -0
  37. package/dist/common/components/StaticWithResize.js +62 -0
  38. package/dist/common/components/StatusBar.d.ts +8 -0
  39. package/dist/common/components/StatusBar.js +64 -0
  40. package/dist/common/components/TextInput.d.ts +12 -0
  41. package/dist/common/components/TextInput.js +239 -0
  42. package/dist/common/components/index.d.ts +3 -0
  43. package/dist/common/components/index.js +3 -0
  44. package/dist/common/hooks/index.d.ts +4 -0
  45. package/dist/common/hooks/index.js +4 -0
  46. package/dist/common/hooks/useCommandHistory.d.ts +7 -0
  47. package/dist/common/hooks/useCommandHistory.js +51 -0
  48. package/dist/common/hooks/useCtrlC.d.ts +9 -0
  49. package/dist/common/hooks/useCtrlC.js +40 -0
  50. package/dist/common/hooks/useKittyKeyboard.d.ts +10 -0
  51. package/dist/common/hooks/useKittyKeyboard.js +26 -0
  52. package/dist/common/hooks/useStaticOutputBuffer.d.ts +31 -0
  53. package/dist/common/hooks/useStaticOutputBuffer.js +58 -0
  54. package/dist/common/hooks/useTerminalResize.d.ts +28 -0
  55. package/dist/common/hooks/useTerminalResize.js +51 -0
  56. package/dist/common/hooks/useTextBuffer.d.ts +13 -0
  57. package/dist/common/hooks/useTextBuffer.js +165 -0
  58. package/dist/common/index.d.ts +13 -0
  59. package/dist/common/index.js +17 -0
  60. package/dist/common/types.d.ts +162 -0
  61. package/dist/common/types.js +1 -0
  62. package/dist/mcp/index.d.ts +12 -0
  63. package/dist/mcp/index.js +66 -0
  64. package/dist/mcp/server.d.ts +25 -0
  65. package/dist/mcp/server.js +837 -0
  66. package/dist/mcp/watcher.d.ts +86 -0
  67. package/dist/mcp/watcher.js +334 -0
  68. package/dist/rag/__tests__/grammar-smoke.test.d.ts +9 -0
  69. package/dist/rag/__tests__/grammar-smoke.test.js +161 -0
  70. package/dist/rag/__tests__/helpers.d.ts +30 -0
  71. package/dist/rag/__tests__/helpers.js +67 -0
  72. package/dist/rag/__tests__/merkle.test.d.ts +5 -0
  73. package/dist/rag/__tests__/merkle.test.js +161 -0
  74. package/dist/rag/__tests__/metadata-extraction.test.d.ts +10 -0
  75. package/dist/rag/__tests__/metadata-extraction.test.js +202 -0
  76. package/dist/rag/__tests__/multi-language.test.d.ts +13 -0
  77. package/dist/rag/__tests__/multi-language.test.js +535 -0
  78. package/dist/rag/__tests__/rag.test.d.ts +10 -0
  79. package/dist/rag/__tests__/rag.test.js +311 -0
  80. package/dist/rag/__tests__/search-exhaustive.test.d.ts +9 -0
  81. package/dist/rag/__tests__/search-exhaustive.test.js +87 -0
  82. package/dist/rag/__tests__/search-filters.test.d.ts +10 -0
  83. package/dist/rag/__tests__/search-filters.test.js +250 -0
  84. package/dist/rag/__tests__/search-modes.test.d.ts +8 -0
  85. package/dist/rag/__tests__/search-modes.test.js +133 -0
  86. package/dist/rag/config/index.d.ts +61 -0
  87. package/dist/rag/config/index.js +111 -0
  88. package/dist/rag/constants.d.ts +41 -0
  89. package/dist/rag/constants.js +57 -0
  90. package/dist/rag/embeddings/fastembed.d.ts +62 -0
  91. package/dist/rag/embeddings/fastembed.js +124 -0
  92. package/dist/rag/embeddings/gemini.d.ts +26 -0
  93. package/dist/rag/embeddings/gemini.js +116 -0
  94. package/dist/rag/embeddings/index.d.ts +10 -0
  95. package/dist/rag/embeddings/index.js +9 -0
  96. package/dist/rag/embeddings/local-4b.d.ts +28 -0
  97. package/dist/rag/embeddings/local-4b.js +51 -0
  98. package/dist/rag/embeddings/local.d.ts +29 -0
  99. package/dist/rag/embeddings/local.js +119 -0
  100. package/dist/rag/embeddings/mistral.d.ts +22 -0
  101. package/dist/rag/embeddings/mistral.js +85 -0
  102. package/dist/rag/embeddings/openai.d.ts +22 -0
  103. package/dist/rag/embeddings/openai.js +85 -0
  104. package/dist/rag/embeddings/types.d.ts +37 -0
  105. package/dist/rag/embeddings/types.js +1 -0
  106. package/dist/rag/gitignore/index.d.ts +57 -0
  107. package/dist/rag/gitignore/index.js +178 -0
  108. package/dist/rag/index.d.ts +15 -0
  109. package/dist/rag/index.js +25 -0
  110. package/dist/rag/indexer/chunker.d.ts +129 -0
  111. package/dist/rag/indexer/chunker.js +1352 -0
  112. package/dist/rag/indexer/index.d.ts +6 -0
  113. package/dist/rag/indexer/index.js +6 -0
  114. package/dist/rag/indexer/indexer.d.ts +73 -0
  115. package/dist/rag/indexer/indexer.js +356 -0
  116. package/dist/rag/indexer/types.d.ts +68 -0
  117. package/dist/rag/indexer/types.js +47 -0
  118. package/dist/rag/logger/index.d.ts +20 -0
  119. package/dist/rag/logger/index.js +75 -0
  120. package/dist/rag/manifest/index.d.ts +50 -0
  121. package/dist/rag/manifest/index.js +97 -0
  122. package/dist/rag/merkle/diff.d.ts +26 -0
  123. package/dist/rag/merkle/diff.js +95 -0
  124. package/dist/rag/merkle/hash.d.ts +34 -0
  125. package/dist/rag/merkle/hash.js +165 -0
  126. package/dist/rag/merkle/index.d.ts +68 -0
  127. package/dist/rag/merkle/index.js +298 -0
  128. package/dist/rag/merkle/node.d.ts +51 -0
  129. package/dist/rag/merkle/node.js +69 -0
  130. package/dist/rag/search/filters.d.ts +21 -0
  131. package/dist/rag/search/filters.js +100 -0
  132. package/dist/rag/search/fts.d.ts +32 -0
  133. package/dist/rag/search/fts.js +61 -0
  134. package/dist/rag/search/hybrid.d.ts +17 -0
  135. package/dist/rag/search/hybrid.js +58 -0
  136. package/dist/rag/search/index.d.ts +89 -0
  137. package/dist/rag/search/index.js +367 -0
  138. package/dist/rag/search/types.d.ts +130 -0
  139. package/dist/rag/search/types.js +4 -0
  140. package/dist/rag/search/vector.d.ts +25 -0
  141. package/dist/rag/search/vector.js +44 -0
  142. package/dist/rag/storage/index.d.ts +92 -0
  143. package/dist/rag/storage/index.js +287 -0
  144. package/dist/rag/storage/lancedb-native.d.ts +7 -0
  145. package/dist/rag/storage/lancedb-native.js +10 -0
  146. package/dist/rag/storage/schema.d.ts +23 -0
  147. package/dist/rag/storage/schema.js +50 -0
  148. package/dist/rag/storage/types.d.ts +100 -0
  149. package/dist/rag/storage/types.js +68 -0
  150. package/package.json +67 -0
  151. package/scripts/check-node-version.js +37 -0
@@ -0,0 +1,509 @@
1
+ /**
2
+ * MCP Setup Logic
3
+ *
4
+ * Functions for generating, writing, and merging MCP configurations
5
+ * for various AI coding tools.
6
+ */
7
+ import fs from 'node:fs/promises';
8
+ import path from 'node:path';
9
+ import { EDITORS, getConfigPath, getZedSettingsPath, getWindsurfConfigPath, getOpenCodeConfigPath, } from '../data/mcp-editors.js';
10
+ /**
11
+ * Generate the viberag MCP server configuration object.
12
+ */
13
+ export function generateViberagConfig() {
14
+ return {
15
+ command: 'npx',
16
+ args: ['viberag-mcp'],
17
+ };
18
+ }
19
+ /**
20
+ * Generate OpenCode-specific viberag MCP server configuration.
21
+ * OpenCode requires: type="local", command as array, no args key.
22
+ */
23
+ export function generateOpenCodeViberagConfig() {
24
+ return {
25
+ type: 'local',
26
+ command: ['npx', '-y', 'viberag-mcp'],
27
+ };
28
+ }
29
+ /**
30
+ * Generate complete MCP config for an editor.
31
+ */
32
+ export function generateMcpConfig(editor) {
33
+ const viberagConfig = editor.id === 'opencode'
34
+ ? generateOpenCodeViberagConfig()
35
+ : generateViberagConfig();
36
+ // Use the editor's specific key
37
+ return {
38
+ [editor.jsonKey]: {
39
+ viberag: viberagConfig,
40
+ },
41
+ };
42
+ }
43
+ /**
44
+ * Generate TOML config for OpenAI Codex.
45
+ */
46
+ export function generateTomlConfig() {
47
+ return `[mcp_servers.viberag]
48
+ command = "npx"
49
+ args = ["viberag-mcp"]
50
+ `;
51
+ }
52
+ /**
53
+ * Check if a config file exists.
54
+ */
55
+ export async function configExists(configPath) {
56
+ try {
57
+ await fs.access(configPath);
58
+ return true;
59
+ }
60
+ catch {
61
+ return false;
62
+ }
63
+ }
64
+ /**
65
+ * Read existing config file as JSON.
66
+ */
67
+ export async function readJsonConfig(configPath) {
68
+ try {
69
+ const content = await fs.readFile(configPath, 'utf-8');
70
+ return JSON.parse(content);
71
+ }
72
+ catch {
73
+ return null;
74
+ }
75
+ }
76
+ /**
77
+ * Merge viberag config into existing config.
78
+ */
79
+ export function mergeConfig(existing, editor) {
80
+ const viberagConfig = editor.id === 'opencode'
81
+ ? generateOpenCodeViberagConfig()
82
+ : generateViberagConfig();
83
+ const jsonKey = editor.jsonKey;
84
+ // Get or create the servers object
85
+ const existingServers = existing[jsonKey] ?? {};
86
+ return {
87
+ ...existing,
88
+ [jsonKey]: {
89
+ ...existingServers,
90
+ viberag: viberagConfig,
91
+ },
92
+ };
93
+ }
94
+ /**
95
+ * Check if viberag is already configured in a config file.
96
+ */
97
+ export function hasViberagConfig(config, editor) {
98
+ const servers = config[editor.jsonKey];
99
+ if (!servers || typeof servers !== 'object') {
100
+ return false;
101
+ }
102
+ return 'viberag' in servers;
103
+ }
104
+ /**
105
+ * Write MCP config to file, creating directories as needed.
106
+ */
107
+ export async function writeMcpConfig(editor, projectRoot) {
108
+ try {
109
+ // Get the config path
110
+ let configPath;
111
+ if (editor.id === 'zed') {
112
+ configPath = getZedSettingsPath();
113
+ }
114
+ else if (editor.id === 'windsurf') {
115
+ configPath = getWindsurfConfigPath();
116
+ }
117
+ else if (editor.id === 'opencode') {
118
+ configPath = getOpenCodeConfigPath();
119
+ }
120
+ else {
121
+ const path = getConfigPath(editor, projectRoot);
122
+ if (!path) {
123
+ return {
124
+ success: false,
125
+ editor: editor.id,
126
+ method: 'instructions-shown',
127
+ error: 'No config path for this editor',
128
+ };
129
+ }
130
+ configPath = path;
131
+ }
132
+ // Ensure parent directory exists
133
+ const dir = path.dirname(configPath);
134
+ await fs.mkdir(dir, { recursive: true });
135
+ // Check if file exists
136
+ const exists = await configExists(configPath);
137
+ if (exists) {
138
+ // Merge with existing config
139
+ const existing = await readJsonConfig(configPath);
140
+ if (!existing) {
141
+ return {
142
+ success: false,
143
+ editor: editor.id,
144
+ method: 'instructions-shown',
145
+ error: 'Could not parse existing config file',
146
+ };
147
+ }
148
+ // Check if already configured
149
+ if (hasViberagConfig(existing, editor)) {
150
+ return {
151
+ success: true,
152
+ editor: editor.id,
153
+ method: 'file-merged',
154
+ configPath,
155
+ error: 'Already configured',
156
+ };
157
+ }
158
+ const merged = mergeConfig(existing, editor);
159
+ await fs.writeFile(configPath, JSON.stringify(merged, null, 2) + '\n');
160
+ return {
161
+ success: true,
162
+ editor: editor.id,
163
+ method: 'file-merged',
164
+ configPath,
165
+ };
166
+ }
167
+ else {
168
+ // Create new config
169
+ const config = generateMcpConfig(editor);
170
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2) + '\n');
171
+ return {
172
+ success: true,
173
+ editor: editor.id,
174
+ method: 'file-created',
175
+ configPath,
176
+ };
177
+ }
178
+ }
179
+ catch (error) {
180
+ return {
181
+ success: false,
182
+ editor: editor.id,
183
+ method: 'instructions-shown',
184
+ error: error instanceof Error ? error.message : String(error),
185
+ };
186
+ }
187
+ }
188
+ /**
189
+ * Generate manual setup instructions for an editor.
190
+ */
191
+ export function getManualInstructions(editor, projectRoot) {
192
+ const lines = [];
193
+ lines.push(`## ${editor.name} MCP Setup`);
194
+ lines.push('');
195
+ if (editor.cliCommand) {
196
+ lines.push('Run this command:');
197
+ lines.push('');
198
+ lines.push(` ${editor.cliCommand}`);
199
+ lines.push('');
200
+ }
201
+ else if (editor.configFormat === 'json') {
202
+ let configPath;
203
+ if (editor.id === 'opencode') {
204
+ configPath = getOpenCodeConfigPath();
205
+ }
206
+ else if (editor.scope === 'project') {
207
+ configPath = editor.configPath;
208
+ }
209
+ else {
210
+ const resolvedPath = getConfigPath(editor, projectRoot);
211
+ configPath = resolvedPath ?? editor.configPath;
212
+ }
213
+ lines.push(`Add to ${configPath}:`);
214
+ lines.push('');
215
+ const config = generateMcpConfig(editor);
216
+ const jsonStr = JSON.stringify(config, null, 2);
217
+ lines.push(jsonStr
218
+ .split('\n')
219
+ .map(l => ' ' + l)
220
+ .join('\n'));
221
+ lines.push('');
222
+ }
223
+ else if (editor.configFormat === 'toml') {
224
+ lines.push(`Add to ${editor.configPath}:`);
225
+ lines.push('');
226
+ lines.push(generateTomlConfig()
227
+ .split('\n')
228
+ .map(l => ' ' + l)
229
+ .join('\n'));
230
+ lines.push('');
231
+ }
232
+ else if (editor.configFormat === 'ui') {
233
+ lines.push('Manual setup required:');
234
+ lines.push('');
235
+ lines.push('1. Open Settings → Tools → AI Assistant → MCP');
236
+ lines.push('2. Click "Add Server"');
237
+ lines.push('3. Set name: viberag');
238
+ lines.push('4. Set command: npx');
239
+ lines.push('5. Set args: viberag-mcp');
240
+ lines.push('');
241
+ }
242
+ lines.push('After setup:');
243
+ lines.push(` ${editor.restartInstructions}`);
244
+ lines.push('');
245
+ lines.push('Verify:');
246
+ for (const step of editor.verificationSteps) {
247
+ lines.push(` - ${step}`);
248
+ }
249
+ lines.push('');
250
+ lines.push(`Documentation: ${editor.docsUrl}`);
251
+ return lines.join('\n');
252
+ }
253
+ /**
254
+ * Get a diff preview of the merge operation.
255
+ */
256
+ export async function getMergeDiff(editor, projectRoot) {
257
+ try {
258
+ let configPath;
259
+ if (editor.id === 'zed') {
260
+ configPath = getZedSettingsPath();
261
+ }
262
+ else if (editor.id === 'windsurf') {
263
+ configPath = getWindsurfConfigPath();
264
+ }
265
+ else if (editor.id === 'opencode') {
266
+ configPath = getOpenCodeConfigPath();
267
+ }
268
+ else {
269
+ const path = getConfigPath(editor, projectRoot);
270
+ if (!path)
271
+ return null;
272
+ configPath = path;
273
+ }
274
+ const exists = await configExists(configPath);
275
+ if (!exists) {
276
+ return {
277
+ before: '(file does not exist)',
278
+ after: JSON.stringify(generateMcpConfig(editor), null, 2),
279
+ configPath,
280
+ };
281
+ }
282
+ const existing = await readJsonConfig(configPath);
283
+ if (!existing)
284
+ return null;
285
+ const merged = mergeConfig(existing, editor);
286
+ return {
287
+ before: JSON.stringify(existing, null, 2),
288
+ after: JSON.stringify(merged, null, 2),
289
+ configPath,
290
+ };
291
+ }
292
+ catch {
293
+ return null;
294
+ }
295
+ }
296
+ /**
297
+ * Check if viberag is already configured for an editor.
298
+ */
299
+ export async function isAlreadyConfigured(editor, projectRoot) {
300
+ try {
301
+ let configPath;
302
+ if (editor.id === 'zed') {
303
+ configPath = getZedSettingsPath();
304
+ }
305
+ else if (editor.id === 'windsurf') {
306
+ configPath = getWindsurfConfigPath();
307
+ }
308
+ else if (editor.id === 'opencode') {
309
+ configPath = getOpenCodeConfigPath();
310
+ }
311
+ else {
312
+ const path = getConfigPath(editor, projectRoot);
313
+ if (!path)
314
+ return false;
315
+ configPath = path;
316
+ }
317
+ const exists = await configExists(configPath);
318
+ if (!exists)
319
+ return false;
320
+ const config = await readJsonConfig(configPath);
321
+ if (!config)
322
+ return false;
323
+ return hasViberagConfig(config, editor);
324
+ }
325
+ catch {
326
+ return false;
327
+ }
328
+ }
329
+ /**
330
+ * Remove viberag from an existing config object.
331
+ * Returns the modified config, or null if nothing to remove.
332
+ */
333
+ export function removeViberagFromConfig(existing, editor) {
334
+ const jsonKey = editor.jsonKey;
335
+ const servers = existing[jsonKey];
336
+ if (!servers || typeof servers !== 'object') {
337
+ return null;
338
+ }
339
+ if (!('viberag' in servers)) {
340
+ return null;
341
+ }
342
+ // Remove viberag from servers
343
+ const remainingServers = Object.fromEntries(Object.entries(servers).filter(([key]) => key !== 'viberag'));
344
+ return {
345
+ ...existing,
346
+ [jsonKey]: remainingServers,
347
+ };
348
+ }
349
+ /**
350
+ * Remove viberag from an editor's MCP config.
351
+ * Always keeps the config file, even if it becomes empty (no other servers).
352
+ */
353
+ export async function removeViberagConfig(editor, projectRoot) {
354
+ try {
355
+ // Get the config path
356
+ let configPath;
357
+ if (editor.id === 'zed') {
358
+ configPath = getZedSettingsPath();
359
+ }
360
+ else if (editor.id === 'windsurf') {
361
+ configPath = getWindsurfConfigPath();
362
+ }
363
+ else if (editor.id === 'opencode') {
364
+ configPath = getOpenCodeConfigPath();
365
+ }
366
+ else {
367
+ const p = getConfigPath(editor, projectRoot);
368
+ if (!p) {
369
+ return {
370
+ success: false,
371
+ editor: editor.id,
372
+ error: 'No config path for this editor',
373
+ };
374
+ }
375
+ configPath = p;
376
+ }
377
+ // Check if file exists
378
+ const exists = await configExists(configPath);
379
+ if (!exists) {
380
+ return {
381
+ success: false,
382
+ editor: editor.id,
383
+ error: 'Config file does not exist',
384
+ };
385
+ }
386
+ // Read existing config
387
+ const existing = await readJsonConfig(configPath);
388
+ if (!existing) {
389
+ return {
390
+ success: false,
391
+ editor: editor.id,
392
+ configPath,
393
+ error: 'Could not parse config file',
394
+ };
395
+ }
396
+ // Check if viberag is configured
397
+ if (!hasViberagConfig(existing, editor)) {
398
+ return {
399
+ success: false,
400
+ editor: editor.id,
401
+ configPath,
402
+ error: 'Viberag not configured in this file',
403
+ };
404
+ }
405
+ // Remove viberag
406
+ const modified = removeViberagFromConfig(existing, editor);
407
+ if (!modified) {
408
+ return {
409
+ success: false,
410
+ editor: editor.id,
411
+ configPath,
412
+ error: 'Failed to remove viberag from config',
413
+ };
414
+ }
415
+ // Write modified config back (keep file even if servers is empty)
416
+ await fs.writeFile(configPath, JSON.stringify(modified, null, 2) + '\n');
417
+ return {
418
+ success: true,
419
+ editor: editor.id,
420
+ configPath,
421
+ };
422
+ }
423
+ catch (error) {
424
+ return {
425
+ success: false,
426
+ editor: editor.id,
427
+ error: error instanceof Error ? error.message : String(error),
428
+ };
429
+ }
430
+ }
431
+ /**
432
+ * Find all editors that have viberag configured.
433
+ * Returns both project-scope and global-scope editors.
434
+ */
435
+ export async function findConfiguredEditors(projectRoot) {
436
+ const projectScope = [];
437
+ const globalScope = [];
438
+ for (const editor of EDITORS) {
439
+ const isConfigured = await isAlreadyConfigured(editor, projectRoot);
440
+ if (isConfigured) {
441
+ if (editor.scope === 'project') {
442
+ projectScope.push(editor);
443
+ }
444
+ else if (editor.scope === 'global') {
445
+ globalScope.push(editor);
446
+ }
447
+ }
448
+ }
449
+ return { projectScope, globalScope };
450
+ }
451
+ /**
452
+ * Add an entry to .gitignore if not already present.
453
+ */
454
+ export async function addToGitignore(projectRoot, entry) {
455
+ const gitignorePath = path.join(projectRoot, '.gitignore');
456
+ try {
457
+ let content = '';
458
+ try {
459
+ content = await fs.readFile(gitignorePath, 'utf-8');
460
+ }
461
+ catch {
462
+ // .gitignore doesn't exist, will create
463
+ }
464
+ // Check if entry already exists (exact line match)
465
+ const lines = content.split('\n');
466
+ if (lines.some(line => line.trim() === entry)) {
467
+ return true; // Already present
468
+ }
469
+ // Add entry with a comment
470
+ const addition = content.endsWith('\n') || content === ''
471
+ ? `# MCP config (local, not committed)\n${entry}\n`
472
+ : `\n# MCP config (local, not committed)\n${entry}\n`;
473
+ await fs.appendFile(gitignorePath, addition);
474
+ return true;
475
+ }
476
+ catch {
477
+ return false;
478
+ }
479
+ }
480
+ /**
481
+ * Get all project-scope config paths that were created.
482
+ * Used for gitignore prompt.
483
+ */
484
+ export function getProjectConfigPaths(results) {
485
+ const paths = [];
486
+ for (const result of results) {
487
+ if (result.success &&
488
+ (result.method === 'file-created' || result.method === 'file-merged') &&
489
+ result.configPath) {
490
+ // Check if it's a project-scope path (doesn't start with ~ or /)
491
+ const configPath = result.configPath;
492
+ if (!configPath.startsWith('/') && !configPath.startsWith('~')) {
493
+ // It's a relative path, so project-scope
494
+ paths.push(configPath);
495
+ }
496
+ else if (configPath.includes('.mcp.json') ||
497
+ configPath.includes('.cursor/') ||
498
+ configPath.includes('.vscode/') ||
499
+ configPath.includes('.roo/')) {
500
+ // Extract relative path from absolute path
501
+ const projectDir = process.cwd();
502
+ if (configPath.startsWith(projectDir)) {
503
+ paths.push(configPath.slice(projectDir.length + 1));
504
+ }
505
+ }
506
+ }
507
+ }
508
+ return paths;
509
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * CLI command handling hook.
3
+ * Consolidates all command routing and handler implementations.
4
+ */
5
+ import { type IndexDisplayStats } from './handlers.js';
6
+ import type { AppStatus, SearchResultsData } from '../../common/types.js';
7
+ type RagCommandContext = {
8
+ addOutput: (type: 'user' | 'system', content: string) => void;
9
+ addSearchResults: (data: SearchResultsData) => void;
10
+ setAppStatus: (status: AppStatus) => void;
11
+ setIndexStats: (stats: IndexDisplayStats | null) => void;
12
+ projectRoot: string;
13
+ stdout: NodeJS.WriteStream;
14
+ startInitWizard: (isReinit: boolean) => void;
15
+ startMcpSetupWizard: (showPrompt?: boolean) => void;
16
+ startCleanWizard: () => void;
17
+ isInitialized: boolean;
18
+ };
19
+ export declare function useRagCommands({ addOutput, addSearchResults, setAppStatus, setIndexStats, projectRoot, stdout, startInitWizard, startMcpSetupWizard, startCleanWizard, isInitialized, }: RagCommandContext): {
20
+ isCommand: (text: string) => boolean;
21
+ executeCommand: (text: string) => void;
22
+ };
23
+ export {};
@@ -0,0 +1,180 @@
1
+ /**
2
+ * CLI command handling hook.
3
+ * Consolidates all command routing and handler implementations.
4
+ */
5
+ import { useCallback } from 'react';
6
+ import { useApp } from 'ink';
7
+ import { runIndex, formatIndexStats, runSearch, getStatus, loadIndexStats, } from './handlers.js';
8
+ import { setupVSCodeTerminal } from '../../common/commands/terminalSetup.js';
9
+ export function useRagCommands({ addOutput, addSearchResults, setAppStatus, setIndexStats, projectRoot, stdout, startInitWizard, startMcpSetupWizard, startCleanWizard, isInitialized, }) {
10
+ const { exit } = useApp();
11
+ // Command handlers
12
+ const handleHelp = useCallback(() => {
13
+ addOutput('system', `Commands:
14
+ /help - Show this help
15
+ /clear - Clear the screen
16
+ /terminal-setup - Configure terminal for Shift+Enter
17
+ /init - Initialize Viberag (interactive wizard)
18
+ /index - Index the codebase
19
+ /reindex - Force full reindex
20
+ /search <query> - Search the codebase
21
+ /status - Show index status
22
+ /mcp-setup - Configure MCP server for AI coding tools
23
+ /clean - Remove Viberag from project (delete .viberag/)
24
+ /quit - Exit
25
+
26
+ Multi-line input:
27
+ Shift+Enter - iTerm2, Kitty, WezTerm (automatic)
28
+ Shift+Enter - VS Code (requires /terminal-setup)
29
+ Option+Enter - Most terminals
30
+ Ctrl+J - All terminals
31
+ \\ then Enter - All terminals
32
+
33
+ Tips:
34
+ Ctrl+C - Clear input (twice to quit)
35
+ Escape - Clear input
36
+ Up/Down - Command history`);
37
+ }, [addOutput]);
38
+ const handleClear = useCallback(() => {
39
+ // Clear screen (\x1B[2J), clear scrollback buffer (\x1B[3J), move cursor home (\x1B[H)
40
+ stdout.write('\x1B[2J\x1B[3J\x1B[H');
41
+ }, [stdout]);
42
+ const handleTerminalSetup = useCallback(() => {
43
+ setupVSCodeTerminal()
44
+ .then(result => addOutput('system', result))
45
+ .catch(err => addOutput('system', `Error: ${err.message}`));
46
+ }, [addOutput]);
47
+ // Trigger init wizard
48
+ const handleInit = useCallback(() => {
49
+ startInitWizard(isInitialized);
50
+ }, [startInitWizard, isInitialized]);
51
+ const handleIndex = useCallback((force) => {
52
+ const action = force ? 'Reindexing' : 'Indexing';
53
+ addOutput('system', `${action} codebase...`);
54
+ setAppStatus({ state: 'indexing', current: 0, total: 0, stage: action });
55
+ runIndex(projectRoot, force, (current, total, stage) => setAppStatus({ state: 'indexing', current, total, stage }))
56
+ .then(async (stats) => {
57
+ addOutput('system', formatIndexStats(stats));
58
+ // Reload stats after indexing
59
+ const newStats = await loadIndexStats(projectRoot);
60
+ setIndexStats(newStats);
61
+ setAppStatus({ state: 'ready' });
62
+ })
63
+ .catch(err => {
64
+ addOutput('system', `Index failed: ${err.message}`);
65
+ setAppStatus({ state: 'ready' });
66
+ });
67
+ }, [projectRoot, addOutput, setAppStatus, setIndexStats]);
68
+ const handleSearch = useCallback((query) => {
69
+ addOutput('system', `Searching for "${query}"...`);
70
+ setAppStatus({ state: 'searching' });
71
+ runSearch(projectRoot, query)
72
+ .then(results => {
73
+ // Use the component-based display with syntax highlighting
74
+ addSearchResults({
75
+ query: results.query,
76
+ elapsedMs: results.elapsedMs,
77
+ results: results.results.map(r => ({
78
+ type: r.type,
79
+ name: r.name,
80
+ filepath: r.filepath,
81
+ filename: r.filename,
82
+ startLine: r.startLine,
83
+ endLine: r.endLine,
84
+ score: r.score,
85
+ text: r.text,
86
+ })),
87
+ });
88
+ setAppStatus({ state: 'ready' });
89
+ })
90
+ .catch(err => {
91
+ addOutput('system', `Search failed: ${err.message}`);
92
+ setAppStatus({ state: 'ready' });
93
+ });
94
+ }, [projectRoot, addOutput, addSearchResults, setAppStatus]);
95
+ const handleStatus = useCallback(() => {
96
+ getStatus(projectRoot)
97
+ .then(status => addOutput('system', status))
98
+ .catch(err => addOutput('system', `Status failed: ${err.message}`));
99
+ }, [projectRoot, addOutput]);
100
+ const handleMcpSetup = useCallback(() => {
101
+ startMcpSetupWizard(false); // showPrompt = false for direct /mcp-setup command
102
+ }, [startMcpSetupWizard]);
103
+ const handleClean = useCallback(() => {
104
+ startCleanWizard();
105
+ }, [startCleanWizard]);
106
+ const handleUnknown = useCallback((command) => {
107
+ addOutput('system', `Unknown command: ${command}. Type /help for available commands.`);
108
+ }, [addOutput]);
109
+ // Command detection
110
+ const isCommand = useCallback((text) => {
111
+ return text.trim().startsWith('/');
112
+ }, []);
113
+ // Command routing
114
+ const executeCommand = useCallback((text) => {
115
+ const trimmed = text.trim();
116
+ const command = trimmed.toLowerCase();
117
+ // Handle commands with arguments
118
+ if (command.startsWith('/search ')) {
119
+ const query = trimmed.slice('/search '.length).trim();
120
+ if (query) {
121
+ handleSearch(query);
122
+ }
123
+ else {
124
+ handleUnknown('/search (missing query)');
125
+ }
126
+ return;
127
+ }
128
+ switch (command) {
129
+ case '/help':
130
+ handleHelp();
131
+ break;
132
+ case '/clear':
133
+ handleClear();
134
+ break;
135
+ case '/terminal-setup':
136
+ handleTerminalSetup();
137
+ break;
138
+ case '/init':
139
+ handleInit();
140
+ break;
141
+ case '/index':
142
+ handleIndex(false);
143
+ break;
144
+ case '/reindex':
145
+ handleIndex(true);
146
+ break;
147
+ case '/status':
148
+ handleStatus();
149
+ break;
150
+ case '/mcp-setup':
151
+ handleMcpSetup();
152
+ break;
153
+ case '/clean':
154
+ case '/uninstall':
155
+ handleClean();
156
+ break;
157
+ case '/quit':
158
+ case '/exit':
159
+ case '/q':
160
+ exit();
161
+ break;
162
+ default:
163
+ handleUnknown(command);
164
+ break;
165
+ }
166
+ }, [
167
+ exit,
168
+ handleHelp,
169
+ handleClear,
170
+ handleTerminalSetup,
171
+ handleInit,
172
+ handleIndex,
173
+ handleSearch,
174
+ handleStatus,
175
+ handleMcpSetup,
176
+ handleClean,
177
+ handleUnknown,
178
+ ]);
179
+ return { isCommand, executeCommand };
180
+ }