codecritique 2.1.1 → 2.1.3
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 +5 -1
- package/package.json +1 -1
- package/src/index.js +8 -7
- package/src/index.test.js +35 -0
- package/src/llm.js +1 -1
- package/src/llm.test.js +1 -1
- package/src/project-analyzer.js +4 -1
- package/src/project-analyzer.test.js +3 -1
- package/src/rag-analyzer.js +3 -2
- package/src/rag-analyzer.test.js +26 -0
package/README.md
CHANGED
|
@@ -415,7 +415,7 @@ jobs:
|
|
|
415
415
|
| `anthropic-api-key` | Anthropic API key for Claude models | **Yes** | - |
|
|
416
416
|
| `skip-label` | Label name to skip AI review | No | `ai-review-disabled` |
|
|
417
417
|
| `verbose` | Show verbose output | No | `false` |
|
|
418
|
-
| `model` | LLM model to use (e.g., `claude-sonnet-4-
|
|
418
|
+
| `model` | LLM model to use (e.g., `claude-sonnet-4-6`) | No | Auto-selected |
|
|
419
419
|
| `max-tokens` | Maximum tokens for LLM response | No | Auto-calculated |
|
|
420
420
|
| `cache-ttl` | Cache TTL for LLM prompts: "5m" (default, no extra cost) or "1h" (extended, extra cost for cache writes) | No | `5m` |
|
|
421
421
|
| `concurrency` | Concurrency for processing multiple files | No | `3` |
|
|
@@ -545,6 +545,10 @@ VERBOSE=true
|
|
|
545
545
|
|
|
546
546
|
Set `CODECRITIQUE_SKIP_DOTENV=1` to disable `.env` loading, as the shell wrapper does after forwarding its supported variables.
|
|
547
547
|
|
|
548
|
+
### Trust and Prompt Injection
|
|
549
|
+
|
|
550
|
+
Repository files, diffs, custom documents, and PR comments are untrusted data included in LLM prompts. A malicious repository can attempt to influence generated review text despite prompt boundaries and truncation. Treat CodeCritique output as advisory, review findings before acting on them, and never grant generated suggestions automatic execution privileges.
|
|
551
|
+
|
|
548
552
|
## Output Formats
|
|
549
553
|
|
|
550
554
|
CodeCritique supports three output formats:
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -42,6 +42,7 @@ async function cleanupEmbeddingsSystemIfInitialized() {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
const packageJson = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
45
|
+
const parseIntegerOption = (value) => Number.parseInt(value, 10);
|
|
45
46
|
|
|
46
47
|
// Configure command-line interface
|
|
47
48
|
program.name('codecritique').description('CLI tool for AI-powered code review using the RAG approach').version(packageJson.version);
|
|
@@ -62,13 +63,13 @@ program
|
|
|
62
63
|
.option('--output-file <file>', 'Save output to file (useful with --output json)')
|
|
63
64
|
.option('--no-color', 'Disable colored output')
|
|
64
65
|
.option('--verbose', 'Show verbose output')
|
|
65
|
-
.option('--model <model>', 'LLM model to use (e.g., claude-sonnet-4-
|
|
66
|
+
.option('--model <model>', 'LLM model to use (e.g., claude-sonnet-4-6)')
|
|
66
67
|
.option('--temperature <number>', 'LLM temperature', parseFloat, 0.2)
|
|
67
|
-
.option('--max-tokens <number>', 'LLM max tokens',
|
|
68
|
+
.option('--max-tokens <number>', 'LLM max tokens', parseIntegerOption, 8192)
|
|
68
69
|
.option('--cache-ttl <ttl>', 'Cache TTL for LLM prompts: "5m" (default, no extra cost) or "1h" (extended, extra cost for writes)', '5m')
|
|
69
70
|
.option('--similarity-threshold <number>', 'Threshold for finding similar code examples', parseFloat, 0.6)
|
|
70
|
-
.option('--max-examples <number>', 'Max similar code examples to use',
|
|
71
|
-
.option('--concurrency <number>', 'Concurrency for processing multiple files',
|
|
71
|
+
.option('--max-examples <number>', 'Max similar code examples to use', parseIntegerOption, 5)
|
|
72
|
+
.option('--concurrency <number>', 'Concurrency for processing multiple files', parseIntegerOption, 3)
|
|
72
73
|
.option(
|
|
73
74
|
'--doc <specs...>',
|
|
74
75
|
'A document to provide custom instructions to the LLM (e.g., "Engineering Guidelines:./docs/guidelines.md"). Can be specified multiple times.'
|
|
@@ -142,11 +143,11 @@ program
|
|
|
142
143
|
.option('-t, --token <token>', 'GitHub API token (or set GITHUB_TOKEN env var)')
|
|
143
144
|
.option('--since <date>', 'Only analyze PRs since this date (ISO format)')
|
|
144
145
|
.option('--until <date>', 'Only analyze PRs until this date (ISO format)')
|
|
145
|
-
.option('--limit <number>', 'Limit number of PRs to analyze',
|
|
146
|
+
.option('--limit <number>', 'Limit number of PRs to analyze', parseIntegerOption)
|
|
146
147
|
.option('--resume', 'Resume interrupted analysis')
|
|
147
148
|
.option('--clear', 'Clear existing data before analysis')
|
|
148
|
-
.option('--concurrency <number>', 'Number of concurrent requests',
|
|
149
|
-
.option('--batch-size <number>', 'Batch size for processing',
|
|
149
|
+
.option('--concurrency <number>', 'Number of concurrent requests', parseIntegerOption, 2)
|
|
150
|
+
.option('--batch-size <number>', 'Batch size for processing', parseIntegerOption, 50)
|
|
150
151
|
.option('--verbose', 'Show verbose output')
|
|
151
152
|
.action(analyzePRHistory);
|
|
152
153
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { expect, it, onTestFinished, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const { originalArgv, originalListeners, reviewFile } = vi.hoisted(() => {
|
|
4
|
+
const originalArgv = process.argv;
|
|
5
|
+
const originalListeners = Object.fromEntries(['SIGINT', 'SIGTERM', 'exit'].map((event) => [event, process.listeners(event)]));
|
|
6
|
+
process.argv = ['node', 'index.js', 'analyze', '--file', 'src/index.js', '--max-examples', '12'];
|
|
7
|
+
return {
|
|
8
|
+
originalArgv,
|
|
9
|
+
originalListeners,
|
|
10
|
+
reviewFile: vi.fn().mockResolvedValue({ success: true, results: [] }),
|
|
11
|
+
};
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
vi.unmock('chalk');
|
|
15
|
+
vi.mock('./rag-review.js', () => ({
|
|
16
|
+
reviewFile,
|
|
17
|
+
reviewFiles: vi.fn(),
|
|
18
|
+
reviewPullRequest: vi.fn(),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
import './index.js';
|
|
22
|
+
|
|
23
|
+
it('parses integer options independently of Commander defaults', async () => {
|
|
24
|
+
onTestFinished(() => {
|
|
25
|
+
process.argv = originalArgv;
|
|
26
|
+
for (const [event, listeners] of Object.entries(originalListeners)) {
|
|
27
|
+
process
|
|
28
|
+
.listeners(event)
|
|
29
|
+
.filter((listener) => !listeners.includes(listener))
|
|
30
|
+
.forEach((listener) => process.removeListener(event, listener));
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
await vi.waitFor(() => expect(reviewFile).toHaveBeenCalled());
|
|
34
|
+
expect(reviewFile.mock.calls[0][1].maxExamples).toBe(12);
|
|
35
|
+
});
|
package/src/llm.js
CHANGED
package/src/llm.test.js
CHANGED
package/src/project-analyzer.js
CHANGED
|
@@ -17,6 +17,7 @@ import { verboseLog } from './utils/logging.js';
|
|
|
17
17
|
import { escapeSqlString } from './utils/string-utils.js';
|
|
18
18
|
|
|
19
19
|
const TERM_SEARCH_CONTENT_CANDIDATE_LIMIT = 500;
|
|
20
|
+
const PROJECT_ANALYSIS_MODEL = 'claude-haiku-4-5';
|
|
20
21
|
|
|
21
22
|
// Consolidated file classification configuration
|
|
22
23
|
const FILE_PATTERNS = {
|
|
@@ -586,6 +587,7 @@ Select files following the criteria in the system instructions.`;
|
|
|
586
587
|
};
|
|
587
588
|
|
|
588
589
|
const response = await this.llm.sendPromptToClaude(prompt, {
|
|
590
|
+
model: PROJECT_ANALYSIS_MODEL,
|
|
589
591
|
system: FILE_SELECTION_SYSTEM_PROMPT,
|
|
590
592
|
temperature: 0.1,
|
|
591
593
|
maxTokens: 1000,
|
|
@@ -878,6 +880,7 @@ Follow the analysis guidelines from the system instructions to identify custom i
|
|
|
878
880
|
};
|
|
879
881
|
|
|
880
882
|
const response = await this.llm.sendPromptToClaude(prompt, {
|
|
883
|
+
model: PROJECT_ANALYSIS_MODEL,
|
|
881
884
|
system: PROJECT_SUMMARY_SYSTEM_PROMPT,
|
|
882
885
|
temperature: 0.1,
|
|
883
886
|
maxTokens: 4000,
|
|
@@ -886,7 +889,7 @@ Follow the analysis guidelines from the system instructions to identify custom i
|
|
|
886
889
|
|
|
887
890
|
const summary = response.json;
|
|
888
891
|
if (summary) {
|
|
889
|
-
// Validate and ensure required fields exist
|
|
892
|
+
// Validate and ensure required fields exist across model responses
|
|
890
893
|
const validatedSummary = this.validateProjectSummary(summary);
|
|
891
894
|
|
|
892
895
|
// Add metadata
|
|
@@ -556,7 +556,7 @@ describe('ProjectAnalyzer', () => {
|
|
|
556
556
|
|
|
557
557
|
const result = await analyzer.selectFinalKeyFiles(candidates, mockProjectPath);
|
|
558
558
|
|
|
559
|
-
expect(llm.sendPromptToClaude).
|
|
559
|
+
expect(llm.sendPromptToClaude).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({ model: 'claude-haiku-4-5' }));
|
|
560
560
|
expect(result.length).toBe(1);
|
|
561
561
|
expect(result[0].relativePath).toBe('package.json');
|
|
562
562
|
});
|
|
@@ -668,9 +668,11 @@ describe('ProjectAnalyzer', () => {
|
|
|
668
668
|
content: JSON.stringify(mockSummary),
|
|
669
669
|
json: mockSummary,
|
|
670
670
|
});
|
|
671
|
+
analyzer.llm = llm;
|
|
671
672
|
|
|
672
673
|
const result = await analyzer.generateProjectSummary(mockKeyFiles, mockProjectPath);
|
|
673
674
|
|
|
675
|
+
expect(llm.sendPromptToClaude).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({ model: 'claude-haiku-4-5' }));
|
|
674
676
|
expect(result.projectName).toBe('test-project');
|
|
675
677
|
expect(result.analysisDate).toBeDefined();
|
|
676
678
|
expect(result.projectPath).toBe(mockProjectPath);
|
package/src/rag-analyzer.js
CHANGED
|
@@ -764,7 +764,7 @@ function prepareContextForLLM(filePath, content, language, finalCodeExamples, fi
|
|
|
764
764
|
async function callLLMForAnalysis(context, options = {}) {
|
|
765
765
|
try {
|
|
766
766
|
let prompt;
|
|
767
|
-
const model = options.model || 'claude-sonnet-4-
|
|
767
|
+
const model = options.model || 'claude-sonnet-4-6';
|
|
768
768
|
const maxTokens = options.maxTokens || 8192; // Default to a safe limit
|
|
769
769
|
|
|
770
770
|
if (options.isHolisticPRReview) {
|
|
@@ -2343,7 +2343,8 @@ async function gatherUnifiedContextForPR(prFiles, options = {}) {
|
|
|
2343
2343
|
// Aggregate and deduplicate results
|
|
2344
2344
|
for (const context of allContexts) {
|
|
2345
2345
|
(context.finalCodeExamples || []).forEach((example) => {
|
|
2346
|
-
const
|
|
2346
|
+
const normalizedContent = example.content?.trim();
|
|
2347
|
+
const key = normalizedContent ? `content:${normalizedContent}` : example.path ? `path:${example.path}` : null;
|
|
2347
2348
|
if (
|
|
2348
2349
|
key &&
|
|
2349
2350
|
(!allProcessedContext.codeExamples.has(key) || example.similarity > allProcessedContext.codeExamples.get(key).similarity)
|
package/src/rag-analyzer.test.js
CHANGED
|
@@ -1136,6 +1136,32 @@ describe('rag-analyzer', () => {
|
|
|
1136
1136
|
expect(context.codeExamples[0].similarity).toBeGreaterThanOrEqual(0.7);
|
|
1137
1137
|
}
|
|
1138
1138
|
});
|
|
1139
|
+
|
|
1140
|
+
it('should deduplicate identical code content from different paths', async () => {
|
|
1141
|
+
mockEmbeddingsSystem.findSimilarCode
|
|
1142
|
+
.mockResolvedValueOnce([{ path: '/first.js', content: 'shared implementation', similarity: 0.7 }])
|
|
1143
|
+
.mockResolvedValueOnce([{ path: '/second.js', content: ' shared implementation ', similarity: 0.9 }]);
|
|
1144
|
+
const context = await gatherUnifiedContextForPR([
|
|
1145
|
+
{ filePath: '/src/file1.js', content: 'code1' },
|
|
1146
|
+
{ filePath: '/src/file2.js', content: 'code2' },
|
|
1147
|
+
]);
|
|
1148
|
+
|
|
1149
|
+
expect(context.codeExamples).toEqual([
|
|
1150
|
+
expect.objectContaining({ path: '/second.js', content: ' shared implementation ', similarity: 0.9 }),
|
|
1151
|
+
]);
|
|
1152
|
+
});
|
|
1153
|
+
|
|
1154
|
+
it('should keep content keys separate from path fallbacks', async () => {
|
|
1155
|
+
mockEmbeddingsSystem.findSimilarCode
|
|
1156
|
+
.mockResolvedValueOnce([{ path: '/first.js', content: '', similarity: 0.9 }])
|
|
1157
|
+
.mockResolvedValueOnce([{ path: '/second.js', content: ' /first.js ', similarity: 0.8 }]);
|
|
1158
|
+
const context = await gatherUnifiedContextForPR([
|
|
1159
|
+
{ filePath: '/src/file1.js', content: 'code1' },
|
|
1160
|
+
{ filePath: '/src/file2.js', content: 'code2' },
|
|
1161
|
+
]);
|
|
1162
|
+
|
|
1163
|
+
expect(context.codeExamples.map((example) => example.path)).toEqual(['/first.js', '/second.js']);
|
|
1164
|
+
});
|
|
1139
1165
|
});
|
|
1140
1166
|
|
|
1141
1167
|
// ==========================================================================
|