@code-rag/cli 0.1.6 → 0.1.8

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.
@@ -0,0 +1,14 @@
1
+ /**
2
+ * CLI command: coderag benchmark
3
+ *
4
+ * Auto-generates a benchmark dataset from an indexed project and evaluates
5
+ * CodeRAG search quality against it. Prints a summary table and optionally
6
+ * saves a detailed JSON report.
7
+ */
8
+ import { Command } from 'commander';
9
+ import type { BenchmarkReport } from '@code-rag/core';
10
+ /**
11
+ * Format a BenchmarkReport as a colored terminal summary.
12
+ */
13
+ export declare function formatColoredSummary(report: BenchmarkReport): string;
14
+ export declare function registerBenchmarkCommand(program: Command): void;
@@ -0,0 +1,198 @@
1
+ /**
2
+ * CLI command: coderag benchmark
3
+ *
4
+ * Auto-generates a benchmark dataset from an indexed project and evaluates
5
+ * CodeRAG search quality against it. Prints a summary table and optionally
6
+ * saves a detailed JSON report.
7
+ */
8
+ import chalk from 'chalk';
9
+ import { writeFile, readFile } from 'node:fs/promises';
10
+ import { resolve, join } from 'node:path';
11
+ import { createRuntime, parseIndexRows, buildCallerMap, buildTestMap, generateQueries, runBenchmark, } from '@code-rag/core';
12
+ /**
13
+ * Format a BenchmarkReport as a colored terminal summary.
14
+ */
15
+ export function formatColoredSummary(report) {
16
+ const lines = [];
17
+ const a = report.aggregate;
18
+ lines.push(chalk.bold('Benchmark Results'));
19
+ lines.push(chalk.dim('================='));
20
+ lines.push(`Queries: ${chalk.cyan(String(report.metadata.totalQueries))}`);
21
+ lines.push(`Index size: ${chalk.cyan(String(report.metadata.totalChunksInIndex))} chunks`);
22
+ lines.push(`Duration: ${chalk.cyan((report.metadata.durationMs / 1000).toFixed(1))}s`);
23
+ lines.push('');
24
+ lines.push(chalk.bold('Aggregate Metrics:'));
25
+ lines.push(` P@5: ${colorMetric(a.precisionAt5)}`);
26
+ lines.push(` P@10: ${colorMetric(a.precisionAt10)}`);
27
+ lines.push(` Recall@10: ${colorMetric(a.recallAt10)}`);
28
+ lines.push(` MRR: ${colorMetric(a.mrr)}`);
29
+ lines.push(` nDCG@10: ${colorMetric(a.ndcgAt10)}`);
30
+ if (report.byQueryType.length > 0) {
31
+ lines.push('');
32
+ lines.push(chalk.bold('By Query Type:'));
33
+ lines.push(chalk.dim(' Type | Count | P@5 | P@10 | R@10 | MRR | nDCG@10'));
34
+ lines.push(chalk.dim(' ---------------------|-------|-------|-------|-------|-------|--------'));
35
+ for (const bt of report.byQueryType) {
36
+ const m = bt.metrics;
37
+ const type = bt.queryType.padEnd(20);
38
+ lines.push(` ${chalk.cyan(type)} | ${String(m.queryCount).padStart(5)} | ${fmt(m.precisionAt5)} | ${fmt(m.precisionAt10)} | ${fmt(m.recallAt10)} | ${fmt(m.mrr)} | ${fmt(m.ndcgAt10)}`);
39
+ }
40
+ }
41
+ return lines.join('\n');
42
+ }
43
+ function colorMetric(value) {
44
+ const formatted = value.toFixed(4).padStart(6);
45
+ if (value >= 0.7)
46
+ return chalk.green(formatted);
47
+ if (value >= 0.4)
48
+ return chalk.yellow(formatted);
49
+ return chalk.red(formatted);
50
+ }
51
+ function fmt(value) {
52
+ return value.toFixed(4).padStart(6);
53
+ }
54
+ export function registerBenchmarkCommand(program) {
55
+ program
56
+ .command('benchmark')
57
+ .description('Auto-generate benchmarks from the index and evaluate search quality')
58
+ .option('--queries <n>', 'Number of benchmark queries to generate', '100')
59
+ .option('--output <path>', 'Save detailed JSON report to this path')
60
+ .option('--top-k <n>', 'Number of search results per query', '10')
61
+ .option('--seed <n>', 'Random seed for reproducible generation', '42')
62
+ .action(async (options) => {
63
+ try {
64
+ const rootDir = process.cwd();
65
+ const queryCount = parseInt(options.queries, 10);
66
+ const topK = parseInt(options.topK, 10);
67
+ const seed = parseInt(options.seed, 10);
68
+ if (isNaN(queryCount) || queryCount < 1) {
69
+ // eslint-disable-next-line no-console
70
+ console.error(chalk.red('Invalid --queries value. Must be a positive integer.'));
71
+ process.exit(1);
72
+ }
73
+ if (isNaN(topK) || topK < 1) {
74
+ // eslint-disable-next-line no-console
75
+ console.error(chalk.red('Invalid --top-k value. Must be a positive integer.'));
76
+ process.exit(1);
77
+ }
78
+ if (isNaN(seed)) {
79
+ // eslint-disable-next-line no-console
80
+ console.error(chalk.red('Invalid --seed value. Must be an integer.'));
81
+ process.exit(1);
82
+ }
83
+ // --- Initialize runtime ---
84
+ // eslint-disable-next-line no-console
85
+ console.log(chalk.dim('Initializing runtime...'));
86
+ const runtimeResult = await createRuntime({ rootDir, searchOnly: true });
87
+ if (runtimeResult.isErr()) {
88
+ // eslint-disable-next-line no-console
89
+ console.error(chalk.red('Initialization failed.'), runtimeResult.error.message);
90
+ process.exit(1);
91
+ }
92
+ const runtime = runtimeResult.value;
93
+ // --- Scan index ---
94
+ // eslint-disable-next-line no-console
95
+ console.log(chalk.dim('Scanning index...'));
96
+ const allRowsResult = await runtime.store.getAll();
97
+ if (allRowsResult.isErr()) {
98
+ runtime.close();
99
+ // eslint-disable-next-line no-console
100
+ console.error(chalk.red('Failed to scan index:'), allRowsResult.error.message);
101
+ process.exit(1);
102
+ }
103
+ const rows = allRowsResult.value;
104
+ if (rows.length === 0) {
105
+ runtime.close();
106
+ // eslint-disable-next-line no-console
107
+ console.log(chalk.yellow('Index is empty. Run "coderag index" first.'));
108
+ return;
109
+ }
110
+ const scanResult = parseIndexRows(rows);
111
+ if (scanResult.isErr()) {
112
+ runtime.close();
113
+ // eslint-disable-next-line no-console
114
+ console.error(chalk.red('Failed to parse index:'), scanResult.error.message);
115
+ process.exit(1);
116
+ }
117
+ const scan = scanResult.value;
118
+ // eslint-disable-next-line no-console
119
+ console.log(chalk.dim(`Found ${scan.totalChunks} chunks in index.`));
120
+ // --- Load dependency graph ---
121
+ let edges = [];
122
+ const storagePath = resolve(rootDir, runtime.config.storage.path);
123
+ const graphPath = join(storagePath, 'graph.json');
124
+ try {
125
+ const graphData = await readFile(graphPath, 'utf-8');
126
+ const parsed = JSON.parse(graphData);
127
+ if (parsed !== null &&
128
+ typeof parsed === 'object' &&
129
+ 'edges' in parsed &&
130
+ Array.isArray(parsed['edges'])) {
131
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Runtime-checked graph JSON structure
132
+ edges = parsed.edges;
133
+ }
134
+ }
135
+ catch {
136
+ // No graph available, proceed with empty edges
137
+ // eslint-disable-next-line no-console
138
+ console.log(chalk.dim('No dependency graph found, skipping caller/import queries.'));
139
+ }
140
+ // --- Build relationship maps ---
141
+ const callerMap = buildCallerMap(edges);
142
+ const testMap = buildTestMap(scan.fileToChunkIds);
143
+ // --- Generate queries ---
144
+ // eslint-disable-next-line no-console
145
+ console.log(chalk.dim(`Generating ${queryCount} benchmark queries...`));
146
+ const queries = generateQueries(scan, edges, callerMap, testMap, { maxQueries: queryCount }, seed);
147
+ if (queries.length === 0) {
148
+ runtime.close();
149
+ // eslint-disable-next-line no-console
150
+ console.log(chalk.yellow('Could not generate any benchmark queries from this index.'));
151
+ return;
152
+ }
153
+ // eslint-disable-next-line no-console
154
+ console.log(chalk.dim(`Generated ${queries.length} queries. Running evaluation...`));
155
+ // --- Run benchmark ---
156
+ const searchFn = async (query) => {
157
+ const result = await runtime.hybridSearch.search(query, { topK });
158
+ if (result.isErr())
159
+ return [];
160
+ return result.value.map((r) => r.chunkId);
161
+ };
162
+ const reportResult = await runBenchmark(queries, searchFn, scan.totalChunks, (completed, total) => {
163
+ if (completed % 10 === 0 || completed === total) {
164
+ // eslint-disable-next-line no-console
165
+ process.stdout.write(`\r${chalk.dim(` Progress: ${completed}/${total}`)}`);
166
+ }
167
+ });
168
+ runtime.close();
169
+ if (reportResult.isErr()) {
170
+ // eslint-disable-next-line no-console
171
+ console.error(chalk.red('\nBenchmark failed:'), reportResult.error.message);
172
+ process.exit(1);
173
+ }
174
+ const report = reportResult.value;
175
+ // --- Print summary ---
176
+ // eslint-disable-next-line no-console
177
+ console.log('\n');
178
+ // eslint-disable-next-line no-console
179
+ console.log(formatColoredSummary(report));
180
+ // --- Save JSON report ---
181
+ if (options.output) {
182
+ const outputPath = resolve(options.output);
183
+ const jsonReport = JSON.stringify(report, null, 2);
184
+ await writeFile(outputPath, jsonReport, 'utf-8');
185
+ // eslint-disable-next-line no-console
186
+ console.log('');
187
+ // eslint-disable-next-line no-console
188
+ console.log(chalk.green(`Detailed report saved to: ${outputPath}`));
189
+ }
190
+ }
191
+ catch (error) {
192
+ const message = error instanceof Error ? error.message : String(error);
193
+ // eslint-disable-next-line no-console
194
+ console.error(chalk.red('Benchmark failed:'), message);
195
+ process.exit(1);
196
+ }
197
+ });
198
+ }
@@ -1,4 +1,34 @@
1
1
  import { Command } from 'commander';
2
- import { type EmbeddingConfig, type EmbeddingProvider } from '@code-rag/core';
2
+ import { type ParsedFile, type CodeRAGConfig, type EmbeddingConfig, type EmbeddingProvider } from '@code-rag/core';
3
3
  export declare function createSimpleEmbeddingProvider(embeddingConfig: EmbeddingConfig): EmbeddingProvider;
4
+ export declare class IndexLogger {
5
+ private spinner;
6
+ private logPath;
7
+ private progressPath;
8
+ private phase;
9
+ private counts;
10
+ private readonly quiet;
11
+ constructor(storagePath: string, quiet?: boolean);
12
+ init(): Promise<void>;
13
+ start(text: string): void;
14
+ info(text: string): Promise<void>;
15
+ succeed(text: string): Promise<void>;
16
+ warn(text: string): Promise<void>;
17
+ fail(text: string): Promise<void>;
18
+ setPhase(phase: string, counts?: Record<string, number>): Promise<void>;
19
+ updateCount(key: string, value: number): Promise<void>;
20
+ private log;
21
+ private writeProgress;
22
+ }
4
23
  export declare function registerIndexCommand(program: Command): void;
24
+ /**
25
+ * Rebuild the root-level merged index from existing per-repo stores.
26
+ * Used when incremental indexing finds no changes but the unified index is missing
27
+ * (e.g., after upgrading CodeRAG or first run after multi-repo index was created).
28
+ */
29
+ export declare function rebuildMergedIndex(storagePath: string, repoResults: Array<{
30
+ repoName: string;
31
+ repoPath: string;
32
+ repoStoragePath: string;
33
+ parsedFiles: ParsedFile[];
34
+ }>, config: CodeRAGConfig, logger: IndexLogger): Promise<void>;
@@ -33,7 +33,7 @@ export function createSimpleEmbeddingProvider(embeddingConfig) {
33
33
  // ---------------------------------------------------------------------------
34
34
  // IndexLogger — dual output: ora spinner (interactive) + file log
35
35
  // ---------------------------------------------------------------------------
36
- class IndexLogger {
36
+ export class IndexLogger {
37
37
  spinner;
38
38
  logPath;
39
39
  progressPath;
@@ -968,6 +968,86 @@ export function registerIndexCommand(program) {
968
968
  }
969
969
  });
970
970
  }
971
+ /**
972
+ * Rebuild the root-level merged index from existing per-repo stores.
973
+ * Used when incremental indexing finds no changes but the unified index is missing
974
+ * (e.g., after upgrading CodeRAG or first run after multi-repo index was created).
975
+ */
976
+ export async function rebuildMergedIndex(storagePath, repoResults, config, logger) {
977
+ await logger.info('Rebuilding unified index from per-repo data...');
978
+ const mergedGraph = new DependencyGraph();
979
+ let totalMergedChunks = 0;
980
+ // Open root LanceDB store
981
+ const rootStore = new LanceDBStore(storagePath, config.embedding.dimensions);
982
+ await rootStore.connect();
983
+ for (const rr of repoResults) {
984
+ const repoGraphPath = join(rr.repoStoragePath, 'graph.json');
985
+ // Read all rows from per-repo LanceDB and copy to root store
986
+ const repoStore = new LanceDBStore(rr.repoStoragePath, config.embedding.dimensions);
987
+ await repoStore.connect();
988
+ try {
989
+ const internal = repoStore;
990
+ const table = internal.table;
991
+ if (table) {
992
+ const allRows = await table.query().toArray();
993
+ if (allRows.length > 0) {
994
+ const ids = allRows.map((r) => r.id);
995
+ // LanceDB returns Arrow-typed vectors (FixedSizeList with .isValid),
996
+ // not plain number[]. Convert to plain arrays for re-ingestion.
997
+ const embeddings = allRows.map((r) => Array.from(r.vector));
998
+ // Parse the metadata JSON string back to an object so upsert
999
+ // preserves all original fields (start_line, end_line, name, repo_name, etc.)
1000
+ // without double-serialization.
1001
+ const metadata = allRows.map((r) => {
1002
+ try {
1003
+ return JSON.parse(r.metadata);
1004
+ }
1005
+ catch {
1006
+ return {
1007
+ content: r.content,
1008
+ nl_summary: r.nl_summary,
1009
+ chunk_type: r.chunk_type,
1010
+ file_path: r.file_path,
1011
+ language: r.language,
1012
+ };
1013
+ }
1014
+ });
1015
+ const upsertResult = await rootStore.upsert(ids, embeddings, metadata);
1016
+ if (upsertResult.isOk()) {
1017
+ totalMergedChunks += allRows.length;
1018
+ }
1019
+ else {
1020
+ await logger.warn(`[${rr.repoName}] LanceDB upsert failed: ${upsertResult.error.message}`);
1021
+ }
1022
+ }
1023
+ }
1024
+ }
1025
+ catch (mergeErr) {
1026
+ const mergeMsg = mergeErr instanceof Error ? mergeErr.message : String(mergeErr);
1027
+ await logger.warn(`[${rr.repoName}] Failed to read LanceDB for merge: ${mergeMsg}`);
1028
+ }
1029
+ repoStore.close();
1030
+ // Merge graph
1031
+ try {
1032
+ const graphData = await readFile(repoGraphPath, 'utf-8');
1033
+ const repoGraph = DependencyGraph.fromJSON(JSON.parse(graphData));
1034
+ for (const node of repoGraph.getAllNodes())
1035
+ mergedGraph.addNode(node);
1036
+ for (const edge of repoGraph.getAllEdges())
1037
+ mergedGraph.addEdge(edge);
1038
+ }
1039
+ catch {
1040
+ // No graph for this repo — skip
1041
+ }
1042
+ }
1043
+ // Build merged BM25 from the root LanceDB (which now has all chunks)
1044
+ const mergedBm25 = await rebuildBm25FromStore(rootStore, logger, '');
1045
+ await writeFile(join(storagePath, 'bm25-index.json'), mergedBm25.serialize(), 'utf-8');
1046
+ rootStore.close();
1047
+ // Write merged graph
1048
+ await writeFile(join(storagePath, 'graph.json'), JSON.stringify(mergedGraph.toJSON()), 'utf-8');
1049
+ await logger.succeed(`Unified index rebuilt: ${totalMergedChunks} chunks from ${repoResults.length} repos`);
1050
+ }
971
1051
  /**
972
1052
  * Multi-repo indexing: iterate configured repos, index each with separate
973
1053
  * progress reporting and per-repo storage directories.
@@ -1113,9 +1193,25 @@ async function indexMultiRepo(config, storagePath, options, logger, startTime, e
1113
1193
  }
1114
1194
  await writeFile(rr.indexStatePath, JSON.stringify(rr.indexState.toJSON(), null, 2), 'utf-8');
1115
1195
  }
1196
+ // Check if root merged index is missing or empty — rebuild from per-repo stores
1197
+ const rootBm25Path = join(storagePath, 'bm25-index.json');
1198
+ let needsRebuild = !existsSync(rootBm25Path);
1199
+ if (!needsRebuild) {
1200
+ try {
1201
+ const bm25Data = await readFile(rootBm25Path, 'utf-8');
1202
+ const parsed = JSON.parse(bm25Data);
1203
+ needsRebuild = !parsed.documentCount || parsed.documentCount === 0;
1204
+ }
1205
+ catch {
1206
+ needsRebuild = true;
1207
+ }
1208
+ }
1209
+ if (needsRebuild) {
1210
+ await rebuildMergedIndex(storagePath, repoResults, config, logger);
1211
+ }
1116
1212
  const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
1117
1213
  // eslint-disable-next-line no-console
1118
- console.log(chalk.yellow('No chunks produced. Nothing to embed.'));
1214
+ console.log(chalk.yellow('No new chunks. Index is up to date.'));
1119
1215
  // eslint-disable-next-line no-console
1120
1216
  console.log(` Time elapsed: ${chalk.cyan(elapsed + 's')}`);
1121
1217
  return;
@@ -1198,6 +1294,12 @@ async function indexMultiRepo(config, storagePath, options, logger, startTime, e
1198
1294
  // ── Phase 3: Embed & Store per repo ─────────────────────────────────
1199
1295
  await logger.setPhase('embed');
1200
1296
  const resolvedEmbeddingProvider = embeddingProvider ?? createSimpleEmbeddingProvider(config.embedding);
1297
+ // Accumulators for merged root-level index
1298
+ const mergedIds = [];
1299
+ const mergedEmbeddings = [];
1300
+ const mergedMetadata = [];
1301
+ const mergedBm25Chunks = [];
1302
+ const mergedGraph = new DependencyGraph();
1201
1303
  for (const rr of repoResults) {
1202
1304
  if (rr.chunks.length === 0)
1203
1305
  continue;
@@ -1215,7 +1317,7 @@ async function indexMultiRepo(config, storagePath, options, logger, startTime, e
1215
1317
  continue;
1216
1318
  }
1217
1319
  const embeddings = embedResult.value;
1218
- // Store in LanceDB
1320
+ // Store in per-repo LanceDB
1219
1321
  await logger.info(`[${rr.repoName}] Storing in LanceDB...`);
1220
1322
  const store = new LanceDBStore(rr.repoStoragePath, config.embedding.dimensions);
1221
1323
  await store.connect();
@@ -1238,7 +1340,12 @@ async function indexMultiRepo(config, storagePath, options, logger, startTime, e
1238
1340
  await logger.fail(`[${rr.repoName}] Store failed: ${upsertResult.error.message}`);
1239
1341
  continue;
1240
1342
  }
1241
- // BM25 index
1343
+ // Accumulate for merged root index
1344
+ mergedIds.push(...ids);
1345
+ mergedEmbeddings.push(...embeddings);
1346
+ mergedMetadata.push(...metadata);
1347
+ mergedBm25Chunks.push(...enrichedChunks);
1348
+ // Per-repo BM25 index
1242
1349
  const bm25Path = join(rr.repoStoragePath, 'bm25-index.json');
1243
1350
  let bm25;
1244
1351
  if (options.full) {
@@ -1269,12 +1376,17 @@ async function indexMultiRepo(config, storagePath, options, logger, startTime, e
1269
1376
  }
1270
1377
  bm25.addChunks(enrichedChunks);
1271
1378
  await writeFile(bm25Path, bm25.serialize(), 'utf-8');
1272
- // Dependency graph
1379
+ // Per-repo dependency graph
1273
1380
  const graphBuilder = new GraphBuilder(rr.repoPath);
1274
1381
  const graphResult = graphBuilder.buildFromFiles(rr.parsedFiles);
1275
1382
  if (graphResult.isOk()) {
1276
1383
  const graphPath = join(rr.repoStoragePath, 'graph.json');
1277
1384
  const newGraph = graphResult.value;
1385
+ // Accumulate for merged graph
1386
+ for (const node of newGraph.getAllNodes())
1387
+ mergedGraph.addNode(node);
1388
+ for (const edge of newGraph.getAllEdges())
1389
+ mergedGraph.addEdge(edge);
1278
1390
  if (options.full) {
1279
1391
  await writeFile(graphPath, JSON.stringify(newGraph.toJSON()), 'utf-8');
1280
1392
  }
@@ -1320,6 +1432,26 @@ async function indexMultiRepo(config, storagePath, options, logger, startTime, e
1320
1432
  store.close();
1321
1433
  await logger.succeed(`[${rr.repoName}] ${enrichedChunks.length} chunks indexed`);
1322
1434
  }
1435
+ // ── Phase 4: Merge all repos into root-level unified index ─────────
1436
+ // This ensures search/MCP/viewer/benchmark can query all repos at once
1437
+ if (mergedIds.length > 0) {
1438
+ await logger.info('Merging all repos into unified index...');
1439
+ // Merged LanceDB at root storagePath
1440
+ const rootStore = new LanceDBStore(storagePath, config.embedding.dimensions);
1441
+ await rootStore.connect();
1442
+ const mergeResult = await rootStore.upsert(mergedIds, mergedEmbeddings, mergedMetadata);
1443
+ if (mergeResult.isErr()) {
1444
+ await logger.fail(`Merged store failed: ${mergeResult.error.message}`);
1445
+ }
1446
+ rootStore.close();
1447
+ // Merged BM25 index at root
1448
+ const rootBm25 = new BM25Index();
1449
+ rootBm25.addChunks(mergedBm25Chunks);
1450
+ await writeFile(join(storagePath, 'bm25-index.json'), rootBm25.serialize(), 'utf-8');
1451
+ // Merged graph at root
1452
+ await writeFile(join(storagePath, 'graph.json'), JSON.stringify(mergedGraph.toJSON()), 'utf-8');
1453
+ await logger.succeed(`Unified index: ${mergedIds.length} chunks across ${repos.length} repos`);
1454
+ }
1323
1455
  // ── Final summary ───────────────────────────────────────────────────
1324
1456
  const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
1325
1457
  // eslint-disable-next-line no-console
@@ -93,3 +93,8 @@ export declare function runNonInteractive(rootDir: string, options: {
93
93
  languages?: string;
94
94
  multi?: boolean;
95
95
  }): Promise<void>;
96
+ /**
97
+ * Ensure .coderag/ is listed in .gitignore so database files are not committed.
98
+ * Creates .gitignore if it does not exist; appends entry if missing.
99
+ */
100
+ export declare function ensureGitignore(rootDir: string): Promise<void>;
@@ -1,7 +1,7 @@
1
1
  import chalk from 'chalk';
2
2
  import { select, input, confirm } from '@inquirer/prompts';
3
3
  import { stringify } from 'yaml';
4
- import { writeFile, mkdir, access, readdir } from 'node:fs/promises';
4
+ import { writeFile, readFile, mkdir, access, readdir } from 'node:fs/promises';
5
5
  import { join, basename } from 'node:path';
6
6
  import { detectLanguages } from './init.js';
7
7
  // --- Constants ---
@@ -419,6 +419,8 @@ export async function runWizard(rootDir) {
419
419
  await mkdir(storageDir, { recursive: true });
420
420
  // eslint-disable-next-line no-console
421
421
  console.log(chalk.green(` Created ${storageDir}`));
422
+ // Step 7b: Ensure .coderag/ is in .gitignore
423
+ await ensureGitignore(rootDir);
422
424
  // Step 8: Summary
423
425
  // eslint-disable-next-line no-console
424
426
  console.log(chalk.bold.green('\n CodeRAG initialized successfully!\n'));
@@ -501,12 +503,37 @@ export async function runNonInteractive(rootDir, options) {
501
503
  await mkdir(storageDir, { recursive: true });
502
504
  // eslint-disable-next-line no-console
503
505
  console.log(chalk.green('Created'), storageDir);
506
+ // Ensure .coderag/ is in .gitignore
507
+ await ensureGitignore(rootDir);
504
508
  // Done
505
509
  // eslint-disable-next-line no-console
506
510
  console.log(chalk.green('\nCodeRAG initialized successfully!'));
507
511
  // eslint-disable-next-line no-console
508
512
  console.log(chalk.dim('Run "coderag index" to index your codebase.'));
509
513
  }
514
+ /**
515
+ * Ensure .coderag/ is listed in .gitignore so database files are not committed.
516
+ * Creates .gitignore if it does not exist; appends entry if missing.
517
+ */
518
+ export async function ensureGitignore(rootDir) {
519
+ const gitignorePath = join(rootDir, '.gitignore');
520
+ const entry = '.coderag/';
521
+ try {
522
+ const content = await readFile(gitignorePath, 'utf-8');
523
+ const lines = content.split('\n').map((l) => l.trim());
524
+ if (lines.includes(entry) || lines.includes('.coderag'))
525
+ return;
526
+ const newContent = content.endsWith('\n') ? `${content}${entry}\n` : `${content}\n${entry}\n`;
527
+ await writeFile(gitignorePath, newContent, 'utf-8');
528
+ // eslint-disable-next-line no-console
529
+ console.log(chalk.green(` Added ${entry} to .gitignore`));
530
+ }
531
+ catch {
532
+ await writeFile(gitignorePath, `${entry}\n`, 'utf-8');
533
+ // eslint-disable-next-line no-console
534
+ console.log(chalk.green(` Created .gitignore with ${entry}`));
535
+ }
536
+ }
510
537
  /**
511
538
  * Pull an Ollama model via the API.
512
539
  */
@@ -1,7 +1,5 @@
1
1
  import chalk from 'chalk';
2
- import { readFile } from 'node:fs/promises';
3
- import { join, resolve, sep } from 'node:path';
4
- import { loadConfig, OllamaEmbeddingProvider, LanceDBStore, BM25Index, HybridSearch, } from '@code-rag/core';
2
+ import { createRuntime, } from '@code-rag/core';
5
3
  /**
6
4
  * Format a single search result for terminal display.
7
5
  */
@@ -39,44 +37,18 @@ export function registerSearchCommand(program) {
39
37
  console.error(chalk.red('Invalid --top-k value. Must be a positive integer.'));
40
38
  process.exit(1);
41
39
  }
42
- // Load config
43
- const configResult = await loadConfig(rootDir);
44
- if (configResult.isErr()) {
40
+ // Initialize runtime (search-only mode: skip graph, reranker, context expander)
41
+ const runtimeResult = await createRuntime({ rootDir, searchOnly: true });
42
+ if (runtimeResult.isErr()) {
45
43
  // eslint-disable-next-line no-console
46
- console.error(chalk.red('Config not found.'), 'Run "coderag init" first.');
44
+ console.error(chalk.red('Initialization failed.'), runtimeResult.error.message);
47
45
  process.exit(1);
48
46
  }
49
- const config = configResult.value;
50
- const storagePath = resolve(rootDir, config.storage.path);
51
- // Prevent path traversal outside project root
52
- if (!storagePath.startsWith(resolve(rootDir) + sep) && storagePath !== resolve(rootDir)) {
53
- // eslint-disable-next-line no-console
54
- console.error(chalk.red('Storage path escapes project root'));
55
- process.exit(1);
56
- }
57
- // Create services
58
- const embeddingProvider = new OllamaEmbeddingProvider({
59
- model: config.embedding.model,
60
- dimensions: config.embedding.dimensions,
61
- });
62
- const store = new LanceDBStore(storagePath, config.embedding.dimensions);
63
- await store.connect();
64
- // Load BM25 index
65
- let bm25 = new BM25Index();
66
- const bm25Path = join(storagePath, 'bm25-index.json');
67
- try {
68
- const bm25Data = await readFile(bm25Path, 'utf-8');
69
- bm25 = BM25Index.deserialize(bm25Data);
70
- }
71
- catch {
72
- // No BM25 index yet
73
- }
74
- // Create hybrid search
75
- const hybridSearch = new HybridSearch(store, bm25, embeddingProvider, config.search);
47
+ const runtime = runtimeResult.value;
76
48
  // Run search
77
- const searchResult = await hybridSearch.search(query, { topK });
49
+ const searchResult = await runtime.hybridSearch.search(query, { topK });
78
50
  if (searchResult.isErr()) {
79
- store.close();
51
+ runtime.close();
80
52
  // eslint-disable-next-line no-console
81
53
  console.error(chalk.red('Search failed:'), searchResult.error.message);
82
54
  process.exit(1);
@@ -95,7 +67,7 @@ export function registerSearchCommand(program) {
95
67
  const fileFilter = options.file.toLowerCase();
96
68
  results = results.filter((r) => (r.chunk?.filePath ?? '').toLowerCase().includes(fileFilter));
97
69
  }
98
- store.close();
70
+ runtime.close();
99
71
  // Display results
100
72
  if (results.length === 0) {
101
73
  // eslint-disable-next-line no-console
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ import { registerStatusCommand } from './commands/status.js';
9
9
  import { registerViewerCommand } from './commands/viewer.js';
10
10
  import { registerWatchCommand } from './commands/watch-cmd.js';
11
11
  import { registerHooksCommand } from './commands/hooks-cmd.js';
12
+ import { registerBenchmarkCommand } from './commands/benchmark-cmd.js';
12
13
  const require = createRequire(import.meta.url);
13
14
  const pkg = require('../package.json');
14
15
  const program = new Command();
@@ -24,4 +25,5 @@ registerStatusCommand(program);
24
25
  registerViewerCommand(program);
25
26
  registerWatchCommand(program);
26
27
  registerHooksCommand(program);
28
+ registerBenchmarkCommand(program);
27
29
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-rag/cli",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "CLI tool for CodeRAG — init, index, search, serve, and status commands for codebase context engine",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -48,9 +48,9 @@
48
48
  "commander": "^13.1.0",
49
49
  "ora": "^8.2.0",
50
50
  "yaml": "^2.7.0",
51
- "@code-rag/api-server": "0.1.6",
52
- "@code-rag/mcp-server": "0.1.6",
53
- "@code-rag/core": "0.1.6"
51
+ "@code-rag/api-server": "0.1.8",
52
+ "@code-rag/core": "0.1.8",
53
+ "@code-rag/mcp-server": "0.1.8"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@types/node": "^22.13.4",