@code-rag/cli 0.1.7 → 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,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.7",
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.7",
52
- "@code-rag/core": "0.1.7",
53
- "@code-rag/mcp-server": "0.1.7"
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",