coderev-cli 1.0.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.
@@ -0,0 +1,546 @@
1
+ const { loadConfig, getApiKey } = require('./config');
2
+ const { cacheKey, getCached, setCached } = require('./cache');
3
+ const { recordReview } = require('./stats');
4
+ const { getRuleDescriptions } = require('./rules');
5
+
6
+ // ── 多智能体并行审查 ──
7
+
8
+ /**
9
+ * Agent roles for parallel review
10
+ */
11
+ const AGENT_ROLES = [
12
+ {
13
+ name: 'security',
14
+ label: '🔒 Security Auditor',
15
+ focus: `Focus on security vulnerabilities:
16
+ - SQL injection, command injection
17
+ - XSS, CSRF, SSRF
18
+ - Hardcoded secrets, weak auth
19
+ - Insecure deserialization
20
+ - Path traversal, IDOR
21
+ - Rate limiting, missing access control
22
+ - Insecure cryptography
23
+ - Supply chain risks (unpinned deps)`,
24
+ weight: 1.0,
25
+ },
26
+ {
27
+ name: 'bugs',
28
+ label: '🐛 Bug Detector',
29
+ focus: `Focus on bugs and correctness:
30
+ - Null pointer / undefined access
31
+ - Race conditions, async issues
32
+ - Off-by-one errors
33
+ - Type mismatches
34
+ - Memory leaks
35
+ - Uncaught exceptions
36
+ - Logic errors
37
+ - Infinite loops, recursion
38
+ - Incorrect API usage`,
39
+ weight: 1.0,
40
+ },
41
+ {
42
+ name: 'quality',
43
+ label: '📐 Code Quality',
44
+ focus: `Focus on code quality and conventions:
45
+ - Project conventions and style
46
+ - DRY violations
47
+ - Over-engineering / complexity
48
+ - Naming conventions
49
+ - Error handling patterns
50
+ - Test coverage gaps
51
+ - Documentation quality
52
+ - Performance anti-patterns
53
+ - Dead code / unused imports`,
54
+ weight: 1.0,
55
+ },
56
+ ];
57
+
58
+ /**
59
+ * Merge results from multiple agents with confidence scoring.
60
+ * Each issue gets a confidence score 0-100 as the agent's confidence
61
+ * that this is a real, actionable issue.
62
+ */
63
+ function mergeAgentResults(agents, diff, projectHint) {
64
+ const allIssues = [];
65
+ const seenMessages = new Set();
66
+
67
+ for (const agentResult of agents) {
68
+ if (!agentResult.success || !agentResult.issues) continue;
69
+
70
+ for (const issue of agentResult.issues) {
71
+ // Deduplicate by message signature
72
+ const sig = `${issue.file || ''}:${issue.line || 0}:${issue.message.slice(0, 60)}`;
73
+ if (seenMessages.has(sig)) continue;
74
+ seenMessages.add(sig);
75
+
76
+ // Assign confidence based on agent's confidence in this issue
77
+ // If agent didn't provide a score, calculate based on issue type
78
+ const confidence = issue.confidence ?? calculateConfidence(issue, agentResult.name);
79
+
80
+ allIssues.push({
81
+ ...issue,
82
+ confidence,
83
+ detectedBy: agentResult.name,
84
+ });
85
+ }
86
+ }
87
+
88
+ // Sort by confidence (highest first)
89
+ allIssues.sort((a, b) => (b.confidence || 0) - (a.confidence || 0));
90
+
91
+ // Filter: only keep confidence >= 60 (below threshold = false positive)
92
+ const filtered = allIssues.filter(i => i.confidence >= 60);
93
+ const filteredCount = allIssues.length - filtered.length;
94
+
95
+ return { issues: filtered, filteredCount };
96
+ }
97
+
98
+ /**
99
+ * Calculate confidence score based on issue characteristics.
100
+ */
101
+ function calculateConfidence(issue, agentName) {
102
+ let base = 70;
103
+
104
+ // Error types are higher confidence
105
+ if (issue.type === 'error') base += 15;
106
+ else if (issue.type === 'warning') base += 5;
107
+
108
+ // Issues with file + line are more actionable
109
+ if (issue.file) base += 5;
110
+ if (issue.line) base += 5;
111
+ if (issue.suggestion) base += 5;
112
+
113
+ // Cap at 99
114
+ return Math.min(99, base);
115
+ }
116
+
117
+ /**
118
+ * Build parallel agent prompts for multi-perspective review.
119
+ */
120
+ function buildAgentPrompts(diff, config, options = {}) {
121
+ const rulesConfig = config.rules || {};
122
+ const ruleLines = getRuleDescriptions(rulesConfig, diff);
123
+ const auditBlock = options.audit ? '\n- **SECURITY AUDIT MODE ACTIVE**' : '';
124
+
125
+ return AGENT_ROLES.map(role => {
126
+ return {
127
+ role,
128
+ messages: [
129
+ {
130
+ role: 'system',
131
+ content: `You are ${role.label}, an expert code reviewer.
132
+
133
+ Your task: Review the provided git diff and ${role.focus}
134
+
135
+ ${options.projectHint ? `Project context:
136
+ ${options.projectHint}
137
+
138
+ ` : ''}
139
+ Return a JSON object:
140
+ \`\`\`json
141
+ {
142
+ "issues": [
143
+ {
144
+ "type": "error|warning|info",
145
+ "severity": "high|medium|low",
146
+ "confidence": <number 0-100, how confident you are this is a real issue>,
147
+ "file": "filename (optional)",
148
+ "line": <line_number> (optional),
149
+ "message": "Description",
150
+ "suggestion": "How to fix (optional)"
151
+ }
152
+ ],
153
+ "summary": "Brief summary of findings from this perspective"
154
+ }
155
+ \`\`\`
156
+
157
+ Confidence scoring guide:
158
+ - 80-100: Absolutely certain, definitely a real issue that should be fixed
159
+ - 60-79: Highly likely, strong evidence but not 100%
160
+ - 40-59: Possible, some evidence but could be false positive
161
+ - 20-39: Weak signal, low confidence
162
+ - 0-19: Not confident, noise
163
+
164
+ Important: Return ONLY valid JSON. No markdown wrapping.${auditBlock}`,
165
+ },
166
+ {
167
+ role: 'user',
168
+ content: `Git diff to review:
169
+
170
+ \`\`\`diff
171
+ ${diff.slice(0, 8000)}
172
+ \`\`\``,
173
+ },
174
+ ],
175
+ };
176
+ });
177
+ }
178
+
179
+ /**
180
+ * Run parallel agents and collect results.
181
+ */
182
+ async function runParallelAgents(apiKey, config, prompts) {
183
+ const results = [];
184
+ const errors = [];
185
+
186
+ const tasks = prompts.map(async (p) => {
187
+ try {
188
+ const text = await callAI(apiKey, p.messages, config);
189
+ const parsed = parseReviewResponse(text);
190
+ return { name: p.role.name, success: true, ...parsed };
191
+ } catch (err) {
192
+ return { name: p.role.name, success: false, error: err.message, issues: [] };
193
+ }
194
+ });
195
+
196
+ // Run all agents in parallel
197
+ const settled = await Promise.allSettled(tasks);
198
+ for (const s of settled) {
199
+ if (s.status === 'fulfilled') {
200
+ results.push(s.value);
201
+ } else {
202
+ errors.push(s.reason);
203
+ }
204
+ }
205
+
206
+ return { results, errors };
207
+ }
208
+
209
+ /**
210
+ * Review a git diff string using multi-agent parallel review.
211
+ * @param {string} diff - The git diff text
212
+ * @param {object} config - Configuration object (optional, loaded if omitted)
213
+ * @param {object} [options] - Additional options
214
+ * @param {boolean} [options.noCache] - Skip cache
215
+ * @param {boolean} [options.single] - Use single agent (legacy mode, no parallel)
216
+ * @param {boolean} [options.audit] - Security audit mode
217
+ * @param {string} [options.context] - Previous review context (for incremental reviews)
218
+ * @param {string} [options.ignorePattern] - File patterns to ignore
219
+ * @param {number} [options.minConfidence] - Minimum confidence threshold (default: 60)
220
+ * @returns {Promise<object>} Review result with issues, suggestions, score, etc.
221
+ */
222
+ async function reviewDiff(diff, config, options = {}) {
223
+ if (!config) config = loadConfig();
224
+
225
+ // Apply ignore patterns: strip ignored files from diff
226
+ if (options.ignorePattern) {
227
+ diff = filterDiffByPattern(diff, options.ignorePattern);
228
+ }
229
+
230
+ // Check cache
231
+ if (!options.noCache && !options.single && !options.audit) {
232
+ const ckey = cacheKey(diff);
233
+ const cached = getCached(ckey);
234
+ if (cached) {
235
+ return { ...cached, _cached: true };
236
+ }
237
+ }
238
+
239
+ const apiKey = getApiKey(config);
240
+ const projectHint = loadProjectHint();
241
+ const minConfidence = options.minConfidence ?? 60;
242
+
243
+ let result;
244
+
245
+ if (options.single) {
246
+ // Legacy single-agent mode
247
+ const prompt = buildReviewPrompt(diff, config, { ...options, projectHint });
248
+ const aiResponse = await callAI(apiKey, prompt, config);
249
+ result = parseReviewResponse(aiResponse);
250
+ // Add default confidence to legacy results
251
+ if (result.issues) {
252
+ result.issues = result.issues.map(i => ({
253
+ ...i,
254
+ confidence: calculateConfidence(i, 'legacy'),
255
+ })).filter(i => i.confidence >= minConfidence);
256
+ }
257
+ } else {
258
+ // Multi-agent parallel review
259
+ const prompts = buildAgentPrompts(diff, config, { ...options, projectHint });
260
+ const { results: agentResults, errors } = await runParallelAgents(apiKey, config, prompts);
261
+
262
+ // Merge and score issues
263
+ const { issues, filteredCount } = mergeAgentResults(agentResults, diff, projectHint);
264
+
265
+ // Build final summary
266
+ const totalAgentIssues = agentResults.reduce((s, a) => s + (a.issues?.length || 0), 0);
267
+ const agentSummaries = agentResults.map(a => ` ${a.name}: ${a.issues?.length || 0} issues`).join('\n');
268
+
269
+ result = {
270
+ summary: agentResults.map(a => a.summary).filter(Boolean).join(' | '),
271
+ score: calculateOverallScore(issues, agentResults),
272
+ issues,
273
+ suggestions: [],
274
+ praise: [],
275
+ _agents: {
276
+ total: agentResults.length,
277
+ summary: agentSummaries,
278
+ totalIssuesFound: totalAgentIssues,
279
+ filteredLowConfidence: filteredCount,
280
+ minConfidence,
281
+ errors: errors.length,
282
+ },
283
+ };
284
+
285
+ // Generate overall suggestions from top issues
286
+ if (issues.length > 0) {
287
+ result.suggestions = issues.slice(0, 3).map(i => i.suggestion).filter(Boolean);
288
+ }
289
+ }
290
+
291
+ // Record to stats
292
+ try { recordReview(result); } catch {}
293
+
294
+ // Cache result
295
+ if (!options.noCache) {
296
+ const ckey = cacheKey(diff);
297
+ setCached(ckey, result);
298
+ }
299
+
300
+ return result;
301
+ }
302
+
303
+ /**
304
+ * Calculate overall quality score from merged multi-agent results.
305
+ */
306
+ function calculateOverallScore(issues, agentResults) {
307
+ if (issues.length === 0) return 100;
308
+
309
+ const errorCount = issues.filter(i => i.type === 'error').length;
310
+ const warningCount = issues.filter(i => i.type === 'warning').length;
311
+ const infoCount = issues.filter(i => i.type === 'info').length;
312
+
313
+ const highConfidence = issues.filter(i => i.confidence >= 85).length;
314
+ const mediumConfidence = issues.filter(i => i.confidence >= 70 && i.confidence < 85).length;
315
+
316
+ // Base deductions
317
+ let score = 100;
318
+ score -= errorCount * 15;
319
+ score -= warningCount * 8;
320
+ score -= infoCount * 3;
321
+ score -= highConfidence * 5; // Extra deduction for high-confidence issues
322
+ score -= mediumConfidence * 2;
323
+
324
+ return Math.max(0, Math.min(100, score));
325
+ }
326
+
327
+ /**
328
+ * Build the system prompt for code review.
329
+ */
330
+ function buildReviewPrompt(diff, config, options = {}) {
331
+ const rulesConfig = config.rules || {};
332
+ const ruleLines = getRuleDescriptions(rulesConfig, diff);
333
+ if (ruleLines.length === 0) ruleLines.push('- General code quality best practices');
334
+
335
+ let contextBlock = '';
336
+ if (options.context) {
337
+ contextBlock = `\n\nPrevious review context (address these if still relevant, and don\'t repeat issues that were already fixed):\n${options.context}`;
338
+ }
339
+
340
+ let hintBlock = '';
341
+ if (options.projectHint) {
342
+ hintBlock = `
343
+
344
+ Project context (use this to guide your review):
345
+ ${options.projectHint}`;
346
+ }
347
+ let auditBlock = '';
348
+ if (options.audit) {
349
+ auditBlock = `\n\n## SECURITY AUDIT MODE
350
+ You are now in security audit mode. Focus on the following:
351
+
352
+ ### CRITICAL (must check):
353
+ - SQL injection: parameterized queries vs string concatenation
354
+ - Command injection: shell command construction with user input
355
+ - Hardcoded secrets: API keys, passwords, tokens, certificates
356
+ - Authentication/authorization: missing access control, privilege escalation
357
+ - Cross-site scripting (XSS): unescaped user input in HTML/output
358
+ - Insecure direct object references (IDOR): accessing data by user-supplied IDs
359
+
360
+ ### HIGH (should check):
361
+ - Path traversal: user-supplied file paths without validation
362
+ - Insecure deserialization: parsing untrusted data without validation
363
+ - Server-side request forgery (SSRF): making requests to user-supplied URLs
364
+ - Insecure cryptography: weak algorithms, hardcoded keys, improper IV usage
365
+ - Rate limiting: endpoints without throttling
366
+
367
+ ### MEDIUM (good to check):
368
+ - Information disclosure: stack traces, debug endpoints, verbose error messages
369
+ - Missing security headers: CSP, X-Frame-Options, HSTS
370
+ - Insecure direct file access
371
+ - Session management: cookie flags, token expiry, CSRF protection
372
+ - Input validation: insufficient validation on user-supplied data
373
+
374
+ For each issue found, assign a CVSS-like score (1-10) and output it as "cvss" field.
375
+ Be aggressive - it's better to flag a false positive than miss a real vulnerability.
376
+ `;
377
+ }
378
+
379
+
380
+ return [
381
+ {
382
+ role: 'system',
383
+ content: `You are an expert code reviewer. Analyze the provided git diff and return a JSON object with the following structure:
384
+
385
+ \`\`\`json
386
+ {
387
+ "summary": "Brief one-line summary of the change",
388
+ "score": <number 0-100>,
389
+ "issues": [
390
+ {
391
+ "type": "error|warning|info",
392
+ "severity": "high|medium|low",
393
+ "file": "filename (optional)",
394
+ "line": <line_number> (optional),
395
+ "message": "Description of the issue",
396
+ "suggestion": "How to fix it (optional)"
397
+ }
398
+ ],
399
+ "suggestions": ["improvement suggestion 1", "suggestion 2"],
400
+ "praise": ["good thing 1", "good thing 2"]
401
+ }
402
+ \`\`\`
403
+
404
+ Rules to enforce:
405
+ ${ruleLines.join('\n')}
406
+
407
+ Important:
408
+ - Score should reflect overall quality. 90+ = excellent, 70-89 = good, 50-69 = needs improvement, <50 = major issues.
409
+ - Be constructive. Include praise for good practices.
410
+ - Return ONLY valid JSON, no markdown wrapping, no explanations outside the JSON.${contextBlock}${hintBlock}${auditBlock}`,
411
+ },
412
+ {
413
+ role: 'user',
414
+ content: `Here is the git diff to review:\n\n\`\`\`diff\n${diff}\n\`\`\``,
415
+ },
416
+ ];
417
+ }
418
+
419
+ /**
420
+ * Call the AI provider.
421
+ */
422
+ async function callAI(apiKey, messages, config) {
423
+ const aiConfig = config.ai || {};
424
+ const provider = aiConfig.provider || 'openai';
425
+
426
+ const OpenAI = require('openai');
427
+
428
+ // Determine base URL from provider or config
429
+ let baseURL = aiConfig.baseURL;
430
+ if (!baseURL) {
431
+ if (provider === 'deepseek') baseURL = 'https://api.deepseek.com';
432
+ // openai and all others default to undefined (official endpoint)
433
+ }
434
+
435
+ const defaultModels = {
436
+ openai: 'gpt-4o',
437
+ deepseek: 'deepseek-chat',
438
+ };
439
+
440
+ const client = new OpenAI({ apiKey, baseURL: baseURL || undefined });
441
+
442
+ const response = await client.chat.completions.create({
443
+ model: aiConfig.model || defaultModels[provider] || provider,
444
+ temperature: aiConfig.temperature ?? 0.3,
445
+ max_tokens: aiConfig.maxTokens || 4096,
446
+ messages,
447
+ });
448
+
449
+ return response.choices[0]?.message?.content || '';
450
+ }
451
+
452
+ /**
453
+ * Parse AI response into structured review result.
454
+ */
455
+ function parseReviewResponse(text) {
456
+ try {
457
+ // Try direct parse first
458
+ return JSON.parse(text);
459
+ } catch {
460
+ // Try extracting JSON from markdown
461
+ const jsonMatch = text.match(/```(?:json)?\s*(\{[\s\S]*?\})\s*```/);
462
+ if (jsonMatch) {
463
+ try {
464
+ return JSON.parse(jsonMatch[1]);
465
+ } catch {
466
+ // fall through
467
+ }
468
+ }
469
+
470
+ // Try finding any JSON object
471
+ const braceMatch = text.match(/\{[\s\S]*\}/);
472
+ if (braceMatch) {
473
+ try {
474
+ return JSON.parse(braceMatch[0]);
475
+ } catch {
476
+ // fall through
477
+ }
478
+ }
479
+
480
+ // Last resort: return raw text as structured error
481
+ return {
482
+ summary: 'Could not parse review response',
483
+ score: 0,
484
+ issues: [{ type: 'error', severity: 'high', message: 'AI returned unparseable response', suggestion: text.slice(0, 500) }],
485
+ suggestions: [],
486
+ praise: [],
487
+ };
488
+ }
489
+ }
490
+
491
+ /**
492
+ * Filter diff to exclude files matching ignore patterns.
493
+ * Support simple glob patterns: *.md, test/**
494
+ */
495
+ function filterDiffByPattern(diff, ignorePattern) {
496
+ if (!ignorePattern) return diff;
497
+
498
+ const patterns = ignorePattern.split(',').map(p => {
499
+ const glob = p.trim();
500
+ // Convert simple glob to regex
501
+ const regex = glob
502
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
503
+ .replace(/\*/g, '[^/]*')
504
+ .replace(/\*\*/g, '.*');
505
+ return new RegExp(regex);
506
+ });
507
+
508
+ const lines = diff.split('\n');
509
+ const filtered = [];
510
+ let skipBlock = false;
511
+
512
+ for (const line of lines) {
513
+ const fileMatch = line.match(/^\+\+\+ b\/(.*)/) || line.match(/^diff --git a\/(.*) b\//);
514
+ if (fileMatch) {
515
+ const filePath = fileMatch[1];
516
+ skipBlock = patterns.some(p => p.test(filePath));
517
+ }
518
+ if (!skipBlock) {
519
+ filtered.push(line);
520
+ }
521
+ }
522
+
523
+ return filtered.join('\n');
524
+ }
525
+
526
+
527
+ /**
528
+ * Load .coderevhint file from current or parent directories.
529
+ */
530
+ function loadProjectHint() {
531
+ const fs = require('fs');
532
+ const path = require('path');
533
+ let current = process.cwd();
534
+ while (true) {
535
+ const hintPath = path.join(current, '.coderevhint');
536
+ if (fs.existsSync(hintPath)) {
537
+ return fs.readFileSync(hintPath, 'utf-8').trim();
538
+ }
539
+ const parent = path.dirname(current);
540
+ if (parent === current) break;
541
+ current = parent;
542
+ }
543
+ return '';
544
+ }
545
+
546
+ module.exports = { reviewDiff, parseReviewResponse };