git-ai-review 2.0.0 → 2.0.2

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,7 +1,34 @@
1
1
  <!-- SPDX-License-Identifier: MPL-2.0 -->
2
2
  # git-ai-review
3
3
 
4
- Review your git diff with your locally installed **Codex CLI** or **Copilot CLI** — run it manually in one command or automatically as a git pre-commit hook.
4
+ [![npm](https://img.shields.io/npm/v/git-ai-review)](https://www.npmjs.com/package/git-ai-review)
5
+
6
+ Review your git diff with your locally installed **Codex CLI**, **Copilot CLI**, or **Claude CLI** — run it manually in one command or automatically as a git pre-commit hook.
7
+
8
+ ## Quick start
9
+
10
+ Install and set up the git hook:
11
+
12
+ ```bash
13
+ npm i -D git-ai-review
14
+ npx git-ai-review install
15
+ ```
16
+
17
+ That's it. Every `git commit` will now run an AI review of your staged changes automatically.
18
+
19
+ You can also run reviews manually:
20
+
21
+ ```bash
22
+ npx git-ai-review review
23
+ npx git-ai-review review --verbose
24
+ npx git-ai-review review --codex
25
+ npx git-ai-review review --copilot
26
+ npx git-ai-review review --claude
27
+ ```
28
+
29
+ Requirements for the target repo:
30
+ - An `AGENTS.md` file must exist in the repo root (it's used as policy context for the review)
31
+ - At least one reviewer CLI must be installed and authenticated: **Codex CLI** (`codex login`), **Copilot CLI** (`copilot auth`), or **Claude CLI** (`claude`)
5
32
 
6
33
  ## License
7
34
 
@@ -9,14 +36,14 @@ This package is licensed under **MPL-2.0**.
9
36
 
10
37
  ## What it does
11
38
 
12
- `git-ai-review` uses your local Codex CLI and Copilot CLI installations to AI-review staged changes before they are committed. You can run it on demand with a single command (`npx git-ai-review review`) or install it as a git hook so every commit is reviewed automatically.
39
+ `git-ai-review` uses your locally installed AI CLIs to review staged changes before they are committed. You can run it on demand with a single command (`npx git-ai-review review`) or install it as a git hook so every commit is reviewed automatically.
13
40
 
14
41
  - Uses your local **Codex CLI** as the primary reviewer.
15
- - Falls back to your local **Copilot CLI** when Codex is unavailable.
42
+ - Falls back to **Copilot CLI**, then **Claude CLI** when the previous reviewer is unavailable.
16
43
  - Blocks commit when:
17
44
  - any reviewer returns `status: fail`, or
18
45
  - any findings are returned (even with `status: pass`), or
19
- - both reviewers are unavailable.
46
+ - all reviewers are unavailable.
20
47
  - Tracks consecutive failures and enables a hard lock after a limit (default `10`).
21
48
  - Builds prompt context from:
22
49
  - full `AGENTS.md`,
@@ -26,12 +53,13 @@ This package is licensed under **MPL-2.0**.
26
53
  ## Prerequisites
27
54
 
28
55
  At least one reviewer CLI must be installed and authenticated on each developer machine.
29
- Recommended: install both for resilient fallback behavior.
56
+ Recommended: install multiple for resilient fallback behavior.
30
57
 
31
- - Codex CLI (`codex`) with active login (`codex login`)
58
+ - Codex CLI (`codex`) with active login (`codex login`) — primary reviewer
32
59
  - Copilot CLI (`copilot`) with active login (`copilot auth`)
60
+ - Claude CLI (`claude`)
33
61
 
34
- If Codex is unavailable, fallback to Copilot is used. If both are unavailable, review fails.
62
+ Default fallback chain: Codex Copilot Claude. If all are unavailable, review fails.
35
63
 
36
64
  ## Install in another repository
37
65
 
@@ -60,6 +88,7 @@ npx git-ai-review review
60
88
  npx git-ai-review review --verbose
61
89
  npx git-ai-review review --codex
62
90
  npx git-ai-review review --copilot
91
+ npx git-ai-review review --claude
63
92
  npx git-ai-review pre-commit
64
93
  npx git-ai-review pre-commit --verbose
65
94
  npx git-ai-review install
@@ -72,6 +101,7 @@ npm run git-ai-review -- review
72
101
  npm run git-ai-review -- review --verbose
73
102
  npm run git-ai-review -- review --codex
74
103
  npm run git-ai-review -- review --copilot
104
+ npm run git-ai-review -- review --claude
75
105
  npm run git-ai-review -- pre-commit
76
106
  npm run git-ai-review -- pre-commit --verbose
77
107
  npm run git-ai-review -- install
@@ -80,14 +110,15 @@ npm run git-ai-review -- install
80
110
  `npm run ai-review` is an alias for `npm run git-ai-review` — both accept the same commands and flags.
81
111
 
82
112
  - `review`: run AI review against staged diff.
83
- - `review --verbose`: print full prompt plus raw Codex/Copilot outputs to stdout.
84
- - `review --codex`: force Codex reviewer only (skip Copilot fallback).
85
- - `review --copilot`: force Copilot reviewer only (skip Codex).
113
+ - `review --verbose`: print full prompt plus raw model outputs to stdout.
114
+ - `review --codex`: force Codex reviewer only (skip Copilot/Claude fallback).
115
+ - `review --copilot`: force Copilot reviewer only (skip Codex/Claude).
116
+ - `review --claude`: force Claude reviewer only (skip Codex/Copilot).
86
117
  - `pre-commit`: run lock-aware pre-commit flow (recommended for hooks).
87
118
  - `pre-commit --verbose`: same as pre-commit, but with detailed prompt/raw model logs.
88
119
  - `install`: install hook script and set `core.hooksPath`.
89
120
 
90
- The `--codex` and `--copilot` flags are mutually exclusive and can be combined with `--verbose`.
121
+ The `--claude`, `--codex`, and `--copilot` flags are mutually exclusive and can be combined with `--verbose`.
91
122
 
92
123
  ## Repository requirements
93
124
 
@@ -109,8 +140,9 @@ In each target repository:
109
140
 
110
141
  - `CODEX_BIN`: custom Codex executable path/name.
111
142
  - `COPILOT_BIN`: custom Copilot executable path/name.
143
+ - `CLAUDE_BIN`: custom Claude executable path/name.
112
144
  - `COPILOT_REVIEW_MODEL`: default `gpt-5.3-codex`.
113
- - `AI_REVIEW_TIMEOUT_MS`: default `180000`.
145
+ - `AI_REVIEW_TIMEOUT_MS`: default `300000` (5 min).
114
146
  - `AI_REVIEW_PREFLIGHT_TIMEOUT_SEC`: default `8`.
115
147
  - `AI_REVIEW_FAIL_LIMIT`: default `10`.
116
148
  - `AI_REVIEW_REPORT_PATH`: custom report location.
@@ -125,7 +157,8 @@ Use `--verbose` to print:
125
157
 
126
158
  - full generated review prompt
127
159
  - raw Codex stdout/stderr and structured response file
128
- - raw Copilot stdout/stderr (when fallback runs)
160
+ - raw Copilot stdout/stderr
161
+ - raw Claude stdout/stderr
129
162
 
130
163
  This is useful for debugging prompt behavior and model integration issues.
131
164
 
@@ -3,7 +3,11 @@
3
3
  * Copyright (c) 2026 ai-review contributors
4
4
  */
5
5
  import { runCli } from '../cli.js';
6
- const code = runCli(process.argv.slice(2));
7
- if (code !== 0) {
8
- process.exit(code);
9
- }
6
+ runCli(process.argv.slice(2)).then((code) => {
7
+ if (code !== 0) {
8
+ process.exit(code);
9
+ }
10
+ }).catch((err) => {
11
+ console.error(err instanceof Error ? err.message : String(err));
12
+ process.exit(1);
13
+ });
package/dist/cli.d.ts CHANGED
@@ -9,6 +9,7 @@ export interface CliDeps {
9
9
  log: (message: string) => void;
10
10
  }
11
11
  export declare function hasVerboseFlag(args: string[]): boolean;
12
+ export declare function hasClaudeFlag(args: string[]): boolean;
12
13
  export declare function hasCodexFlag(args: string[]): boolean;
13
14
  export declare function hasCopilotFlag(args: string[]): boolean;
14
- export declare function runCli(argv?: string[], deps?: CliDeps): number;
15
+ export declare function runCli(argv?: string[], deps?: CliDeps): Promise<number>;
package/dist/cli.js CHANGED
@@ -20,37 +20,43 @@ function printHelp(log) {
20
20
  log(' install Install .githooks/pre-commit and set core.hooksPath');
21
21
  log('');
22
22
  log('Options:');
23
- log(' --codex Force Codex reviewer only (skip Copilot fallback)');
24
- log(' --copilot Force Copilot reviewer only (skip Codex)');
23
+ log(' --codex Force Codex reviewer only (skip Copilot/Claude fallback)');
24
+ log(' --copilot Force Copilot reviewer only (skip Codex/Claude)');
25
+ log(' --claude Force Claude reviewer only (skip Codex/Copilot)');
25
26
  log(' --verbose Print full prompt and raw model outputs to stdout');
26
27
  }
27
28
  export function hasVerboseFlag(args) {
28
29
  return args.includes('--verbose');
29
30
  }
31
+ export function hasClaudeFlag(args) {
32
+ return args.includes('--claude');
33
+ }
30
34
  export function hasCodexFlag(args) {
31
35
  return args.includes('--codex');
32
36
  }
33
37
  export function hasCopilotFlag(args) {
34
38
  return args.includes('--copilot');
35
39
  }
36
- export function runCli(argv = process.argv.slice(2), deps = DEFAULT_CLI_DEPS) {
40
+ export async function runCli(argv = process.argv.slice(2), deps = DEFAULT_CLI_DEPS) {
37
41
  const command = argv[0] || 'help';
38
42
  const args = argv.slice(1);
39
43
  const verbose = hasVerboseFlag(args);
44
+ const useClaude = hasClaudeFlag(args);
40
45
  const useCodex = hasCodexFlag(args);
41
46
  const useCopilot = hasCopilotFlag(args);
42
47
  const cwd = deps.getCwd();
43
- if (useCodex && useCopilot) {
44
- deps.log('Error: --codex and --copilot are mutually exclusive.');
48
+ const selectedCount = [useClaude, useCodex, useCopilot].filter(Boolean).length;
49
+ if (selectedCount > 1) {
50
+ deps.log('Error: --claude, --codex, and --copilot are mutually exclusive.');
45
51
  return 1;
46
52
  }
47
- const reviewer = useCodex ? 'codex' : useCopilot ? 'copilot' : undefined;
53
+ const reviewer = useClaude ? 'claude' : useCodex ? 'codex' : useCopilot ? 'copilot' : undefined;
48
54
  if (command === 'review') {
49
- const result = deps.runReview(cwd, { verbose, reviewer });
55
+ const result = await deps.runReview(cwd, { verbose, reviewer });
50
56
  return result.pass ? 0 : 1;
51
57
  }
52
58
  if (command === 'pre-commit') {
53
- return deps.runPreCommit(cwd, { verbose, reviewer });
59
+ return await deps.runPreCommit(cwd, { verbose, reviewer });
54
60
  }
55
61
  if (command === 'install') {
56
62
  deps.installPreCommitHook();
@@ -1,9 +1,9 @@
1
1
  import { runReview } from './review.js';
2
2
  export interface PreCommitOptions {
3
3
  verbose?: boolean;
4
- reviewer?: 'codex' | 'copilot';
4
+ reviewer?: 'claude' | 'codex' | 'copilot';
5
5
  }
6
6
  export interface PreCommitDeps {
7
7
  runReviewFn: typeof runReview;
8
8
  }
9
- export declare function runPreCommit(cwd?: string, options?: PreCommitOptions, deps?: Partial<PreCommitDeps>): number;
9
+ export declare function runPreCommit(cwd?: string, options?: PreCommitOptions, deps?: Partial<PreCommitDeps>): Promise<number>;
package/dist/precommit.js CHANGED
@@ -25,7 +25,7 @@ function printLastReport(reportPath) {
25
25
  console.log('Review output: report file not found.');
26
26
  }
27
27
  }
28
- export function runPreCommit(cwd = process.cwd(), options = {}, deps = {}) {
28
+ export async function runPreCommit(cwd = process.cwd(), options = {}, deps = {}) {
29
29
  const verbose = options.verbose === true;
30
30
  const runReviewFn = deps.runReviewFn ?? runReview;
31
31
  if (isCiEnvironment()) {
@@ -42,7 +42,7 @@ export function runPreCommit(cwd = process.cwd(), options = {}, deps = {}) {
42
42
  printLastReport(reportPath);
43
43
  return 1;
44
44
  }
45
- const review = runReviewFn(cwd, { verbose, reviewer: options.reviewer });
45
+ const review = await runReviewFn(cwd, { verbose, reviewer: options.reviewer });
46
46
  if (review.pass) {
47
47
  rmSync(failCountPath, { force: true });
48
48
  return 0;
package/dist/review.d.ts CHANGED
@@ -1,13 +1,14 @@
1
1
  import type { ParsedReview, ReviewReport, ReviewRunnerResult } from './types.js';
2
2
  export interface RunReviewOptions {
3
3
  verbose?: boolean;
4
- reviewer?: 'codex' | 'copilot';
4
+ reviewer?: 'claude' | 'codex' | 'copilot';
5
5
  }
6
6
  export interface RunReviewDeps {
7
7
  getStagedDiff: () => string;
8
8
  buildPrompt: (diff: string, cwd: string) => string;
9
- runCodex: (prompt: string, verbose: boolean) => ReviewRunnerResult;
10
- runCopilot: (prompt: string, verbose: boolean) => ReviewRunnerResult;
9
+ runClaude: (prompt: string, verbose: boolean) => Promise<ReviewRunnerResult>;
10
+ runCodex: (prompt: string, verbose: boolean) => Promise<ReviewRunnerResult>;
11
+ runCopilot: (prompt: string, verbose: boolean) => Promise<ReviewRunnerResult>;
11
12
  writeReport: (reportPath: string, report: ReviewReport) => void;
12
13
  log: (message: string) => void;
13
14
  }
@@ -28,12 +29,19 @@ export interface ResolveBinaryOptions {
28
29
  }
29
30
  export declare function resolveBinary(envName: string, candidates: string[], options?: ResolveBinaryOptions): string | null;
30
31
  export declare function parseSubagentOutput(raw: string): ParsedReview;
31
- export declare function evaluateResults(codex: ReviewRunnerResult, copilot: ReviewRunnerResult): {
32
+ export declare function buildSpawnOptions(cwd: string, env: Record<string, string>, osPlatform?: NodeJS.Platform): {
33
+ cwd: string;
34
+ env: Record<string, string>;
35
+ stdio: ['pipe', 'pipe', 'pipe'];
36
+ detached: boolean;
37
+ windowsHide: boolean;
38
+ };
39
+ export declare function evaluateResults(claude: ReviewRunnerResult, codex: ReviewRunnerResult, copilot: ReviewRunnerResult): {
32
40
  pass: boolean;
33
41
  reason: string;
34
42
  };
35
- export declare function runReview(cwd?: string, options?: RunReviewOptions, deps?: Partial<RunReviewDeps>): {
43
+ export declare function runReview(cwd?: string, options?: RunReviewOptions, deps?: Partial<RunReviewDeps>): Promise<{
36
44
  pass: boolean;
37
45
  reportPath: string;
38
46
  reason: string;
39
- };
47
+ }>;
package/dist/review.js CHANGED
@@ -4,13 +4,13 @@
4
4
  import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
5
5
  import { join, resolve } from 'node:path';
6
6
  import { platform, tmpdir } from 'node:os';
7
- import { execFileSync, spawnSync } from 'node:child_process';
7
+ import { execFileSync, spawn, spawnSync } from 'node:child_process';
8
8
  import { pathToFileURL } from 'node:url';
9
9
  import { REVIEW_SCHEMA } from './schema.js';
10
10
  import { buildAgentsContext, resolvePromptHeaderLines } from './prompt.js';
11
11
  import { getGitPath, git } from './git.js';
12
12
  const COPILOT_MODEL = process.env.COPILOT_REVIEW_MODEL || 'gpt-5.3-codex';
13
- const TIMEOUT_MS = Number(process.env.AI_REVIEW_TIMEOUT_MS || 180000);
13
+ const TIMEOUT_MS = Number(process.env.AI_REVIEW_TIMEOUT_MS || 300000);
14
14
  const PREFLIGHT_TIMEOUT_SEC = Number(process.env.AI_REVIEW_PREFLIGHT_TIMEOUT_SEC || 8);
15
15
  export function logVerboseRunnerOutput(output, log = console.log) {
16
16
  const label = output.model.toUpperCase();
@@ -118,28 +118,148 @@ function buildPrompt(diff, cwd = process.cwd()) {
118
118
  diff,
119
119
  ].join('\n');
120
120
  }
121
+ function validateParsedReview(parsed) {
122
+ if (!parsed || typeof parsed !== 'object') {
123
+ throw new Error('Subagent output is not a JSON object.');
124
+ }
125
+ if (!['pass', 'fail'].includes(parsed.status)) {
126
+ throw new Error('Subagent output has an invalid status.');
127
+ }
128
+ if (!Array.isArray(parsed.findings)) {
129
+ throw new Error('Subagent output has an invalid findings list.');
130
+ }
131
+ return parsed;
132
+ }
121
133
  export function parseSubagentOutput(raw) {
122
134
  const trimmed = String(raw || '').trim();
123
135
  if (!trimmed) {
124
136
  throw new Error('Subagent returned an empty response.');
125
137
  }
126
138
  let cleaned = trimmed.replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/i, '');
139
+ // Try direct parse first (handles clean JSON without extraction)
140
+ try {
141
+ return validateParsedReview(JSON.parse(cleaned));
142
+ }
143
+ catch {
144
+ // not valid or not a review — try extraction
145
+ }
146
+ // Extract JSON object from surrounding text
127
147
  const jsonStart = cleaned.indexOf('{');
128
148
  const jsonEnd = cleaned.lastIndexOf('}');
129
149
  if (jsonStart !== -1 && jsonEnd > jsonStart) {
130
150
  cleaned = cleaned.slice(jsonStart, jsonEnd + 1);
131
151
  }
132
- const parsed = JSON.parse(cleaned);
133
- if (!parsed || typeof parsed !== 'object') {
134
- throw new Error('Subagent output is not a JSON object.');
152
+ return validateParsedReview(JSON.parse(cleaned));
153
+ }
154
+ export function buildSpawnOptions(cwd, env, osPlatform = process.platform) {
155
+ return {
156
+ cwd,
157
+ env,
158
+ stdio: ['pipe', 'pipe', 'pipe'],
159
+ detached: osPlatform !== 'win32',
160
+ windowsHide: true,
161
+ };
162
+ }
163
+ function spawnClaude(bin, args, prompt, env, cwd, timeoutMs) {
164
+ return new Promise((resolve, reject) => {
165
+ const child = spawn(bin, args, buildSpawnOptions(cwd, env));
166
+ let stdout = '';
167
+ let stderr = '';
168
+ let settled = false;
169
+ const timer = setTimeout(() => {
170
+ if (!settled) {
171
+ settled = true;
172
+ try {
173
+ if (process.platform !== 'win32' && child.pid) {
174
+ process.kill(-child.pid, 'SIGTERM');
175
+ }
176
+ else {
177
+ child.kill();
178
+ }
179
+ }
180
+ catch { /* ignore */ }
181
+ reject(new Error(`spawn ${bin} ETIMEDOUT`));
182
+ }
183
+ }, timeoutMs);
184
+ child.stdout.on('data', (chunk) => { stdout += chunk.toString(); });
185
+ child.stderr.on('data', (chunk) => { stderr += chunk.toString(); });
186
+ child.on('error', (err) => {
187
+ if (!settled) {
188
+ settled = true;
189
+ clearTimeout(timer);
190
+ reject(err);
191
+ }
192
+ });
193
+ child.on('close', (code) => {
194
+ if (!settled) {
195
+ settled = true;
196
+ clearTimeout(timer);
197
+ resolve({ stdout, stderr, exitCode: code ?? 1 });
198
+ }
199
+ });
200
+ child.stdin.on('error', () => { });
201
+ child.stdin.write(prompt);
202
+ child.stdin.end();
203
+ });
204
+ }
205
+ async function runClaude(prompt, verbose) {
206
+ const claude = resolveBinary('CLAUDE_BIN', ['claude']);
207
+ if (!claude) {
208
+ console.error('Claude: binary not found in PATH (or CLAUDE_BIN not set) — skipping.');
209
+ return { available: false };
135
210
  }
136
- if (!['pass', 'fail'].includes(parsed.status)) {
137
- throw new Error('Subagent output has an invalid status.');
211
+ if (!canReach('https://api.anthropic.com')) {
212
+ console.error('Claude: network preflight failed (api.anthropic.com) — skipping.');
213
+ return { available: false };
138
214
  }
139
- if (!Array.isArray(parsed.findings)) {
140
- throw new Error('Subagent output has an invalid findings list.');
215
+ try {
216
+ const claudeArgs = [
217
+ '--print',
218
+ '--output-format', 'json',
219
+ '--no-session-persistence',
220
+ '--max-turns', '1',
221
+ '--allowedTools', '',
222
+ ];
223
+ if (verbose) {
224
+ console.log(claude, claudeArgs.join(' '), '< <prompt via stdin>');
225
+ }
226
+ const cleanEnv = {};
227
+ for (const [key, val] of Object.entries(process.env)) {
228
+ if (val !== undefined && !(key.toUpperCase().startsWith('CLAUDE') && key.toUpperCase() !== 'CLAUDE_BIN')) {
229
+ cleanEnv[key] = val;
230
+ }
231
+ }
232
+ const { stdout, stderr, exitCode } = await spawnClaude(claude, claudeArgs, prompt, cleanEnv, process.cwd(), TIMEOUT_MS);
233
+ if (verbose) {
234
+ logVerboseRunnerOutput({ model: 'Claude', stdout, stderr });
235
+ }
236
+ if (exitCode !== 0) {
237
+ const err = stderr.trim() || stdout.trim();
238
+ console.error(`Claude: execution failed${err ? `: ${err}` : ''} — skipping.`);
239
+ return { available: false };
240
+ }
241
+ const trimmed = stdout.trim();
242
+ if (!trimmed) {
243
+ console.error('Claude: empty response — skipping.');
244
+ return { available: false };
245
+ }
246
+ let reviewPayload = trimmed;
247
+ try {
248
+ const envelope = JSON.parse(trimmed);
249
+ if (envelope && typeof envelope === 'object' && typeof envelope.result === 'string') {
250
+ reviewPayload = envelope.result;
251
+ }
252
+ }
253
+ catch {
254
+ // not an envelope — use raw output
255
+ }
256
+ return { available: true, result: parseSubagentOutput(reviewPayload) };
257
+ }
258
+ catch (error) {
259
+ const message = error instanceof Error ? error.message : String(error);
260
+ console.error(`Claude: ${message} — skipping.`);
261
+ return { available: false };
141
262
  }
142
- return parsed;
143
263
  }
144
264
  function runCodex(prompt, verbose) {
145
265
  const codex = resolveBinary('CODEX_BIN', ['codex']);
@@ -243,31 +363,32 @@ function runCopilot(prompt, verbose) {
243
363
  return { available: false };
244
364
  }
245
365
  }
246
- export function evaluateResults(codex, copilot) {
247
- if (!codex.available && !copilot.available) {
248
- return { pass: false, reason: 'Both Codex and Copilot are unavailable. At least one AI review is required.' };
366
+ export function evaluateResults(claude, codex, copilot) {
367
+ if (!claude.available && !codex.available && !copilot.available) {
368
+ return { pass: false, reason: 'All reviewers (Claude, Codex, Copilot) are unavailable. At least one AI review is required.' };
249
369
  }
370
+ const runners = [['Claude', claude], ['Codex', codex], ['Copilot', copilot]];
250
371
  const failures = [];
251
- if (codex.available && codex.result.status === 'fail')
252
- failures.push('Codex');
253
- if (copilot.available && copilot.result.status === 'fail')
254
- failures.push('Copilot');
372
+ for (const [name, runner] of runners) {
373
+ if (runner.available && runner.result.status === 'fail')
374
+ failures.push(name);
375
+ }
255
376
  if (failures.length > 0) {
256
377
  return { pass: false, reason: `AI review rejected by: ${failures.join(', ')}.` };
257
378
  }
258
379
  const withFindings = [];
259
- if (codex.available && codex.result.findings.length > 0)
260
- withFindings.push('Codex');
261
- if (copilot.available && copilot.result.findings.length > 0)
262
- withFindings.push('Copilot');
380
+ for (const [name, runner] of runners) {
381
+ if (runner.available && runner.result.findings.length > 0)
382
+ withFindings.push(name);
383
+ }
263
384
  if (withFindings.length > 0) {
264
385
  return { pass: false, reason: `AI review has unresolved findings from: ${withFindings.join(', ')}. Pass requires zero findings.` };
265
386
  }
266
387
  const passed = [];
267
- if (codex.available)
268
- passed.push('Codex');
269
- if (copilot.available)
270
- passed.push('Copilot');
388
+ for (const [name, runner] of runners) {
389
+ if (runner.available)
390
+ passed.push(name);
391
+ }
271
392
  return { pass: true, reason: `AI review approved by: ${passed.join(', ')}.` };
272
393
  }
273
394
  function printReview(model, result) {
@@ -284,14 +405,15 @@ function printReview(model, result) {
284
405
  console.log(` ${finding.details}`);
285
406
  }
286
407
  }
287
- export function runReview(cwd = process.cwd(), options = {}, deps = {}) {
408
+ export async function runReview(cwd = process.cwd(), options = {}, deps = {}) {
288
409
  const verbose = options.verbose === true;
289
410
  const reviewer = options.reviewer;
290
411
  const runtimeDeps = {
291
412
  getStagedDiff,
292
413
  buildPrompt,
293
- runCodex,
294
- runCopilot,
414
+ runClaude,
415
+ runCodex: (p, v) => Promise.resolve(runCodex(p, v)),
416
+ runCopilot: (p, v) => Promise.resolve(runCopilot(p, v)),
295
417
  writeReport: (reportPath, report) => {
296
418
  writeFileSync(reportPath, JSON.stringify(report, null, 2), 'utf8');
297
419
  },
@@ -309,31 +431,47 @@ export function runReview(cwd = process.cwd(), options = {}, deps = {}) {
309
431
  runtimeDeps.log(prompt);
310
432
  runtimeDeps.log('----- END REVIEW PROMPT -----');
311
433
  }
434
+ let claude = { available: false };
312
435
  let codex = { available: false };
313
436
  let copilot = { available: false };
314
- if (reviewer === 'copilot') {
437
+ if (reviewer === 'claude') {
438
+ runtimeDeps.log('Running Claude review (--claude)...');
439
+ claude = await runtimeDeps.runClaude(prompt, verbose);
440
+ }
441
+ else if (reviewer === 'copilot') {
315
442
  runtimeDeps.log('Running Copilot review (--copilot)...');
316
- copilot = runtimeDeps.runCopilot(prompt, verbose);
443
+ copilot = await runtimeDeps.runCopilot(prompt, verbose);
317
444
  }
318
445
  else if (reviewer === 'codex') {
319
446
  runtimeDeps.log('Running Codex review (--codex)...');
320
- codex = runtimeDeps.runCodex(prompt, verbose);
447
+ codex = await runtimeDeps.runCodex(prompt, verbose);
321
448
  }
322
449
  else {
323
450
  runtimeDeps.log('Running Codex review...');
324
- codex = runtimeDeps.runCodex(prompt, verbose);
451
+ codex = await runtimeDeps.runCodex(prompt, verbose);
325
452
  if (!codex.available) {
326
453
  runtimeDeps.log('Codex unavailable — falling back to Copilot review...');
327
- copilot = runtimeDeps.runCopilot(prompt, verbose);
454
+ copilot = await runtimeDeps.runCopilot(prompt, verbose);
455
+ if (!copilot.available) {
456
+ runtimeDeps.log('Copilot unavailable — falling back to Claude review...');
457
+ claude = await runtimeDeps.runClaude(prompt, verbose);
458
+ }
328
459
  }
329
460
  }
330
461
  const report = {
462
+ claude: claude.available ? claude.result : { status: 'unavailable' },
331
463
  codex: codex.available ? codex.result : { status: 'unavailable' },
332
464
  copilot: copilot.available ? copilot.result : { status: 'unavailable' },
333
465
  };
334
466
  const reportPath = resolve(cwd, process.env.AI_REVIEW_REPORT_PATH || getGitPath('ai-review-last.json'));
335
467
  runtimeDeps.writeReport(reportPath, report);
336
468
  runtimeDeps.log(`AI review raw report saved: ${reportPath}`);
469
+ if (claude.available) {
470
+ printReview('Claude', claude.result);
471
+ }
472
+ else {
473
+ console.log('\nClaude: unavailable — skipped.');
474
+ }
337
475
  if (codex.available) {
338
476
  printReview('Codex', codex.result);
339
477
  }
@@ -346,12 +484,16 @@ export function runReview(cwd = process.cwd(), options = {}, deps = {}) {
346
484
  else {
347
485
  console.log('\nCopilot: unavailable — skipped.');
348
486
  }
349
- const verdict = evaluateResults(codex, copilot);
487
+ const verdict = evaluateResults(claude, codex, copilot);
350
488
  console.log(`\n${verdict.reason}`);
351
489
  return { pass: verdict.pass, reportPath, reason: verdict.reason };
352
490
  }
353
491
  if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
354
- const result = runReview();
355
- if (!result.pass)
492
+ runReview().then((result) => {
493
+ if (!result.pass)
494
+ process.exit(1);
495
+ }).catch((err) => {
496
+ console.error(err instanceof Error ? err.message : String(err));
356
497
  process.exit(1);
498
+ });
357
499
  }
package/dist/types.d.ts CHANGED
@@ -17,6 +17,9 @@ export type ReviewRunnerResult = {
17
17
  result: ParsedReview;
18
18
  };
19
19
  export interface ReviewReport {
20
+ claude: ParsedReview | {
21
+ status: 'unavailable';
22
+ };
20
23
  codex: ParsedReview | {
21
24
  status: 'unavailable';
22
25
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-ai-review",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "description": "Review your git diff with local Codex CLI or Copilot CLI — run manually in one command or automatically as a git hook",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",