codecritique 2.0.3 → 2.0.5

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 (3) hide show
  1. package/package.json +1 -1
  2. package/src/index.js +208 -52
  3. package/src/llm.js +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codecritique",
3
- "version": "2.0.3",
3
+ "version": "2.0.5",
4
4
  "description": "AI-powered code review tool for any programming language",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/index.js CHANGED
@@ -30,7 +30,9 @@ import { ProjectAnalyzer } from './project-analyzer.js';
30
30
  import { reviewFile, reviewFiles, reviewPullRequest } from './rag-review.js';
31
31
  import { execGitSafe } from './utils/command.js';
32
32
  import { ensureBranchExists, findBaseBranch } from './utils/git.js';
33
- import { verboseLog } from './utils/logging.js';
33
+ import { diagnosticLog, isDebugEnabled, isVerboseEnabled, verboseLog } from './utils/logging.js';
34
+ import { collectPRLevelFindings, getFileLevelIssueCount, hasPRLevelFindings } from './utils/review-results.js';
35
+ import { configureCleanStdoutForDataOutput, isBrokenStdoutPipeError, writeStdout } from './utils/stdout.js';
34
36
 
35
37
  // Create a default embeddings system instance
36
38
  const embeddingsSystem = getDefaultEmbeddingsSystem();
@@ -212,7 +214,7 @@ else {
212
214
 
213
215
  // Register process event handlers for cleanup (embeddings cleanup primarily)
214
216
  process.on('SIGINT', async () => {
215
- console.log(chalk.yellow('\nReceived SIGINT. Attempting graceful shutdown...'));
217
+ diagnosticLog(chalk.yellow('\nReceived SIGINT. Attempting graceful shutdown...'));
216
218
  // Set a timeout to force exit if cleanup hangs
217
219
  const forceExitTimeout = setTimeout(() => {
218
220
  console.error(chalk.red('Cleanup timed out after 10 seconds. Forcing exit...'));
@@ -220,23 +222,23 @@ process.on('SIGINT', async () => {
220
222
  }, 10000); // 10 seconds timeout
221
223
 
222
224
  try {
223
- console.log(chalk.cyan('SIGINT handler: Attempting embeddingsSystem.cleanup()...'));
225
+ diagnosticLog(chalk.cyan('SIGINT handler: Attempting embeddingsSystem.cleanup()...'));
224
226
  await embeddingsSystem.cleanup();
225
- console.log(chalk.green('embeddingsSystem.cleanup() completed.'));
227
+ diagnosticLog(chalk.green('embeddingsSystem.cleanup() completed.'));
226
228
  clearTimeout(forceExitTimeout); // Cleanup finished, clear the timeout
227
- console.log(chalk.cyan('SIGINT handler: Exiting normally (code 0).'));
229
+ diagnosticLog(chalk.cyan('SIGINT handler: Exiting normally (code 0).'));
228
230
  process.exit(0); // Exit normally
229
231
  }
230
232
  catch (err) {
231
233
  console.error(chalk.red('Error during embeddingsSystem.cleanup():'), err.message);
232
234
  clearTimeout(forceExitTimeout);
233
- console.log(chalk.cyan('SIGINT handler: Exiting after error (code 1).'));
235
+ diagnosticLog(chalk.cyan('SIGINT handler: Exiting after error (code 1).'));
234
236
  process.exit(1); // Exit with error code
235
237
  }
236
238
  });
237
239
 
238
240
  process.on('SIGTERM', async () => {
239
- console.log(chalk.yellow('\nReceived SIGTERM. Attempting graceful shutdown...'));
241
+ diagnosticLog(chalk.yellow('\nReceived SIGTERM. Attempting graceful shutdown...'));
240
242
  // Set a timeout to force exit if cleanup hangs
241
243
  const forceExitTimeout = setTimeout(() => {
242
244
  console.error(chalk.red('Cleanup timed out after 10 seconds. Forcing exit...'));
@@ -244,17 +246,17 @@ process.on('SIGTERM', async () => {
244
246
  }, 10000);
245
247
 
246
248
  try {
247
- console.log(chalk.cyan('SIGTERM handler: Attempting embeddingsSystem.cleanup()...'));
249
+ diagnosticLog(chalk.cyan('SIGTERM handler: Attempting embeddingsSystem.cleanup()...'));
248
250
  await embeddingsSystem.cleanup();
249
- console.log(chalk.green('embeddingsSystem.cleanup() completed.'));
251
+ diagnosticLog(chalk.green('embeddingsSystem.cleanup() completed.'));
250
252
  clearTimeout(forceExitTimeout); // Cleanup finished, clear the timeout
251
- console.log(chalk.cyan('SIGTERM handler: Exiting normally (code 0).'));
253
+ diagnosticLog(chalk.cyan('SIGTERM handler: Exiting normally (code 0).'));
252
254
  process.exit(0); // Exit normally
253
255
  }
254
256
  catch (err) {
255
257
  console.error(chalk.red('Error during embeddingsSystem.cleanup():'), err.message);
256
258
  clearTimeout(forceExitTimeout);
257
- console.log(chalk.cyan('SIGTERM handler: Exiting after error (code 1).'));
259
+ diagnosticLog(chalk.cyan('SIGTERM handler: Exiting after error (code 1).'));
258
260
  process.exit(1); // Exit with error code
259
261
  }
260
262
  });
@@ -262,7 +264,7 @@ process.on('SIGTERM', async () => {
262
264
  // Ensure cleanup on normal exit
263
265
  process.on('exit', () => {
264
266
  // Note: Async cleanup might not fully complete here
265
- console.log(chalk.cyan('Exiting...'));
267
+ diagnosticLog(chalk.cyan('Exiting...'));
266
268
  });
267
269
 
268
270
  // REMOVED: Old options processing logic for ignore/severity
@@ -272,6 +274,7 @@ process.on('exit', () => {
272
274
 
273
275
  // Main function to run the code review (Refactored to use cag-review.js)
274
276
  async function runCodeReview(options) {
277
+ configureCleanStdoutForDataOutput(options);
275
278
  let reviewTask = null;
276
279
  let operationDescription = '';
277
280
  const startTime = Date.now();
@@ -280,7 +283,7 @@ async function runCodeReview(options) {
280
283
  // If --directory is specified, use that as the project directory
281
284
  // Otherwise, use the current working directory
282
285
  const projectPath = options.directory ? path.resolve(options.directory) : process.cwd();
283
- console.log(chalk.gray(`Using project path for analysis: ${projectPath}`));
286
+ diagnosticLog(chalk.gray(`Using project path for analysis: ${projectPath}`));
284
287
 
285
288
  // Parse custom documents
286
289
  const customDocs = [];
@@ -321,12 +324,12 @@ async function runCodeReview(options) {
321
324
  .trim(); // Remove leading/trailing whitespace
322
325
 
323
326
  customDocs.push({ title, content });
324
- console.log(chalk.gray(`Loaded custom document '${title}' from ${filePath} (${content.length} chars)`));
327
+ diagnosticLog(chalk.gray(`Loaded custom document '${title}' from ${filePath} (${content.length} chars)`));
325
328
 
326
329
  // Debug: Show a clean preview of the content
327
330
  const previewLength = 200;
328
331
  const preview = content.substring(0, previewLength).replace(/\n/g, '\\n');
329
- console.log(chalk.gray(` Content preview: ${preview}${content.length > previewLength ? '...' : ''}`));
332
+ diagnosticLog(chalk.gray(` Content preview: ${preview}${content.length > previewLength ? '...' : ''}`));
330
333
  }
331
334
  }
332
335
 
@@ -351,7 +354,7 @@ async function runCodeReview(options) {
351
354
  };
352
355
 
353
356
  try {
354
- console.log(chalk.bold.blue('AI Code Review (RAG Approach) - Starting analysis...'));
357
+ diagnosticLog(chalk.bold.blue('AI Code Review (RAG Approach) - Starting analysis...'));
355
358
 
356
359
  // Determine the review mode based on options
357
360
  // Only support: single file, specific files, or diff with branch
@@ -360,7 +363,9 @@ async function runCodeReview(options) {
360
363
  const gitWorkingDir = options.directory ? path.resolve(options.directory) : process.cwd();
361
364
  const changedFiles = getChangedFiles(options.diffWith, gitWorkingDir);
362
365
  if (changedFiles.length === 0) {
363
- console.log(chalk.yellow(`No changed files found compared to branch '${options.diffWith}'. Exiting.`));
366
+ const message = `No changed files found compared to branch '${options.diffWith}'. Exiting.`;
367
+ diagnosticLog(chalk.yellow(message));
368
+ await emitEmptyReviewOutputIfNeeded(options, { success: true, results: [], message });
364
369
  return;
365
370
  }
366
371
  operationDescription = `${changedFiles.length} files changed vs ${options.diffWith}`;
@@ -382,7 +387,9 @@ async function runCodeReview(options) {
382
387
  else if (options.files && options.files.length > 0) {
383
388
  const filesToAnalyze = await expandFilePatterns(options.files);
384
389
  if (filesToAnalyze.length === 0) {
385
- console.log(chalk.yellow('No files found matching the specified patterns. Exiting.'));
390
+ const message = 'No files found matching the specified patterns. Exiting.';
391
+ diagnosticLog(chalk.yellow(message));
392
+ await emitEmptyReviewOutputIfNeeded(options, { success: true, results: [], message });
386
393
  return;
387
394
  }
388
395
  operationDescription = `${filesToAnalyze.length} specific files/patterns`;
@@ -407,7 +414,7 @@ async function runCodeReview(options) {
407
414
  process.exit(1);
408
415
  }
409
416
 
410
- console.log(chalk.cyan(`Starting review for ${operationDescription}...`));
417
+ diagnosticLog(chalk.cyan(`Starting review for ${operationDescription}...`));
411
418
 
412
419
  // Execute the selected review task
413
420
  const reviewResult = await reviewTask;
@@ -420,60 +427,77 @@ async function runCodeReview(options) {
420
427
  // Process and output results
421
428
  if (reviewResult && reviewResult.success) {
422
429
  if (reviewResult.results && reviewResult.results.length > 0) {
423
- console.log(chalk.green(`Found ${reviewResult.results.length} result items to display`));
424
- // Determine output function based on format option
425
- const outputFn = options.output === 'json' ? outputJson : options.output === 'markdown' ? outputMarkdown : outputText;
426
- // Pass the detailed results array to the output function
427
- outputFn(reviewResult.results, options);
428
- console.log(chalk.bold.green(`\nAnalysis complete for ${operationDescription}! (${duration}s)`));
430
+ diagnosticLog(chalk.green(`Found ${reviewResult.results.length} result items to display`));
431
+ await outputReviewResults(reviewResult.results, options, reviewResult);
432
+ diagnosticLog(chalk.bold.green(`\nAnalysis complete for ${operationDescription}! (${duration}s)`));
429
433
  }
430
434
  else {
431
435
  // No results to display (e.g., all files were excluded/skipped)
432
436
  const message = reviewResult.message || 'All files were excluded from review (e.g., config files, lock files).';
433
- console.log(chalk.yellow(message));
437
+ diagnosticLog(chalk.yellow(message));
434
438
 
435
- // Still output empty results if outputFile is specified (for CI/CD pipelines)
436
- if (options.outputFile) {
437
- const outputFn = options.output === 'json' ? outputJson : options.output === 'markdown' ? outputMarkdown : outputText;
438
- outputFn([], options);
439
- console.log(chalk.yellow(`Empty results written to: ${options.outputFile}`));
439
+ if (await emitEmptyReviewOutputIfNeeded(options, reviewResult)) {
440
+ if (options.outputFile) {
441
+ diagnosticLog(chalk.yellow(`Empty results written to: ${options.outputFile}`));
442
+ }
440
443
  }
441
444
 
442
- console.log(chalk.bold.yellow(`\nReview complete for ${operationDescription} - no reviewable files found (${duration}s)`));
445
+ diagnosticLog(chalk.bold.yellow(`\nReview complete for ${operationDescription} - no reviewable files found (${duration}s)`));
443
446
  }
444
447
  }
445
448
  else {
449
+ const error = reviewResult?.error || 'Code review process failed.';
446
450
  console.error(chalk.red('\nCode review process failed.'));
447
- if (reviewResult && reviewResult.error) {
448
- console.error(chalk.red(`Error: ${reviewResult.error}`));
451
+ console.error(chalk.red(`Error: ${error}`));
452
+
453
+ if (await emitErrorReviewOutputIfNeeded(options, error, reviewResult || {})) {
454
+ if (options.outputFile) {
455
+ diagnosticLog(chalk.yellow(`Error results written to: ${options.outputFile}`));
456
+ }
449
457
  }
458
+ // AI review output is advisory; report review-task failures without failing CI.
450
459
  }
451
460
 
452
461
  // Clean up resources
453
- console.log(chalk.cyan('Cleaning up resources...'));
462
+ diagnosticLog(chalk.cyan('Cleaning up resources...'));
454
463
  try {
455
464
  await embeddingsSystem.cleanup();
456
465
  await cleanupClassifier();
457
- console.log(chalk.green('All resources cleaned up successfully'));
466
+ diagnosticLog(chalk.green('All resources cleaned up successfully'));
458
467
  }
459
468
  catch (cleanupErr) {
460
469
  console.error(chalk.yellow('Error during cleanup:'), cleanupErr.message);
461
- process.exit(1);
470
+ process.exitCode = 1;
471
+ return;
462
472
  }
463
473
  }
464
474
  catch (err) {
465
- console.error(chalk.red(`\nError during code review (${operationDescription}):`), err.message);
466
- console.error(err.stack);
475
+ if (!isBrokenStdoutPipeError(err)) {
476
+ console.error(chalk.red(`\nError during code review (${operationDescription}):`), err.message);
477
+ if (shouldPrintErrorStack(options, err)) {
478
+ console.error(err.stack);
479
+ }
480
+ }
481
+
482
+ try {
483
+ await emitErrorReviewOutputIfNeeded(options, err.message, { success: false, error: err.message });
484
+ }
485
+ catch (outputErr) {
486
+ if (!isBrokenStdoutPipeError(outputErr)) {
487
+ console.error(chalk.red('Error writing review error output:'), outputErr.message);
488
+ }
489
+ }
490
+
467
491
  // Clean up resources even on error
468
492
  try {
469
493
  await embeddingsSystem.cleanup();
470
494
  await cleanupClassifier();
471
- console.log(chalk.green('All resources cleaned up successfully'));
495
+ diagnosticLog(chalk.green('All resources cleaned up successfully'));
472
496
  }
473
497
  catch (cleanupErr) {
474
498
  console.error(chalk.red('Error during cleanup:'), cleanupErr.message);
475
499
  }
476
- process.exit(1);
500
+ process.exitCode = 1;
477
501
  }
478
502
  }
479
503
 
@@ -960,7 +984,7 @@ function getChangedFiles(branch, workingDir = process.cwd()) {
960
984
  try {
961
985
  // Get git root directory
962
986
  const gitRoot = execSync('git rev-parse --show-toplevel', { cwd: workingDir }).toString().trim();
963
- console.log(chalk.gray(`Git repository: ${gitRoot}`));
987
+ diagnosticLog(chalk.gray(`Git repository: ${gitRoot}`));
964
988
 
965
989
  // Ensure the branch exists locally (fetch if needed)
966
990
  ensureBranchExists(branch, workingDir);
@@ -977,7 +1001,7 @@ function getChangedFiles(branch, workingDir = process.cwd()) {
977
1001
  // Continue with the original baseBranch name, it might work with remote refs
978
1002
  }
979
1003
 
980
- console.log(chalk.gray(`Comparing ${branch} against ${baseBranch}...`));
1004
+ diagnosticLog(chalk.gray(`Comparing ${branch} against ${baseBranch}...`));
981
1005
 
982
1006
  // Use three-dot notation to get changes in branch compared to base
983
1007
  // This shows commits that are in 'branch' but not in 'baseBranch'
@@ -991,7 +1015,7 @@ function getChangedFiles(branch, workingDir = process.cwd()) {
991
1015
  .map((file) => path.resolve(gitRoot, file)); // Get absolute path
992
1016
 
993
1017
  if (changedFiles.length > 0) {
994
- console.log(chalk.gray(`Found ${changedFiles.length} changed files in ${branch} vs ${baseBranch}`));
1018
+ diagnosticLog(chalk.gray(`Found ${changedFiles.length} changed files in ${branch} vs ${baseBranch}`));
995
1019
  }
996
1020
 
997
1021
  return changedFiles;
@@ -1008,19 +1032,77 @@ function getChangedFiles(branch, workingDir = process.cwd()) {
1008
1032
  // --- Output Formatting Functions --- //
1009
1033
  // These consume the normalized results returned by the RAG review pipeline
1010
1034
 
1035
+ function getReviewOutputFunction(outputFormat) {
1036
+ if (outputFormat === 'json') {
1037
+ return outputJson;
1038
+ }
1039
+ if (outputFormat === 'markdown') {
1040
+ return outputMarkdown;
1041
+ }
1042
+ return outputText;
1043
+ }
1044
+
1045
+ function shouldEmitReviewOutput(options = {}) {
1046
+ return Boolean(options.outputFile || options.output === 'json' || options.output === 'markdown');
1047
+ }
1048
+
1049
+ function shouldPrintErrorStack(options, error) {
1050
+ return Boolean(error?.stack && (isVerboseEnabled(options) || isDebugEnabled()));
1051
+ }
1052
+
1053
+ async function outputReviewResults(reviewResults, options, aggregateResult = {}) {
1054
+ await getReviewOutputFunction(options.output)(reviewResults, options, aggregateResult);
1055
+ }
1056
+
1057
+ async function emitEmptyReviewOutputIfNeeded(options, aggregateResult = {}) {
1058
+ if (!shouldEmitReviewOutput(options)) {
1059
+ return false;
1060
+ }
1061
+
1062
+ await outputReviewResults([], options, aggregateResult);
1063
+ return true;
1064
+ }
1065
+
1066
+ async function emitErrorReviewOutputIfNeeded(options, error, aggregateResult = {}) {
1067
+ if (!shouldEmitReviewOutput(options)) {
1068
+ return false;
1069
+ }
1070
+
1071
+ await outputReviewResults([createReviewErrorResult(error)], options, {
1072
+ ...aggregateResult,
1073
+ success: false,
1074
+ error,
1075
+ });
1076
+ return true;
1077
+ }
1078
+
1079
+ function createReviewErrorResult(error) {
1080
+ return {
1081
+ filePath: 'review',
1082
+ success: false,
1083
+ error,
1084
+ };
1085
+ }
1086
+
1011
1087
  /**
1012
1088
  * Output results in JSON format
1013
1089
  *
1014
1090
  * @param {Array<Object>} reviewResults - Array of individual file review results
1015
1091
  * @param {Object} options - Command line options
1092
+ * @param {Object} aggregateResult - Full review result with PR-level context
1016
1093
  */
1017
- function outputJson(reviewResults, options) {
1094
+ async function outputJson(reviewResults, options, aggregateResult = {}) {
1095
+ const prLevelFindings = collectPRLevelFindings(reviewResults, aggregateResult);
1096
+ const fileLevelIssues = getFileLevelIssueCount(reviewResults);
1097
+
1018
1098
  // Structure the output to be informative
1019
1099
  const output = {
1020
1100
  summary: {
1021
1101
  totalFilesReviewed: reviewResults.length,
1022
1102
  filesWithIssues: reviewResults.filter((r) => r.success && !r.skipped && r.results?.issues?.length > 0).length,
1023
- totalIssues: reviewResults.reduce((sum, r) => sum + (r.results?.issues?.length || 0), 0),
1103
+ totalIssues: fileLevelIssues + prLevelFindings.issues.length,
1104
+ fileLevelIssues,
1105
+ prLevelIssues: prLevelFindings.issues.length,
1024
1106
  issuesWithCodeSuggestions: reviewResults.reduce((sum, r) => {
1025
1107
  if (!r.success || r.skipped || !r.results?.issues) {
1026
1108
  return sum;
@@ -1049,6 +1131,10 @@ function outputJson(reviewResults, options) {
1049
1131
  }),
1050
1132
  };
1051
1133
 
1134
+ if (hasPRLevelFindings(prLevelFindings)) {
1135
+ output.prLevelFindings = prLevelFindings;
1136
+ }
1137
+
1052
1138
  const jsonOutput = JSON.stringify(output, null, 2);
1053
1139
 
1054
1140
  // If output-file is specified, write to file instead of stdout
@@ -1058,7 +1144,7 @@ function outputJson(reviewResults, options) {
1058
1144
  }
1059
1145
  else {
1060
1146
  // Write JSON output to stdout (process.stdout is not buffered)
1061
- process.stdout.write(jsonOutput);
1147
+ await writeStdout(jsonOutput);
1062
1148
  }
1063
1149
  }
1064
1150
 
@@ -1067,13 +1153,15 @@ function outputJson(reviewResults, options) {
1067
1153
  *
1068
1154
  * @param {Array<Object>} reviewResults - Array of individual file review results
1069
1155
  * @param {Object} options - Command line options
1156
+ * @param {Object} aggregateResult - Full review result with PR-level context
1070
1157
  */
1071
- function outputMarkdown(reviewResults, options) {
1158
+ async function outputMarkdown(reviewResults, options, aggregateResult = {}) {
1072
1159
  const totalFiles = reviewResults.length;
1073
1160
  const filesWithIssues = reviewResults.filter((r) => r.success && !r.skipped && r.results?.issues?.length > 0).length;
1074
- const totalIssues = reviewResults.reduce((sum, r) => sum + (r.results?.issues?.length || 0), 0);
1075
1161
  const skippedFiles = reviewResults.filter((r) => r.skipped).length;
1076
1162
  const errorFiles = reviewResults.filter((r) => !r.success).length;
1163
+ const prLevelFindings = collectPRLevelFindings(reviewResults, aggregateResult);
1164
+ const totalIssues = getFileLevelIssueCount(reviewResults) + prLevelFindings.issues.length;
1077
1165
 
1078
1166
  const lines = [
1079
1167
  '# AI Code Review Results (RAG Approach)',
@@ -1092,6 +1180,8 @@ function outputMarkdown(reviewResults, options) {
1092
1180
  lines.push(`- **Errors:** ${errorFiles}`);
1093
1181
  }
1094
1182
 
1183
+ appendPRLevelFindingsMarkdown(lines, prLevelFindings);
1184
+
1095
1185
  lines.push('', '## Detailed Review per File', '');
1096
1186
 
1097
1187
  reviewResults.forEach((fileResult) => {
@@ -1149,7 +1239,7 @@ function outputMarkdown(reviewResults, options) {
1149
1239
  return;
1150
1240
  }
1151
1241
 
1152
- process.stdout.write(markdownOutput);
1242
+ await writeStdout(markdownOutput);
1153
1243
  }
1154
1244
 
1155
1245
  /**
@@ -1157,13 +1247,15 @@ function outputMarkdown(reviewResults, options) {
1157
1247
  *
1158
1248
  * @param {Array<Object>} reviewResults - Array of individual file review results
1159
1249
  * @param {Object} cliOptions - Command line options
1250
+ * @param {Object} aggregateResult - Full review result with PR-level context
1160
1251
  */
1161
- function outputText(reviewResults, cliOptions) {
1252
+ function outputText(reviewResults, cliOptions, aggregateResult = {}) {
1162
1253
  const totalFiles = reviewResults.length;
1163
1254
  const filesWithIssues = reviewResults.filter((r) => r.success && !r.skipped && r.results?.issues?.length > 0).length;
1164
- const totalIssues = reviewResults.reduce((sum, r) => sum + (r.results?.issues?.length || 0), 0);
1165
1255
  const skippedFiles = reviewResults.filter((r) => r.skipped).length;
1166
1256
  const errorFiles = reviewResults.filter((r) => !r.success).length;
1257
+ const prLevelFindings = collectPRLevelFindings(reviewResults, aggregateResult);
1258
+ const totalIssues = getFileLevelIssueCount(reviewResults) + prLevelFindings.issues.length;
1167
1259
 
1168
1260
  console.log(chalk.bold.blue('\n===== AI Code Review Summary ====='));
1169
1261
  console.log(`Files Analyzed: ${chalk.bold(totalFiles)}`);
@@ -1177,6 +1269,8 @@ function outputText(reviewResults, cliOptions) {
1177
1269
  }
1178
1270
  console.log(chalk.bold.blue('================================================'));
1179
1271
 
1272
+ outputPRLevelFindingsText(prLevelFindings);
1273
+
1180
1274
  reviewResults.forEach((fileResult) => {
1181
1275
  if (!fileResult.success) {
1182
1276
  console.log(chalk.bold.red(`\n===== Error reviewing ${fileResult.filePath} =====`));
@@ -1230,6 +1324,68 @@ function outputText(reviewResults, cliOptions) {
1230
1324
  });
1231
1325
  }
1232
1326
 
1327
+ function appendPRLevelFindingsMarkdown(lines, prLevelFindings) {
1328
+ if (!hasPRLevelFindings(prLevelFindings)) {
1329
+ return;
1330
+ }
1331
+
1332
+ lines.push('', '## PR-Level Findings', '');
1333
+
1334
+ if (prLevelFindings.summary) {
1335
+ lines.push(`**Summary:** ${prLevelFindings.summary}`, '');
1336
+ }
1337
+
1338
+ for (const issue of prLevelFindings.issues) {
1339
+ const severity = issue.severity || 'info';
1340
+ const severityEmoji = getSeverityEmoji(severity);
1341
+ lines.push(`- **[${severity.toUpperCase()}] ${severityEmoji}** ${issue.description}`);
1342
+ if (issue.files?.length > 0) {
1343
+ lines.push(` - Affected files: ${issue.files.join(', ')}`);
1344
+ }
1345
+ if (issue.suggestion) {
1346
+ lines.push(` - Suggestion: ${issue.suggestion}`);
1347
+ }
1348
+ }
1349
+
1350
+ if (prLevelFindings.recommendations.length > 0) {
1351
+ lines.push('', '**Recommendations:**', '');
1352
+ for (const recommendation of prLevelFindings.recommendations) {
1353
+ lines.push(`- ${recommendation}`);
1354
+ }
1355
+ }
1356
+ }
1357
+
1358
+ function outputPRLevelFindingsText(prLevelFindings) {
1359
+ if (!hasPRLevelFindings(prLevelFindings)) {
1360
+ return;
1361
+ }
1362
+
1363
+ console.log(chalk.bold.underline('\n===== PR-Level Findings ====='));
1364
+ if (prLevelFindings.summary) {
1365
+ console.log(chalk.bold.cyan(`Summary: ${prLevelFindings.summary}`));
1366
+ }
1367
+
1368
+ for (const issue of prLevelFindings.issues) {
1369
+ const severity = issue.severity || 'info';
1370
+ const severityColor = getSeverityColor(severity);
1371
+ console.log(` ${severityColor(`[${severity.toUpperCase()}]`)} ${issue.description}`);
1372
+ if (issue.files?.length > 0) {
1373
+ console.log(` Affected files: ${issue.files.join(', ')}`);
1374
+ }
1375
+ if (issue.suggestion) {
1376
+ console.log(` ${chalk.green(`Suggestion: ${issue.suggestion}`)}`);
1377
+ }
1378
+ console.log('');
1379
+ }
1380
+
1381
+ if (prLevelFindings.recommendations.length > 0) {
1382
+ console.log(chalk.bold.yellow('Recommendations:'));
1383
+ for (const recommendation of prLevelFindings.recommendations) {
1384
+ console.log(` - ${recommendation}`);
1385
+ }
1386
+ }
1387
+ }
1388
+
1233
1389
  // --- Severity Helpers (Remain Unchanged) --- //
1234
1390
 
1235
1391
  /**
package/src/llm.js CHANGED
@@ -18,7 +18,7 @@ import dotenv from 'dotenv';
18
18
  import { verboseLog } from './utils/logging.js';
19
19
 
20
20
  // Load env variables if present; do not enforce key at import time
21
- dotenv.config();
21
+ dotenv.config({ quiet: true });
22
22
 
23
23
  let anthropic = null;
24
24