codecritique 2.1.2 → 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 CHANGED
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codecritique",
3
- "version": "2.1.2",
3
+ "version": "2.1.3",
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
@@ -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);
@@ -64,11 +65,11 @@ program
64
65
  .option('--verbose', 'Show verbose output')
65
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', parseInt, 8192)
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', parseInt, 5)
71
- .option('--concurrency <number>', 'Concurrency for processing multiple files', parseInt, 3)
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', parseInt)
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', parseInt, 2)
149
- .option('--batch-size <number>', 'Batch size for processing', parseInt, 50)
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
+ });
@@ -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 key = example.path;
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)
@@ -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
  // ==========================================================================