git-diff-ai-reviewer 1.1.4 → 1.2.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.
package/README.md CHANGED
@@ -1,6 +1,15 @@
1
1
  # git-diff-ai-reviewer
2
2
 
3
- AI-powered code review tool using **Claude** or **Gemini** APIs. Reviews git branch diffs and generates structured, actionable feedback with severity levels.
3
+ AI-powered code review tool using **Claude** or **Gemini** APIs. Reviews git branch diffs with full code context, iterative AI follow-ups, and structured severity-tagged feedback.
4
+
5
+ ## Features
6
+
7
+ - 🔍 **Context-aware review** — sends not just the diff, but the full functions, imports, and callers of changed code
8
+ - 🔄 **Iterative context gathering** — if the AI needs more context, it asks for it automatically (configurable rounds)
9
+ - 🚀 **Pre-push hook** — automatically review code before every `git push`
10
+ - 🎯 **Severity levels** — CRITICAL, WARNING, SUGGESTION with CI-friendly exit codes
11
+ - 🔧 **Fix prompt generation** — auto-generate prompts for AI agents to fix issues
12
+ - ⚙️ **Configurable rules** — built-in presets or custom review rules
4
13
 
5
14
  ## Installation
6
15
 
@@ -59,7 +68,7 @@ git-diff-ai-reviewer review --provider gemini
59
68
  # Review against a specific base branch
60
69
  git-diff-ai-reviewer review --base develop
61
70
 
62
- # Preview diff without calling API
71
+ # Preview diff and context without calling API
63
72
  git-diff-ai-reviewer review --dry-run
64
73
 
65
74
  # Generate fix prompt from latest review
@@ -79,6 +88,71 @@ Add to your `package.json`:
79
88
  }
80
89
  ```
81
90
 
91
+ ## Pre-Push Hook (Review on Push)
92
+
93
+ Automatically run an AI code review before every `git push`. If CRITICAL issues are found, the push is blocked.
94
+
95
+ ### Install the hook
96
+
97
+ ```bash
98
+ git-diff-ai-reviewer hook install
99
+ ```
100
+
101
+ This creates a `.git/hooks/pre-push` script that runs the review before each push.
102
+
103
+ ### Skip a review
104
+
105
+ Sometimes you need to push without waiting for a review:
106
+
107
+ ```bash
108
+ # Option 1: Environment variable
109
+ GIT_SKIP_REVIEW=1 git push
110
+
111
+ # Option 2: Git's built-in flag (skips all hooks)
112
+ git push --no-verify
113
+ ```
114
+
115
+ ### Uninstall the hook
116
+
117
+ ```bash
118
+ git-diff-ai-reviewer hook uninstall
119
+ ```
120
+
121
+ ### Check hook status
122
+
123
+ ```bash
124
+ git-diff-ai-reviewer hook status
125
+ ```
126
+
127
+ ### How it works
128
+
129
+ | Review result | Push behavior |
130
+ |---------------|---------------|
131
+ | ✅ No critical issues | Push proceeds |
132
+ | ❌ CRITICAL issues found | Push blocked (exit code 1) |
133
+ | ⚠️ Review error (API down, etc.) | Push proceeds (fails open) |
134
+
135
+ ## Context-Aware Review
136
+
137
+ Unlike simple diff-only review tools, `git-diff-ai-reviewer` automatically gathers context for each change:
138
+
139
+ 1. **Function context** — extracts the full body of every function/class that was modified
140
+ 2. **Imports** — includes the import/require statements from each changed file
141
+ 3. **Callers** — finds where modified functions are used across the project (via `git grep`)
142
+
143
+ This context is sent alongside the diff, giving the AI a much deeper understanding of the changes.
144
+
145
+ ### Iterative Context Requests
146
+
147
+ If the AI determines it needs more context to give a thorough review, it can request:
148
+
149
+ - Specific file ranges (`FILE: path/to/file.js LINES: 10-50`)
150
+ - Function definitions (`FILE: path/to/file.js FUNCTION: helperName`)
151
+ - Caller/usage information (`CALLERS: functionName`)
152
+ - Full file contents (`FILE: path/to/config.json`)
153
+
154
+ The tool automatically fulfills these requests and continues the conversation, up to `maxContextRounds` times (default: 3).
155
+
82
156
  ## Providers
83
157
 
84
158
  | Provider | Env Variable | Default Model |
@@ -135,6 +209,7 @@ Create `.ai-review.config.json` in your project root:
135
209
  "baseBranch": "main",
136
210
  "model": "gemini-2.0-flash",
137
211
  "maxTokens": 4096,
212
+ "maxContextRounds": 3,
138
213
  "outputDir": "./reviews",
139
214
  "reviewRules": {
140
215
  "preset": "standard",
@@ -151,6 +226,7 @@ Create `.ai-review.config.json` in your project root:
151
226
  | `baseBranch` | string | `"main"` | Branch to compare against |
152
227
  | `model` | string | per-provider | Override the AI model |
153
228
  | `maxTokens` | number | `4096` | Max response tokens |
229
+ | `maxContextRounds` | number | `3` | Max iterative context follow-up rounds |
154
230
  | `outputDir` | string | `"./reviews"` | Where to write output files |
155
231
  | `reviewRules` | string\|array | `"standard"` | Preset name or custom rules array |
156
232
 
@@ -191,21 +267,30 @@ const {
191
267
  getDiff,
192
268
  getChangedFiles,
193
269
  detectProvider,
270
+ buildInitialContext,
271
+ formatContextForPrompt,
272
+ installHook,
194
273
  STANDARD_RULES,
195
274
  } = require('git-diff-ai-reviewer');
196
275
 
276
+ // Review with context
197
277
  const diff = getDiff('main');
198
278
  const files = getChangedFiles('main');
199
- const provider = detectProvider({ provider: 'gemini' });
279
+ const context = buildInitialContext(diff, files);
280
+ const formattedContext = formatContextForPrompt(context);
281
+
200
282
  const review = await reviewCode(diff, {
201
- provider,
283
+ provider: 'gemini',
202
284
  changedFiles: files,
203
285
  branchName: 'feature/xyz',
204
- reviewRules: {
205
- preset: 'standard',
206
- extend: ['Check that all functions are properly documented']
207
- },
286
+ formattedContext,
287
+ maxContextRounds: 3,
288
+ reviewRules: STANDARD_RULES,
208
289
  });
290
+
291
+ // Install pre-push hook programmatically
292
+ const result = installHook();
293
+ console.log(result.message);
209
294
  ```
210
295
 
211
296
  ## Output Files
package/bin/review.js CHANGED
@@ -8,6 +8,8 @@ const { reviewCode, parseSeverityCounts, detectProvider, PROVIDERS, DEFAULT_MODE
8
8
  const { buildFixPrompt } = require('../src/prompts');
9
9
  const { loadConfig } = require('../src/config');
10
10
  const { writeReviewOutput, writeFixPrompt, findLatestReview } = require('../src/output');
11
+ const { buildInitialContext, formatContextForPrompt } = require('../src/context');
12
+ const { installHook, uninstallHook, hookStatus } = require('../src/hooks');
11
13
 
12
14
  // ─── Argument Parsing ────────────────────────────────────────────────
13
15
 
@@ -45,10 +47,15 @@ function parseArgs(argv) {
45
47
  case '-h':
46
48
  flags.help = true;
47
49
  break;
50
+ case '--skip-review':
51
+ flags.skipReview = true;
52
+ break;
48
53
  default:
49
54
  if (!args[i].startsWith('-') && !command) {
50
55
  command = args[i];
51
- } else {
56
+ } else if (!args[i].startsWith('-') && command === 'hook') {
57
+ // Hook subcommands are parsed separately
58
+ } else if (args[i].startsWith('-')) {
52
59
  console.warn(`Unknown option: ${args[i]}`);
53
60
  }
54
61
  }
@@ -65,11 +72,14 @@ function showHelp() {
65
72
  ║ 🤖 AI Code Review CLI ║
66
73
  ╚══════════════════════════════════════════════════════╝
67
74
 
68
- Usage: ai-review <command> [options]
75
+ Usage: git-diff-ai-reviewer <command> [options]
69
76
 
70
77
  Commands:
71
- review Review code changes using AI (default)
72
- fix Generate a fix prompt from the latest review
78
+ review Review code changes using AI (default)
79
+ fix Generate a fix prompt from the latest review
80
+ hook install Install pre-push git hook (review before push)
81
+ hook uninstall Remove the pre-push git hook
82
+ hook status Check if the pre-push hook is installed
73
83
 
74
84
  Options:
75
85
  -b, --base <branch> Base branch to compare against (default: main)
@@ -94,12 +104,19 @@ Review Rule Presets:
94
104
  standard Balanced everyday review (default)
95
105
  comprehensive Full audit for critical code
96
106
 
107
+ Pre-Push Hook:
108
+ Install: git-diff-ai-reviewer hook install
109
+ Uninstall: git-diff-ai-reviewer hook uninstall
110
+ Skip once: GIT_SKIP_REVIEW=1 git push
111
+ Skip once: git push --no-verify
112
+
97
113
  Examples:
98
- ai-review review Review with auto-detected provider
99
- ai-review review --provider gemini Review using Gemini
100
- ai-review review --base develop Review against develop branch
101
- ai-review review --dry-run Preview without calling API
102
- ai-review fix Generate fix prompt from latest review
114
+ git-diff-ai-reviewer review Review with auto-detected provider
115
+ git-diff-ai-reviewer review --provider gemini Review using Gemini
116
+ git-diff-ai-reviewer review --base develop Review against develop branch
117
+ git-diff-ai-reviewer review --dry-run Preview without calling API
118
+ git-diff-ai-reviewer fix Generate fix prompt from latest review
119
+ git-diff-ai-reviewer hook install Enable review-on-push
103
120
 
104
121
  Config File (.ai-review.config.json):
105
122
  {
@@ -107,6 +124,7 @@ Config File (.ai-review.config.json):
107
124
  "baseBranch": "main",
108
125
  "model": "",
109
126
  "maxTokens": 4096,
127
+ "maxContextRounds": 3,
110
128
  "outputDir": "./reviews",
111
129
  "reviewRules": "standard"
112
130
  }
@@ -150,14 +168,30 @@ async function commandReview(config, flags) {
150
168
  console.log(` Diff size: ${diff.length} characters`);
151
169
  console.log('─'.repeat(40));
152
170
 
171
+ // Gather code context
172
+ console.log('\n📦 Gathering code context...');
173
+ const context = buildInitialContext(diff, changedFiles);
174
+ const formattedContext = formatContextForPrompt(context);
175
+
176
+ const contextFunctions = context.files.reduce((sum, f) => sum + f.functions.length, 0);
177
+ const contextCallers = context.files.reduce((sum, f) => sum + f.callers.length, 0);
178
+ console.log(` ${contextFunctions} function(s) extracted, ${contextCallers} caller group(s) found`);
179
+ if (formattedContext) {
180
+ console.log(` Context size: ${formattedContext.length} characters`);
181
+ }
182
+
153
183
  // Dry run mode — show what would be sent
154
184
  if (flags.dryRun) {
155
185
  console.log('\n📋 DRY RUN — Diff extracted, API not called.\n');
156
186
  console.log(`Provider: ${provider} | Model: ${model}`);
157
187
  console.log(`Rules: ${config.reviewRules.length} active`);
188
+ console.log(`Context: ${contextFunctions} function(s), ${contextCallers} caller group(s)`);
158
189
  console.log('Changed files:');
159
190
  changedFiles.forEach(f => console.log(` - ${f}`));
160
191
  console.log(`\nDiff preview (first 500 chars):\n${diff.slice(0, 500)}...`);
192
+ if (formattedContext) {
193
+ console.log(`\nContext preview (first 500 chars):\n${formattedContext.slice(0, 500)}...`);
194
+ }
161
195
  console.log('\n💡 Remove --dry-run to send to AI for review.');
162
196
  process.exit(0);
163
197
  }
@@ -173,6 +207,8 @@ async function commandReview(config, flags) {
173
207
  model,
174
208
  maxTokens: config.maxTokens,
175
209
  reviewRules: config.reviewRules,
210
+ formattedContext,
211
+ maxContextRounds: config.maxContextRounds,
176
212
  });
177
213
 
178
214
  // Parse severity counts
@@ -245,6 +281,67 @@ async function commandFix(config, flags) {
245
281
  console.log('');
246
282
  }
247
283
 
284
+ // ─── Hook Command ───────────────────────────────────────────────────
285
+
286
+ function commandHook(subcommand) {
287
+ switch (subcommand) {
288
+ case 'install': {
289
+ const result = installHook();
290
+ if (result.success) {
291
+ console.log('');
292
+ console.log('✅ ' + result.message);
293
+ console.log(` 📄 ${result.path}`);
294
+ console.log('');
295
+ console.log(' The AI review will run automatically before each push.');
296
+ console.log(' To skip a review:');
297
+ console.log(' GIT_SKIP_REVIEW=1 git push');
298
+ console.log(' git push --no-verify');
299
+ console.log('');
300
+ } else {
301
+ console.error('');
302
+ console.error('❌ ' + result.message);
303
+ console.error('');
304
+ process.exit(1);
305
+ }
306
+ break;
307
+ }
308
+ case 'uninstall': {
309
+ const result = uninstallHook();
310
+ if (result.success) {
311
+ console.log('');
312
+ console.log('✅ ' + result.message);
313
+ console.log('');
314
+ } else {
315
+ console.error('');
316
+ console.error('❌ ' + result.message);
317
+ console.error('');
318
+ process.exit(1);
319
+ }
320
+ break;
321
+ }
322
+ case 'status': {
323
+ const result = hookStatus();
324
+ console.log('');
325
+ if (!result.installed) {
326
+ console.log('📋 Pre-push hook: not installed');
327
+ console.log(' Run "git-diff-ai-reviewer hook install" to enable.');
328
+ } else if (result.ours) {
329
+ console.log('📋 Pre-push hook: ✅ installed (by git-diff-ai-reviewer)');
330
+ console.log(` 📄 ${result.path}`);
331
+ } else {
332
+ console.log('📋 Pre-push hook: ⚠️ installed (by another tool)');
333
+ console.log(` 📄 ${result.path}`);
334
+ }
335
+ console.log('');
336
+ break;
337
+ }
338
+ default:
339
+ console.error('❌ Unknown hook subcommand: ' + (subcommand || '(none)'));
340
+ console.error(' Usage: git-diff-ai-reviewer hook <install|uninstall|status>');
341
+ process.exit(1);
342
+ }
343
+ }
344
+
248
345
  // ─── Main ───────────────────────────────────────────────────────────
249
346
 
250
347
  async function main() {
@@ -264,6 +361,15 @@ async function main() {
264
361
  // Load config
265
362
  const config = loadConfig(flags.configPath);
266
363
 
364
+ // Handle 'hook' command — parse subcommand from argv
365
+ if (command === 'hook') {
366
+ const args = process.argv.slice(2);
367
+ const hookIdx = args.indexOf('hook');
368
+ const subcommand = args[hookIdx + 1];
369
+ commandHook(subcommand);
370
+ return;
371
+ }
372
+
267
373
  switch (command) {
268
374
  case 'review':
269
375
  await commandReview(config, flags);
@@ -273,7 +379,7 @@ async function main() {
273
379
  break;
274
380
  default:
275
381
  console.error(`❌ Unknown command: ${command}`);
276
- console.error(' Run "ai-review --help" for usage info.');
382
+ console.error(' Run "git-diff-ai-reviewer --help" for usage info.');
277
383
  process.exit(1);
278
384
  }
279
385
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-diff-ai-reviewer",
3
- "version": "1.1.4",
3
+ "version": "1.2.0",
4
4
  "description": "AI-powered code review using Claude or Gemini API. Reviews git branch diffs and generates actionable fix prompts.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/claude.js CHANGED
@@ -1,4 +1,5 @@
1
- const { buildReviewSystemPrompt, buildReviewUserPrompt } = require('./prompts');
1
+ const { buildReviewSystemPrompt, buildReviewUserPromptWithContext, buildReviewUserPrompt, buildFollowUpContextMessage } = require('./prompts');
2
+ const { fulfillContextRequest, formatFollowUpContext } = require('./context');
2
3
 
3
4
  /**
4
5
  * Dynamically load the Anthropic SDK.
@@ -32,7 +33,7 @@ function createClient() {
32
33
  }
33
34
 
34
35
  /**
35
- * Send code diff to Claude for review.
36
+ * Send code diff to Claude for review with iterative context gathering.
36
37
  * @param {string} diff - The git diff content
37
38
  * @param {Object} options
38
39
  * @param {string[]} options.changedFiles - List of changed file paths
@@ -40,6 +41,8 @@ function createClient() {
40
41
  * @param {string} [options.model] - Claude model to use
41
42
  * @param {number} [options.maxTokens] - Max response tokens
42
43
  * @param {string[]} [options.reviewRules] - Custom review rules
44
+ * @param {string} [options.formattedContext] - Pre-formatted context string
45
+ * @param {number} [options.maxContextRounds] - Max follow-up context rounds
43
46
  * @returns {Promise<string>} The review text
44
47
  */
45
48
  async function reviewCode(diff, options = {}) {
@@ -49,28 +52,74 @@ async function reviewCode(diff, options = {}) {
49
52
  model = 'claude-sonnet-4-20250514',
50
53
  maxTokens = 4096,
51
54
  reviewRules = [],
55
+ formattedContext = '',
56
+ maxContextRounds = 3,
52
57
  } = options;
53
58
 
54
59
  const client = createClient();
55
60
 
56
61
  const systemPrompt = buildReviewSystemPrompt({ reviewRules });
57
- const userMessage = buildReviewUserPrompt(diff, changedFiles, branchName);
62
+
63
+ // Build initial message — with or without context
64
+ const userMessage = formattedContext
65
+ ? buildReviewUserPromptWithContext(diff, changedFiles, branchName, formattedContext)
66
+ : buildReviewUserPrompt(diff, changedFiles, branchName);
58
67
 
59
68
  console.log(`🤖 Sending ${diff.length} chars of diff to Claude (${model})...`);
69
+ if (formattedContext) {
70
+ console.log(`📦 Including code context (${formattedContext.length} chars)`);
71
+ }
72
+
73
+ // Conversation history for multi-turn
74
+ const messages = [
75
+ { role: 'user', content: userMessage },
76
+ ];
77
+
78
+ let reviewText = '';
79
+ let round = 0;
80
+
81
+ while (round <= maxContextRounds) {
82
+ const response = await client.messages.create({
83
+ model,
84
+ max_tokens: maxTokens,
85
+ system: systemPrompt,
86
+ messages,
87
+ });
88
+
89
+ const responseText = response.content
90
+ .filter(block => block.type === 'text')
91
+ .map(block => block.text)
92
+ .join('\n');
60
93
 
61
- const response = await client.messages.create({
62
- model,
63
- max_tokens: maxTokens,
64
- system: systemPrompt,
65
- messages: [
66
- { role: 'user', content: userMessage },
67
- ],
68
- });
94
+ // Check if AI is requesting more context
95
+ const { hasRequest, contextData, cleanResponse } = fulfillContextRequest(responseText);
69
96
 
70
- const reviewText = response.content
71
- .filter(block => block.type === 'text')
72
- .map(block => block.text)
73
- .join('\n');
97
+ if (hasRequest && round < maxContextRounds) {
98
+ round++;
99
+ console.log(`🔄 AI requested more context (round ${round}/${maxContextRounds})...`);
100
+ console.log(` Fulfilling ${contextData.length} context request(s)...`);
101
+
102
+ // Add assistant response to history
103
+ messages.push({ role: 'assistant', content: responseText });
104
+
105
+ // Fulfill the request and add as user message
106
+ const followUpText = formatFollowUpContext(contextData);
107
+ const followUpMessage = buildFollowUpContextMessage(followUpText);
108
+ messages.push({ role: 'user', content: followUpMessage });
109
+
110
+ // If there was a partial review in the response, keep it
111
+ if (cleanResponse.trim()) {
112
+ reviewText = cleanResponse + '\n\n';
113
+ }
114
+ } else {
115
+ // No more context requests — this is the final review
116
+ reviewText += hasRequest ? cleanResponse : responseText;
117
+ if (round > 0) {
118
+ console.log(`✅ Context gathering complete after ${round} round(s).`);
119
+ }
120
+ break;
121
+ }
122
+ }
74
123
 
75
124
  return reviewText;
76
125
  }
@@ -93,3 +142,4 @@ module.exports = {
93
142
  reviewCode,
94
143
  parseSeverityCounts,
95
144
  };
145
+
package/src/config.js CHANGED
@@ -11,6 +11,7 @@ const DEFAULT_CONFIG = {
11
11
  maxTokens: 4096,
12
12
  outputDir: './reviews',
13
13
  reviewRules: 'standard', // preset name ('basic', 'standard', 'comprehensive') or custom array
14
+ maxContextRounds: 3, // max follow-up context rounds for iterative review
14
15
  };
15
16
 
16
17
  /**