codecritique 1.2.4 → 2.0.1
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.
- package/README.md +3 -3
- package/package.json +22 -20
- package/src/content-retrieval.js +38 -26
- package/src/custom-documents.js +29 -14
- package/src/feedback-loader.js +19 -8
- package/src/index.js +95 -45
- package/src/llm.js +7 -3
- package/src/project-analyzer.js +56 -31
- package/src/project-analyzer.test.js +8 -0
- package/src/rag-analyzer.js +70 -34
- package/src/rag-analyzer.test.js +3 -1
- package/src/rag-review.js +14 -7
- package/src/zero-shot-classifier-open.js +16 -7
package/src/index.js
CHANGED
|
@@ -103,11 +103,13 @@ program
|
|
|
103
103
|
await embeddingsSystem.clearAllEmbeddings();
|
|
104
104
|
console.log(chalk.green('All embeddings have been cleared.'));
|
|
105
105
|
await embeddingsSystem.databaseManager.cleanup();
|
|
106
|
-
}
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
107
108
|
console.error(chalk.red('Error clearing all embeddings:'), err.message);
|
|
108
109
|
try {
|
|
109
110
|
await embeddingsSystem.databaseManager.cleanup();
|
|
110
|
-
}
|
|
111
|
+
}
|
|
112
|
+
catch (cleanupErr) {
|
|
111
113
|
console.error(chalk.red('Error during cleanup:'), cleanupErr.message);
|
|
112
114
|
}
|
|
113
115
|
process.exit(1);
|
|
@@ -203,7 +205,8 @@ const hasCommand = process.argv
|
|
|
203
205
|
if (!hasCommand && process.argv.length > 2) {
|
|
204
206
|
// If no command is specified but there are arguments, default to 'analyze'
|
|
205
207
|
program.parse(['node', 'index.js', 'analyze', ...process.argv.slice(2)]);
|
|
206
|
-
}
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
207
210
|
program.parse();
|
|
208
211
|
}
|
|
209
212
|
|
|
@@ -223,7 +226,8 @@ process.on('SIGINT', async () => {
|
|
|
223
226
|
clearTimeout(forceExitTimeout); // Cleanup finished, clear the timeout
|
|
224
227
|
console.log(chalk.cyan('SIGINT handler: Exiting normally (code 0).'));
|
|
225
228
|
process.exit(0); // Exit normally
|
|
226
|
-
}
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
227
231
|
console.error(chalk.red('Error during embeddingsSystem.cleanup():'), err.message);
|
|
228
232
|
clearTimeout(forceExitTimeout);
|
|
229
233
|
console.log(chalk.cyan('SIGINT handler: Exiting after error (code 1).'));
|
|
@@ -246,7 +250,8 @@ process.on('SIGTERM', async () => {
|
|
|
246
250
|
clearTimeout(forceExitTimeout); // Cleanup finished, clear the timeout
|
|
247
251
|
console.log(chalk.cyan('SIGTERM handler: Exiting normally (code 0).'));
|
|
248
252
|
process.exit(0); // Exit normally
|
|
249
|
-
}
|
|
253
|
+
}
|
|
254
|
+
catch (err) {
|
|
250
255
|
console.error(chalk.red('Error during embeddingsSystem.cleanup():'), err.message);
|
|
251
256
|
clearTimeout(forceExitTimeout);
|
|
252
257
|
console.log(chalk.cyan('SIGTERM handler: Exiting after error (code 1).'));
|
|
@@ -366,13 +371,15 @@ async function runCodeReview(options) {
|
|
|
366
371
|
diffWith: options.diffWith,
|
|
367
372
|
};
|
|
368
373
|
reviewTask = reviewPullRequest(changedFiles, enhancedReviewOptions);
|
|
369
|
-
}
|
|
374
|
+
}
|
|
375
|
+
else if (options.file) {
|
|
370
376
|
operationDescription = `single file: ${options.file}`;
|
|
371
377
|
if (!fs.existsSync(options.file)) {
|
|
372
378
|
throw new Error(`File not found: ${options.file}`);
|
|
373
379
|
}
|
|
374
380
|
reviewTask = reviewFile(options.file, reviewOptions);
|
|
375
|
-
}
|
|
381
|
+
}
|
|
382
|
+
else if (options.files && options.files.length > 0) {
|
|
376
383
|
const filesToAnalyze = await expandFilePatterns(options.files);
|
|
377
384
|
if (filesToAnalyze.length === 0) {
|
|
378
385
|
console.log(chalk.yellow('No files found matching the specified patterns. Exiting.'));
|
|
@@ -380,7 +387,8 @@ async function runCodeReview(options) {
|
|
|
380
387
|
}
|
|
381
388
|
operationDescription = `${filesToAnalyze.length} specific files/patterns`;
|
|
382
389
|
reviewTask = reviewFiles(filesToAnalyze, reviewOptions);
|
|
383
|
-
}
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
384
392
|
// No valid options provided - show error and exit
|
|
385
393
|
console.error(chalk.red('Error: You must specify one of the following:'));
|
|
386
394
|
console.error(chalk.yellow(' --file <file> Analyze a single file'));
|
|
@@ -418,7 +426,8 @@ async function runCodeReview(options) {
|
|
|
418
426
|
// Pass the detailed results array to the output function
|
|
419
427
|
outputFn(reviewResult.results, options);
|
|
420
428
|
console.log(chalk.bold.green(`\nAnalysis complete for ${operationDescription}! (${duration}s)`));
|
|
421
|
-
}
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
422
431
|
// No results to display (e.g., all files were excluded/skipped)
|
|
423
432
|
const message = reviewResult.message || 'All files were excluded from review (e.g., config files, lock files).';
|
|
424
433
|
console.log(chalk.yellow(message));
|
|
@@ -432,7 +441,8 @@ async function runCodeReview(options) {
|
|
|
432
441
|
|
|
433
442
|
console.log(chalk.bold.yellow(`\nReview complete for ${operationDescription} - no reviewable files found (${duration}s)`));
|
|
434
443
|
}
|
|
435
|
-
}
|
|
444
|
+
}
|
|
445
|
+
else {
|
|
436
446
|
console.error(chalk.red('\nCode review process failed.'));
|
|
437
447
|
if (reviewResult && reviewResult.error) {
|
|
438
448
|
console.error(chalk.red(`Error: ${reviewResult.error}`));
|
|
@@ -445,11 +455,13 @@ async function runCodeReview(options) {
|
|
|
445
455
|
await embeddingsSystem.cleanup();
|
|
446
456
|
await cleanupClassifier();
|
|
447
457
|
console.log(chalk.green('All resources cleaned up successfully'));
|
|
448
|
-
}
|
|
458
|
+
}
|
|
459
|
+
catch (cleanupErr) {
|
|
449
460
|
console.error(chalk.yellow('Error during cleanup:'), cleanupErr.message);
|
|
450
461
|
process.exit(1);
|
|
451
462
|
}
|
|
452
|
-
}
|
|
463
|
+
}
|
|
464
|
+
catch (err) {
|
|
453
465
|
console.error(chalk.red(`\nError during code review (${operationDescription}):`), err.message);
|
|
454
466
|
console.error(err.stack);
|
|
455
467
|
// Clean up resources even on error
|
|
@@ -457,7 +469,8 @@ async function runCodeReview(options) {
|
|
|
457
469
|
await embeddingsSystem.cleanup();
|
|
458
470
|
await cleanupClassifier();
|
|
459
471
|
console.log(chalk.green('All resources cleaned up successfully'));
|
|
460
|
-
}
|
|
472
|
+
}
|
|
473
|
+
catch (cleanupErr) {
|
|
461
474
|
console.error(chalk.red('Error during cleanup:'), cleanupErr.message);
|
|
462
475
|
}
|
|
463
476
|
process.exit(1);
|
|
@@ -500,7 +513,8 @@ async function generateEmbeddings(options) {
|
|
|
500
513
|
.map((line) => line.trim())
|
|
501
514
|
.filter((line) => line && !line.startsWith('#'));
|
|
502
515
|
excludePatterns = [...excludePatterns, ...filePatterns];
|
|
503
|
-
}
|
|
516
|
+
}
|
|
517
|
+
else {
|
|
504
518
|
console.warn(chalk.yellow(`Exclude file not found: ${excludeFilePath}`));
|
|
505
519
|
}
|
|
506
520
|
}
|
|
@@ -512,7 +526,8 @@ async function generateEmbeddings(options) {
|
|
|
512
526
|
// Log gitignore status
|
|
513
527
|
if (options.gitignore === false) {
|
|
514
528
|
console.log(chalk.yellow('Automatic .gitignore exclusion is disabled.'));
|
|
515
|
-
}
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
516
531
|
console.log(chalk.cyan('Respecting .gitignore patterns (if present).'));
|
|
517
532
|
}
|
|
518
533
|
console.log(chalk.green('Exclusion pattern processing complete.'));
|
|
@@ -525,7 +540,8 @@ async function generateEmbeddings(options) {
|
|
|
525
540
|
console.log(chalk.cyan('Processing specified files/patterns...'));
|
|
526
541
|
filesToProcess = await expandFilePatterns(options.files, baseDir);
|
|
527
542
|
console.log(chalk.green(`Expanded specified files/patterns to ${filesToProcess.length} files.`));
|
|
528
|
-
}
|
|
543
|
+
}
|
|
544
|
+
else {
|
|
529
545
|
console.log(chalk.cyan(`Scanning directory for supported files: ${baseDir}`));
|
|
530
546
|
// Show spinner during file discovery
|
|
531
547
|
const scanSpinner = new Spinner('Scanning files... %s');
|
|
@@ -591,11 +607,14 @@ async function generateEmbeddings(options) {
|
|
|
591
607
|
// Update counters based on status
|
|
592
608
|
if (status === 'processed') {
|
|
593
609
|
processedCount++;
|
|
594
|
-
}
|
|
610
|
+
}
|
|
611
|
+
else if (status === 'skipped') {
|
|
595
612
|
skippedCount++;
|
|
596
|
-
}
|
|
613
|
+
}
|
|
614
|
+
else if (status === 'failed') {
|
|
597
615
|
failedCount++;
|
|
598
|
-
}
|
|
616
|
+
}
|
|
617
|
+
else if (status === 'excluded') {
|
|
599
618
|
excludedCount++;
|
|
600
619
|
}
|
|
601
620
|
|
|
@@ -603,7 +622,8 @@ async function generateEmbeddings(options) {
|
|
|
603
622
|
updateSpinner();
|
|
604
623
|
},
|
|
605
624
|
});
|
|
606
|
-
}
|
|
625
|
+
}
|
|
626
|
+
finally {
|
|
607
627
|
// Clean up the progress display even if embedding generation fails.
|
|
608
628
|
clearInterval(progressInterval);
|
|
609
629
|
spinner.stop(true);
|
|
@@ -636,7 +656,8 @@ async function generateEmbeddings(options) {
|
|
|
636
656
|
)
|
|
637
657
|
);
|
|
638
658
|
verboseLog(options, chalk.gray(` Key patterns: ${projectSummary.keyPatterns.length}`));
|
|
639
|
-
}
|
|
659
|
+
}
|
|
660
|
+
catch (error) {
|
|
640
661
|
console.error(chalk.red('⚠️ Project analysis failed but continuing:'), error.message);
|
|
641
662
|
}
|
|
642
663
|
|
|
@@ -644,7 +665,8 @@ async function generateEmbeddings(options) {
|
|
|
644
665
|
console.log(chalk.cyan('Cleaning up resources...'));
|
|
645
666
|
await embeddingsSystem.cleanup();
|
|
646
667
|
console.log(chalk.green('Cleanup successful.'));
|
|
647
|
-
}
|
|
668
|
+
}
|
|
669
|
+
catch (err) {
|
|
648
670
|
console.error(chalk.red('Error generating embeddings:'), err.message);
|
|
649
671
|
console.error(err.stack);
|
|
650
672
|
// Clean up resources even on error
|
|
@@ -652,7 +674,8 @@ async function generateEmbeddings(options) {
|
|
|
652
674
|
console.log(chalk.cyan('Cleaning up resources after error...'));
|
|
653
675
|
await embeddingsSystem.cleanup();
|
|
654
676
|
console.log(chalk.green('Cleanup successful.'));
|
|
655
|
-
}
|
|
677
|
+
}
|
|
678
|
+
catch (cleanupErr) {
|
|
656
679
|
console.error(chalk.red('Error during cleanup:'), cleanupErr.message);
|
|
657
680
|
}
|
|
658
681
|
process.exit(1);
|
|
@@ -678,13 +701,15 @@ async function clearEmbeddings(options) {
|
|
|
678
701
|
// Clean up resources (only database connection since we skipped full initialization)
|
|
679
702
|
console.log(chalk.cyan('Cleaning up resources...'));
|
|
680
703
|
await embeddingsSystem.databaseManager.cleanup();
|
|
681
|
-
}
|
|
704
|
+
}
|
|
705
|
+
catch (err) {
|
|
682
706
|
console.error(chalk.red('Error clearing embeddings:'), err.message);
|
|
683
707
|
console.error(err.stack);
|
|
684
708
|
// Clean up resources even on error (only database connection)
|
|
685
709
|
try {
|
|
686
710
|
await embeddingsSystem.databaseManager.cleanup();
|
|
687
|
-
}
|
|
711
|
+
}
|
|
712
|
+
catch (cleanupErr) {
|
|
688
713
|
console.error(chalk.red('Error during cleanup:'), cleanupErr.message);
|
|
689
714
|
}
|
|
690
715
|
process.exit(1);
|
|
@@ -703,7 +728,8 @@ async function showEmbeddingStats(options) {
|
|
|
703
728
|
|
|
704
729
|
if (options.directory) {
|
|
705
730
|
console.log(chalk.cyan(`Fetching embedding statistics for project: ${projectDir}`));
|
|
706
|
-
}
|
|
731
|
+
}
|
|
732
|
+
else {
|
|
707
733
|
console.log(chalk.cyan('Fetching embedding statistics for all projects...'));
|
|
708
734
|
}
|
|
709
735
|
|
|
@@ -714,7 +740,8 @@ async function showEmbeddingStats(options) {
|
|
|
714
740
|
|
|
715
741
|
if (!stats || Object.keys(stats).length === 0 || stats.totalCount === 0) {
|
|
716
742
|
console.log(chalk.yellow('No embeddings found or database is empty.'));
|
|
717
|
-
}
|
|
743
|
+
}
|
|
744
|
+
else {
|
|
718
745
|
console.log(` ${chalk.cyan('Total Embeddings:')} ${chalk.green(stats.totalCount)}`);
|
|
719
746
|
if (stats.dimensions) {
|
|
720
747
|
console.log(` ${chalk.cyan('Vector Dimensions:')} ${chalk.green(stats.dimensions)}`);
|
|
@@ -733,7 +760,8 @@ async function showEmbeddingStats(options) {
|
|
|
733
760
|
// Clean up resources
|
|
734
761
|
// console.log(chalk.cyan('Cleaning up resources...'));
|
|
735
762
|
// await embeddingsSystem.cleanup();
|
|
736
|
-
}
|
|
763
|
+
}
|
|
764
|
+
catch (err) {
|
|
737
765
|
console.error(chalk.red('Error fetching embedding statistics:'), err.message);
|
|
738
766
|
console.error(err.stack);
|
|
739
767
|
// Clean up resources even on error
|
|
@@ -869,10 +897,12 @@ async function findSupportedFiles(directory, options = {}) {
|
|
|
869
897
|
// Add log after the filtering loop (now just assignment)
|
|
870
898
|
verboseLog(options, chalk.green(`Finished filtering glob results. ${finalFiles.length} files remain.`));
|
|
871
899
|
return finalFiles;
|
|
872
|
-
}
|
|
900
|
+
}
|
|
901
|
+
catch (err) {
|
|
873
902
|
if (err.name === 'AbortError') {
|
|
874
903
|
console.error(chalk.red('Glob operation timed out. The directory might be too large or complex.'));
|
|
875
|
-
}
|
|
904
|
+
}
|
|
905
|
+
else {
|
|
876
906
|
console.error(chalk.red(`Error during glob file search: ${err.message}`));
|
|
877
907
|
}
|
|
878
908
|
console.error(err.stack); // Log stack for debugging
|
|
@@ -897,7 +927,8 @@ async function expandFilePatterns(patterns, baseDir = process.cwd()) {
|
|
|
897
927
|
// Check if it's a direct file path first
|
|
898
928
|
if (fs.existsSync(absolutePattern) && fs.statSync(absolutePattern).isFile()) {
|
|
899
929
|
files.add(absolutePattern);
|
|
900
|
-
}
|
|
930
|
+
}
|
|
931
|
+
else {
|
|
901
932
|
// Treat as a glob pattern
|
|
902
933
|
// Use the original pattern with baseDir as cwd for correct globbing
|
|
903
934
|
const matchedFiles = await glob.glob(pattern, { cwd: baseDir, absolute: true, nodir: true });
|
|
@@ -910,7 +941,8 @@ async function expandFilePatterns(patterns, baseDir = process.cwd()) {
|
|
|
910
941
|
}
|
|
911
942
|
}
|
|
912
943
|
return Array.from(files);
|
|
913
|
-
}
|
|
944
|
+
}
|
|
945
|
+
catch (err) {
|
|
914
946
|
console.error(chalk.red('Error expanding file patterns:'), err.message);
|
|
915
947
|
return [];
|
|
916
948
|
}
|
|
@@ -939,7 +971,8 @@ function getChangedFiles(branch, workingDir = process.cwd()) {
|
|
|
939
971
|
// Ensure the base branch exists locally as well (crucial for diff operations)
|
|
940
972
|
try {
|
|
941
973
|
ensureBranchExists(baseBranch, workingDir);
|
|
942
|
-
}
|
|
974
|
+
}
|
|
975
|
+
catch (error) {
|
|
943
976
|
console.warn(chalk.yellow(`Warning: Could not ensure base branch '${baseBranch}' exists locally: ${error.message}`));
|
|
944
977
|
// Continue with the original baseBranch name, it might work with remote refs
|
|
945
978
|
}
|
|
@@ -962,7 +995,8 @@ function getChangedFiles(branch, workingDir = process.cwd()) {
|
|
|
962
995
|
}
|
|
963
996
|
|
|
964
997
|
return changedFiles;
|
|
965
|
-
}
|
|
998
|
+
}
|
|
999
|
+
catch (err) {
|
|
966
1000
|
console.error(chalk.red('Error getting git diff:'), err.message);
|
|
967
1001
|
return [];
|
|
968
1002
|
}
|
|
@@ -988,7 +1022,9 @@ function outputJson(reviewResults, options) {
|
|
|
988
1022
|
filesWithIssues: reviewResults.filter((r) => r.success && !r.skipped && r.results?.issues?.length > 0).length,
|
|
989
1023
|
totalIssues: reviewResults.reduce((sum, r) => sum + (r.results?.issues?.length || 0), 0),
|
|
990
1024
|
issuesWithCodeSuggestions: reviewResults.reduce((sum, r) => {
|
|
991
|
-
if (!r.success || r.skipped || !r.results?.issues)
|
|
1025
|
+
if (!r.success || r.skipped || !r.results?.issues) {
|
|
1026
|
+
return sum;
|
|
1027
|
+
}
|
|
992
1028
|
return sum + r.results.issues.filter((issue) => issue.codeSuggestion).length;
|
|
993
1029
|
}, 0),
|
|
994
1030
|
skippedFiles: reviewResults.filter((r) => r.skipped).length,
|
|
@@ -1019,7 +1055,8 @@ function outputJson(reviewResults, options) {
|
|
|
1019
1055
|
if (options && options.outputFile) {
|
|
1020
1056
|
fs.writeFileSync(options.outputFile, jsonOutput, 'utf8');
|
|
1021
1057
|
console.log(chalk.green(`JSON output saved to: ${options.outputFile}`));
|
|
1022
|
-
}
|
|
1058
|
+
}
|
|
1059
|
+
else {
|
|
1023
1060
|
// Write JSON output to stdout (process.stdout is not buffered)
|
|
1024
1061
|
process.stdout.write(jsonOutput);
|
|
1025
1062
|
}
|
|
@@ -1048,8 +1085,12 @@ function outputMarkdown(reviewResults, options) {
|
|
|
1048
1085
|
`- **Total Issues Found:** ${totalIssues}`,
|
|
1049
1086
|
];
|
|
1050
1087
|
|
|
1051
|
-
if (skippedFiles > 0)
|
|
1052
|
-
|
|
1088
|
+
if (skippedFiles > 0) {
|
|
1089
|
+
lines.push(`- **Files Skipped:** ${skippedFiles}`);
|
|
1090
|
+
}
|
|
1091
|
+
if (errorFiles > 0) {
|
|
1092
|
+
lines.push(`- **Errors:** ${errorFiles}`);
|
|
1093
|
+
}
|
|
1053
1094
|
|
|
1054
1095
|
lines.push('', '## Detailed Review per File', '');
|
|
1055
1096
|
|
|
@@ -1128,8 +1169,12 @@ function outputText(reviewResults, cliOptions) {
|
|
|
1128
1169
|
console.log(`Files Analyzed: ${chalk.bold(totalFiles)}`);
|
|
1129
1170
|
console.log(`Files with Issues: ${chalk.bold(filesWithIssues)}`);
|
|
1130
1171
|
console.log(`Total Issues Found: ${chalk.bold(totalIssues)}`);
|
|
1131
|
-
if (skippedFiles > 0)
|
|
1132
|
-
|
|
1172
|
+
if (skippedFiles > 0) {
|
|
1173
|
+
console.log(`Files Skipped: ${chalk.yellow(skippedFiles)}`);
|
|
1174
|
+
}
|
|
1175
|
+
if (errorFiles > 0) {
|
|
1176
|
+
console.log(`Errors: ${chalk.red(errorFiles)}`);
|
|
1177
|
+
}
|
|
1133
1178
|
console.log(chalk.bold.blue('================================================'));
|
|
1134
1179
|
|
|
1135
1180
|
reviewResults.forEach((fileResult) => {
|
|
@@ -1289,7 +1334,8 @@ async function analyzePRHistory(options) {
|
|
|
1289
1334
|
// Display results using utility function
|
|
1290
1335
|
displayAnalysisResults(results, duration);
|
|
1291
1336
|
console.log(chalk.bold.green(`\nPR history analysis complete for ${repository}!`));
|
|
1292
|
-
}
|
|
1337
|
+
}
|
|
1338
|
+
catch (error) {
|
|
1293
1339
|
const endTime = Date.now();
|
|
1294
1340
|
const duration = ((endTime - startTime) / 1000).toFixed(2);
|
|
1295
1341
|
console.error(chalk.red(`\nError during PR history analysis (${duration}s):`), error.message);
|
|
@@ -1323,10 +1369,12 @@ async function getPRHistoryStatus(options) {
|
|
|
1323
1369
|
if (hasComments) {
|
|
1324
1370
|
const stats = await getPRCommentsStats(repository, projectPath);
|
|
1325
1371
|
displayDatabaseStats(stats, hasComments);
|
|
1326
|
-
}
|
|
1372
|
+
}
|
|
1373
|
+
else {
|
|
1327
1374
|
displayDatabaseStats(null, hasComments);
|
|
1328
1375
|
}
|
|
1329
|
-
}
|
|
1376
|
+
}
|
|
1377
|
+
catch (error) {
|
|
1330
1378
|
console.error(chalk.red('Error getting PR history status:'), error.message);
|
|
1331
1379
|
process.exit(1);
|
|
1332
1380
|
}
|
|
@@ -1387,10 +1435,12 @@ async function clearPRHistory(options) {
|
|
|
1387
1435
|
|
|
1388
1436
|
if (cleared) {
|
|
1389
1437
|
console.log(chalk.bold.green(`\nPR analysis data cleared successfully for ${repository}`));
|
|
1390
|
-
}
|
|
1438
|
+
}
|
|
1439
|
+
else {
|
|
1391
1440
|
console.log(chalk.yellow('No data was found to clear.'));
|
|
1392
1441
|
}
|
|
1393
|
-
}
|
|
1442
|
+
}
|
|
1443
|
+
catch (error) {
|
|
1394
1444
|
console.error(chalk.red('Error clearing PR history data:'), error.message);
|
|
1395
1445
|
verboseLog(options, error.stack);
|
|
1396
1446
|
process.exit(1);
|
package/src/llm.js
CHANGED
|
@@ -27,7 +27,9 @@ let anthropic = null;
|
|
|
27
27
|
* @returns {Anthropic} The Anthropic client
|
|
28
28
|
*/
|
|
29
29
|
function getAnthropicClient() {
|
|
30
|
-
if (anthropic)
|
|
30
|
+
if (anthropic) {
|
|
31
|
+
return anthropic;
|
|
32
|
+
}
|
|
31
33
|
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
32
34
|
if (!apiKey) {
|
|
33
35
|
throw new Error('ANTHROPIC_API_KEY is required for analysis. Set it in env or .env before running analyze.');
|
|
@@ -127,14 +129,16 @@ async function sendPromptToClaude(prompt, options = {}) {
|
|
|
127
129
|
usage: response.usage,
|
|
128
130
|
json: toolUse.input,
|
|
129
131
|
};
|
|
130
|
-
}
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
131
134
|
return {
|
|
132
135
|
content: response.content[0]?.text || '',
|
|
133
136
|
model: response.model,
|
|
134
137
|
usage: response.usage,
|
|
135
138
|
};
|
|
136
139
|
}
|
|
137
|
-
}
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
138
142
|
console.error(chalk.red(`Error sending prompt to Claude: ${error.message}`));
|
|
139
143
|
throw error;
|
|
140
144
|
}
|
package/src/project-analyzer.js
CHANGED
|
@@ -14,6 +14,7 @@ import * as llm from './llm.js';
|
|
|
14
14
|
import { FILE_SELECTION_SYSTEM_PROMPT, PROJECT_SUMMARY_SYSTEM_PROMPT } from './prompt-cache.js';
|
|
15
15
|
import { isDocumentationFile, isTestFile } from './utils/file-validation.js';
|
|
16
16
|
import { verboseLog } from './utils/logging.js';
|
|
17
|
+
import { escapeSqlString } from './utils/string-utils.js';
|
|
17
18
|
|
|
18
19
|
// Consolidated file classification configuration
|
|
19
20
|
const FILE_PATTERNS = {
|
|
@@ -212,7 +213,8 @@ export class ProjectAnalyzer {
|
|
|
212
213
|
const currentEmbeddingInventoryHash = await this.calculateEmbeddingInventoryHash(projectPath);
|
|
213
214
|
if (existingSummary.embeddingInventoryHash !== currentEmbeddingInventoryHash) {
|
|
214
215
|
verboseLog(verbose, chalk.yellow('🔄 Embedding inventory changed, regenerating analysis...'));
|
|
215
|
-
}
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
216
218
|
const currentHash = await this.calculateKeyFilesHash(existingSummary.keyFiles);
|
|
217
219
|
if (existingSummary.keyFilesHash === currentHash) {
|
|
218
220
|
verboseLog(verbose, chalk.green('✅ Project analysis up-to-date (no key file changes detected)'));
|
|
@@ -220,7 +222,8 @@ export class ProjectAnalyzer {
|
|
|
220
222
|
}
|
|
221
223
|
verboseLog(verbose, chalk.yellow('🔄 Key files changed, regenerating analysis...'));
|
|
222
224
|
}
|
|
223
|
-
}
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
224
227
|
verboseLog(
|
|
225
228
|
verbose,
|
|
226
229
|
chalk.cyan(
|
|
@@ -260,7 +263,8 @@ export class ProjectAnalyzer {
|
|
|
260
263
|
verboseLog(verbose, chalk.gray(` Key files tracked: ${keyFiles.length}`));
|
|
261
264
|
|
|
262
265
|
return projectSummary;
|
|
263
|
-
}
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
264
268
|
console.error(chalk.red('Error analyzing project:'), error.message);
|
|
265
269
|
return this.createFallbackSummary(projectPath);
|
|
266
270
|
}
|
|
@@ -285,7 +289,8 @@ export class ProjectAnalyzer {
|
|
|
285
289
|
return { ...summary, keyFiles };
|
|
286
290
|
}
|
|
287
291
|
return null;
|
|
288
|
-
}
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
289
294
|
console.error(chalk.yellow('Warning: Could not load existing analysis:'), error.message);
|
|
290
295
|
return null;
|
|
291
296
|
}
|
|
@@ -299,7 +304,8 @@ export class ProjectAnalyzer {
|
|
|
299
304
|
const embeddingsSystem = getDefaultEmbeddingsSystem();
|
|
300
305
|
await embeddingsSystem.storeProjectSummary(projectPath, projectSummary);
|
|
301
306
|
verboseLog({}, chalk.green('✅ Project analysis stored in database'));
|
|
302
|
-
}
|
|
307
|
+
}
|
|
308
|
+
catch (error) {
|
|
303
309
|
console.error(chalk.yellow('Warning: Could not store analysis:'), error.message);
|
|
304
310
|
}
|
|
305
311
|
}
|
|
@@ -358,10 +364,12 @@ export class ProjectAnalyzer {
|
|
|
358
364
|
// Optimize table to sync indices with data and prevent TakeExec panics
|
|
359
365
|
try {
|
|
360
366
|
await table.optimize();
|
|
361
|
-
}
|
|
367
|
+
}
|
|
368
|
+
catch (optimizeError) {
|
|
362
369
|
if (optimizeError.message && optimizeError.message.includes('legacy format')) {
|
|
363
370
|
console.warn(chalk.yellow(`Skipping optimization due to legacy index format - will be auto-upgraded during normal operations`));
|
|
364
|
-
}
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
365
373
|
console.warn(chalk.yellow(`Warning: Failed to optimize file embeddings table: ${optimizeError.message}`));
|
|
366
374
|
}
|
|
367
375
|
}
|
|
@@ -370,6 +378,7 @@ export class ProjectAnalyzer {
|
|
|
370
378
|
|
|
371
379
|
try {
|
|
372
380
|
verboseLog({}, chalk.gray(` 📊 Using LanceDB hybrid search for project: ${projectPath}`));
|
|
381
|
+
const projectPathFilter = `project_path = '${escapeSqlString(projectPath)}'`;
|
|
373
382
|
|
|
374
383
|
// Unified query function
|
|
375
384
|
const queryFiles = async (config) => {
|
|
@@ -377,14 +386,11 @@ export class ProjectAnalyzer {
|
|
|
377
386
|
let query = table.query().select(['path', 'name', 'content', 'type', 'language']);
|
|
378
387
|
|
|
379
388
|
if (config.whereClause) {
|
|
380
|
-
query = query.where(
|
|
381
|
-
}
|
|
389
|
+
query = query.where(`${projectPathFilter} AND (${config.whereClause})`);
|
|
390
|
+
}
|
|
391
|
+
else if (config.terms) {
|
|
382
392
|
// For term-based searches, query ALL files and sort by depth to prioritize shallow config files
|
|
383
|
-
const allFiles = await table
|
|
384
|
-
.query()
|
|
385
|
-
.select(['path', 'name', 'content', 'type', 'language'])
|
|
386
|
-
.where(`project_path = '${projectPath}'`)
|
|
387
|
-
.toArray(); // NO LIMIT - get all files
|
|
393
|
+
const allFiles = await table.query().select(['path', 'name', 'content', 'type', 'language']).where(projectPathFilter).toArray(); // NO LIMIT - get all files
|
|
388
394
|
|
|
389
395
|
// Sort by path depth (shorter paths first) to prioritize config files
|
|
390
396
|
allFiles.sort((a, b) => {
|
|
@@ -406,12 +412,14 @@ export class ProjectAnalyzer {
|
|
|
406
412
|
|
|
407
413
|
return matches;
|
|
408
414
|
});
|
|
409
|
-
}
|
|
410
|
-
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
query = query.where(projectPathFilter);
|
|
411
418
|
}
|
|
412
419
|
|
|
413
420
|
return await query.limit(config.limit || 30).toArray();
|
|
414
|
-
}
|
|
421
|
+
}
|
|
422
|
+
catch (error) {
|
|
415
423
|
verboseLog({}, chalk.yellow(` ⚠️ Query failed for ${config.category}: ${error.message}`));
|
|
416
424
|
return [];
|
|
417
425
|
}
|
|
@@ -430,7 +438,8 @@ export class ProjectAnalyzer {
|
|
|
430
438
|
}
|
|
431
439
|
});
|
|
432
440
|
}
|
|
433
|
-
}
|
|
441
|
+
}
|
|
442
|
+
catch (error) {
|
|
434
443
|
console.error(chalk.red('Error mining embeddings:'), error.message);
|
|
435
444
|
return [];
|
|
436
445
|
}
|
|
@@ -444,11 +453,17 @@ export class ProjectAnalyzer {
|
|
|
444
453
|
* Unified file type matching using consolidated patterns
|
|
445
454
|
*/
|
|
446
455
|
matchesFileType(filePath, fileName, type) {
|
|
447
|
-
if (type === 'docs')
|
|
448
|
-
|
|
456
|
+
if (type === 'docs') {
|
|
457
|
+
return isDocumentationFile(filePath);
|
|
458
|
+
}
|
|
459
|
+
if (type === 'tests') {
|
|
460
|
+
return isTestFile(filePath);
|
|
461
|
+
}
|
|
449
462
|
|
|
450
463
|
const config = FILE_PATTERNS[type];
|
|
451
|
-
if (!config)
|
|
464
|
+
if (!config) {
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
452
467
|
|
|
453
468
|
const fileNameLower = fileName.toLowerCase();
|
|
454
469
|
const filePathLower = filePath.toLowerCase();
|
|
@@ -546,10 +561,12 @@ Select files following the criteria in the system instructions.`;
|
|
|
546
561
|
|
|
547
562
|
verboseLog({}, chalk.cyan(`🎯 LLM selected ${keyFiles.length} final key files`));
|
|
548
563
|
return keyFiles;
|
|
549
|
-
}
|
|
564
|
+
}
|
|
565
|
+
else {
|
|
550
566
|
throw new Error(`Failed to extract valid JSON array from LLM response`);
|
|
551
567
|
}
|
|
552
|
-
}
|
|
568
|
+
}
|
|
569
|
+
catch (error) {
|
|
553
570
|
console.error(chalk.red('Error in LLM selection:'), error.message);
|
|
554
571
|
verboseLog({}, chalk.yellow(' 🔄 Falling back to automatic selection...'));
|
|
555
572
|
return this.fallbackFileSelection(candidates, projectPath);
|
|
@@ -611,7 +628,8 @@ Select files following the criteria in the system instructions.`;
|
|
|
611
628
|
const content = fs.readFileSync(fullPath, 'utf8');
|
|
612
629
|
hash.update(content.substring(0, 1000));
|
|
613
630
|
}
|
|
614
|
-
}
|
|
631
|
+
}
|
|
632
|
+
catch {
|
|
615
633
|
hash.update(file.relativePath || file.path || '');
|
|
616
634
|
}
|
|
617
635
|
}
|
|
@@ -635,7 +653,7 @@ Select files following the criteria in the system instructions.`;
|
|
|
635
653
|
const records = await table
|
|
636
654
|
.query()
|
|
637
655
|
.select(['type', 'path', 'content_hash', 'project_path'])
|
|
638
|
-
.where(`project_path = '${projectPath
|
|
656
|
+
.where(`project_path = '${escapeSqlString(projectPath)}'`)
|
|
639
657
|
.toArray();
|
|
640
658
|
|
|
641
659
|
const hash = crypto.createHash('sha256');
|
|
@@ -646,7 +664,8 @@ Select files following the criteria in the system instructions.`;
|
|
|
646
664
|
}
|
|
647
665
|
|
|
648
666
|
return hash.digest('hex');
|
|
649
|
-
}
|
|
667
|
+
}
|
|
668
|
+
catch (error) {
|
|
650
669
|
verboseLog({}, chalk.yellow(`Warning: Could not calculate embedding inventory hash: ${error.message}`));
|
|
651
670
|
return 'embedding-inventory-unavailable';
|
|
652
671
|
}
|
|
@@ -816,12 +835,14 @@ Follow the analysis guidelines from the system instructions to identify custom i
|
|
|
816
835
|
validatedSummary.projectPath = projectPath;
|
|
817
836
|
validatedSummary.keyFilesCount = keyFiles.length;
|
|
818
837
|
return validatedSummary;
|
|
819
|
-
}
|
|
838
|
+
}
|
|
839
|
+
else {
|
|
820
840
|
console.error(chalk.red('Failed to parse LLM response as JSON'));
|
|
821
841
|
console.error(chalk.gray('Response content preview:'), response.content.substring(0, 500));
|
|
822
842
|
throw new Error('Failed to parse LLM response as JSON');
|
|
823
843
|
}
|
|
824
|
-
}
|
|
844
|
+
}
|
|
845
|
+
catch (error) {
|
|
825
846
|
console.error(chalk.red('Error generating project summary:'), error.message);
|
|
826
847
|
const fallback = this.createFallbackSummary(projectPath, keyFiles);
|
|
827
848
|
verboseLog({}, chalk.yellow('Using fallback summary with technologies:'), fallback.technologies);
|
|
@@ -839,7 +860,9 @@ Follow the analysis guidelines from the system instructions to identify custom i
|
|
|
839
860
|
|
|
840
861
|
for (const file of keyFiles.slice(0, 25)) {
|
|
841
862
|
// Max 25 files
|
|
842
|
-
if (totalSize >= maxTotalSize)
|
|
863
|
+
if (totalSize >= maxTotalSize) {
|
|
864
|
+
break;
|
|
865
|
+
}
|
|
843
866
|
|
|
844
867
|
try {
|
|
845
868
|
const fileContent = fs.readFileSync(file.fullPath, 'utf8');
|
|
@@ -848,7 +871,8 @@ Follow the analysis guidelines from the system instructions to identify custom i
|
|
|
848
871
|
|
|
849
872
|
content += `\n\n=== ${file.relativePath} (${file.category}) ===\n${contentToAdd}`;
|
|
850
873
|
totalSize += contentToAdd.length;
|
|
851
|
-
}
|
|
874
|
+
}
|
|
875
|
+
catch (error) {
|
|
852
876
|
content += `\n\n=== ${file.relativePath} (${file.category}) ===\n[Could not read file: ${error.message}]`;
|
|
853
877
|
}
|
|
854
878
|
}
|
|
@@ -912,7 +936,8 @@ Follow the analysis guidelines from the system instructions to identify custom i
|
|
|
912
936
|
projectName = packageJson.name || projectName;
|
|
913
937
|
const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
|
914
938
|
technologies = Object.keys(deps).slice(0, 10);
|
|
915
|
-
}
|
|
939
|
+
}
|
|
940
|
+
catch {
|
|
916
941
|
// Continue with defaults
|
|
917
942
|
}
|
|
918
943
|
}
|
|
@@ -402,6 +402,14 @@ describe('ProjectAnalyzer', () => {
|
|
|
402
402
|
expect(mockTable.query).toHaveBeenCalled();
|
|
403
403
|
});
|
|
404
404
|
|
|
405
|
+
it('should escape project paths in key file queries', async () => {
|
|
406
|
+
const queryChain = mockTable.query();
|
|
407
|
+
|
|
408
|
+
await analyzer.mineKeyFilesFromEmbeddings("/mock/we're/project");
|
|
409
|
+
|
|
410
|
+
expect(queryChain.where).toHaveBeenCalledWith(expect.stringContaining("/mock/we''re/project"));
|
|
411
|
+
});
|
|
412
|
+
|
|
405
413
|
it('should handle table optimization errors gracefully', async () => {
|
|
406
414
|
mockTable.optimize.mockRejectedValue(new Error('legacy format'));
|
|
407
415
|
mockTable.query().toArray.mockResolvedValue([]);
|