git-ai-review 2.1.0 → 2.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 +11 -1
- package/dist/review.d.ts +8 -3
- package/dist/review.js +88 -22
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,6 +26,13 @@ npx git-ai-review review --copilot
|
|
|
26
26
|
npx git-ai-review review --claude
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
+
Or review the diff between two branches:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npx git-ai-review diff main
|
|
33
|
+
npx git-ai-review diff main feature-branch
|
|
34
|
+
```
|
|
35
|
+
|
|
29
36
|
Requirements for the target repo:
|
|
30
37
|
- An `AGENTS.md` file must exist in the repo root (it's used as policy context for the review)
|
|
31
38
|
- At least one reviewer CLI must be installed and authenticated: **Codex CLI** (`codex login`), **Copilot CLI** (`copilot auth`), or **Claude CLI** (`claude`)
|
|
@@ -61,7 +68,9 @@ Recommended: install multiple for resilient fallback behavior.
|
|
|
61
68
|
|
|
62
69
|
When an API token env var is set, the network preflight check for that reviewer is skipped — this is useful in CI environments or when interactive OAuth login is not available.
|
|
63
70
|
|
|
64
|
-
Default fallback chain: Codex → Copilot → Claude. If all are unavailable, review fails.
|
|
71
|
+
Default fallback chain: Codex → Copilot → Claude. If a provider was unavailable on the previous run, it is rotated to the end of the chain on the next attempt (state stored in `.git/ai-review-last-unavailable`, cleared on success). If all are unavailable, review fails.
|
|
72
|
+
|
|
73
|
+
When a reviewer is explicitly selected via `--claude`, `--codex`, or `--copilot`, the preflight check is skipped entirely — the CLI tool handles its own authentication.
|
|
65
74
|
|
|
66
75
|
## Install in another repository
|
|
67
76
|
|
|
@@ -142,6 +151,7 @@ In each target repository:
|
|
|
142
151
|
|
|
143
152
|
- In CI (`CI=true|1|yes`) pre-commit review is skipped.
|
|
144
153
|
- Report is written to `.git/ai-review-last.json` by default.
|
|
154
|
+
- Last-unavailable provider state: `.git/ai-review-last-unavailable` (used for fallback rotation).
|
|
145
155
|
- Failure counter file: `.git/ai-review-fail-count`.
|
|
146
156
|
- Lock file after repeated failures: `.git/ai-review.lock`.
|
|
147
157
|
- Unlock manually by removing lock and fail-count files.
|
package/dist/review.d.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { ParsedReview, ReviewReport, ReviewRunnerResult } from './types.js';
|
|
2
|
+
export type ReviewerName = 'codex' | 'copilot' | 'claude';
|
|
3
|
+
export declare function buildFallbackOrder(lastUnavailable: string | null): ReviewerName[];
|
|
4
|
+
export declare function readLastUnavailable(cwd: string): string | null;
|
|
5
|
+
export declare function writeLastUnavailable(cwd: string, reviewer: string): void;
|
|
6
|
+
export declare function clearLastUnavailable(cwd: string): void;
|
|
2
7
|
export interface RunReviewOptions {
|
|
3
8
|
verbose?: boolean;
|
|
4
9
|
reviewer?: 'claude' | 'codex' | 'copilot';
|
|
@@ -6,9 +11,9 @@ export interface RunReviewOptions {
|
|
|
6
11
|
export interface RunReviewDeps {
|
|
7
12
|
getStagedDiff: () => string;
|
|
8
13
|
buildPrompt: (diff: string, cwd: string) => string;
|
|
9
|
-
runClaude: (prompt: string, verbose: boolean) => Promise<ReviewRunnerResult>;
|
|
10
|
-
runCodex: (prompt: string, verbose: boolean) => Promise<ReviewRunnerResult>;
|
|
11
|
-
runCopilot: (prompt: string, verbose: boolean) => Promise<ReviewRunnerResult>;
|
|
14
|
+
runClaude: (prompt: string, verbose: boolean, skipPreflight?: boolean) => Promise<ReviewRunnerResult>;
|
|
15
|
+
runCodex: (prompt: string, verbose: boolean, skipPreflight?: boolean) => Promise<ReviewRunnerResult>;
|
|
16
|
+
runCopilot: (prompt: string, verbose: boolean, skipPreflight?: boolean) => Promise<ReviewRunnerResult>;
|
|
12
17
|
writeReport: (reportPath: string, report: ReviewReport) => void;
|
|
13
18
|
log: (message: string) => void;
|
|
14
19
|
}
|
package/dist/review.js
CHANGED
|
@@ -12,6 +12,45 @@ import { getGitPath, git } from './git.js';
|
|
|
12
12
|
const COPILOT_MODEL = process.env.COPILOT_REVIEW_MODEL || 'gpt-5.3-codex';
|
|
13
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
|
+
const DEFAULT_FALLBACK_ORDER = ['codex', 'copilot', 'claude'];
|
|
16
|
+
export function buildFallbackOrder(lastUnavailable) {
|
|
17
|
+
if (!lastUnavailable || !DEFAULT_FALLBACK_ORDER.includes(lastUnavailable)) {
|
|
18
|
+
return [...DEFAULT_FALLBACK_ORDER];
|
|
19
|
+
}
|
|
20
|
+
const order = DEFAULT_FALLBACK_ORDER.filter((r) => r !== lastUnavailable);
|
|
21
|
+
order.push(lastUnavailable);
|
|
22
|
+
return order;
|
|
23
|
+
}
|
|
24
|
+
export function readLastUnavailable(cwd) {
|
|
25
|
+
try {
|
|
26
|
+
const filePath = resolve(cwd, getGitPath('ai-review-last-unavailable'));
|
|
27
|
+
if (!existsSync(filePath))
|
|
28
|
+
return null;
|
|
29
|
+
const value = readFileSync(filePath, 'utf8').trim();
|
|
30
|
+
return value || null;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export function writeLastUnavailable(cwd, reviewer) {
|
|
37
|
+
const filePath = resolve(cwd, getGitPath('ai-review-last-unavailable'));
|
|
38
|
+
try {
|
|
39
|
+
writeFileSync(filePath, reviewer, 'utf8');
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
console.error(`Warning: could not write last-unavailable state to ${filePath}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function clearLastUnavailable(cwd) {
|
|
46
|
+
const filePath = resolve(cwd, getGitPath('ai-review-last-unavailable'));
|
|
47
|
+
try {
|
|
48
|
+
rmSync(filePath, { force: true });
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
console.error(`Warning: could not clear last-unavailable state at ${filePath}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
15
54
|
export function logVerboseRunnerOutput(output, log = console.log) {
|
|
16
55
|
const label = output.model.toUpperCase();
|
|
17
56
|
log(`\n----- ${label} STDOUT (RAW) -----`);
|
|
@@ -216,13 +255,13 @@ function spawnClaude(bin, args, prompt, env, cwd, timeoutMs) {
|
|
|
216
255
|
child.stdin.end();
|
|
217
256
|
});
|
|
218
257
|
}
|
|
219
|
-
async function runClaude(prompt, verbose) {
|
|
258
|
+
async function runClaude(prompt, verbose, skipPreflight = false) {
|
|
220
259
|
const claude = resolveBinary('CLAUDE_BIN', ['claude']);
|
|
221
260
|
if (!claude) {
|
|
222
261
|
console.error('Claude: binary not found in PATH (or CLAUDE_BIN not set) — skipping.');
|
|
223
262
|
return { available: false };
|
|
224
263
|
}
|
|
225
|
-
if (!checkPreflight('Claude', 'ANTHROPIC_API_KEY', ['https://api.anthropic.com'], verbose)) {
|
|
264
|
+
if (!skipPreflight && !checkPreflight('Claude', 'ANTHROPIC_API_KEY', ['https://api.anthropic.com'], verbose)) {
|
|
226
265
|
return { available: false };
|
|
227
266
|
}
|
|
228
267
|
try {
|
|
@@ -274,13 +313,13 @@ async function runClaude(prompt, verbose) {
|
|
|
274
313
|
return { available: false };
|
|
275
314
|
}
|
|
276
315
|
}
|
|
277
|
-
function runCodex(prompt, verbose) {
|
|
316
|
+
function runCodex(prompt, verbose, skipPreflight = false) {
|
|
278
317
|
const codex = resolveBinary('CODEX_BIN', ['codex']);
|
|
279
318
|
if (!codex) {
|
|
280
319
|
console.error('Codex: binary not found in PATH (or CODEX_BIN not set) — skipping.');
|
|
281
320
|
return { available: false };
|
|
282
321
|
}
|
|
283
|
-
if (!checkPreflight('Codex', 'OPENAI_API_KEY', ['https://api.openai.com/v1/models', 'https://chatgpt.com'], verbose)) {
|
|
322
|
+
if (!skipPreflight && !checkPreflight('Codex', 'OPENAI_API_KEY', ['https://api.openai.com/v1/models', 'https://chatgpt.com'], verbose)) {
|
|
284
323
|
return { available: false };
|
|
285
324
|
}
|
|
286
325
|
const tempDir = mkdtempSync(join(tmpdir(), 'ai-review-codex-'));
|
|
@@ -332,13 +371,13 @@ function runCodex(prompt, verbose) {
|
|
|
332
371
|
rmSync(tempDir, { recursive: true, force: true });
|
|
333
372
|
}
|
|
334
373
|
}
|
|
335
|
-
function runCopilot(prompt, verbose) {
|
|
374
|
+
function runCopilot(prompt, verbose, skipPreflight = false) {
|
|
336
375
|
const copilot = resolveBinary('COPILOT_BIN', ['copilot']);
|
|
337
376
|
if (!copilot) {
|
|
338
377
|
console.error('Copilot: binary not found in PATH (or COPILOT_BIN not set) — skipping.');
|
|
339
378
|
return { available: false };
|
|
340
379
|
}
|
|
341
|
-
if (!checkPreflight('Copilot', 'GITHUB_TOKEN', ['https://api.github.com'], verbose)) {
|
|
380
|
+
if (!skipPreflight && !checkPreflight('Copilot', 'GITHUB_TOKEN', ['https://api.github.com'], verbose)) {
|
|
342
381
|
return { available: false };
|
|
343
382
|
}
|
|
344
383
|
try {
|
|
@@ -423,8 +462,8 @@ export async function runReview(cwd = process.cwd(), options = {}, deps = {}) {
|
|
|
423
462
|
getStagedDiff,
|
|
424
463
|
buildPrompt,
|
|
425
464
|
runClaude,
|
|
426
|
-
runCodex: (p, v) => Promise.resolve(runCodex(p, v)),
|
|
427
|
-
runCopilot: (p, v) => Promise.resolve(runCopilot(p, v)),
|
|
465
|
+
runCodex: (p, v, s) => Promise.resolve(runCodex(p, v, s)),
|
|
466
|
+
runCopilot: (p, v, s) => Promise.resolve(runCopilot(p, v, s)),
|
|
428
467
|
writeReport: (reportPath, report) => {
|
|
429
468
|
writeFileSync(reportPath, JSON.stringify(report, null, 2), 'utf8');
|
|
430
469
|
},
|
|
@@ -432,9 +471,9 @@ export async function runReview(cwd = process.cwd(), options = {}, deps = {}) {
|
|
|
432
471
|
...deps,
|
|
433
472
|
};
|
|
434
473
|
const diff = runtimeDeps.getStagedDiff();
|
|
435
|
-
if (!diff) {
|
|
436
|
-
runtimeDeps.log('AI review:
|
|
437
|
-
return { pass: true, reportPath: resolve(cwd, getGitPath('ai-review-last.json')), reason: '
|
|
474
|
+
if (!diff || !diff.trim()) {
|
|
475
|
+
runtimeDeps.log('AI review: diff is empty — nothing to review.');
|
|
476
|
+
return { pass: true, reportPath: resolve(cwd, getGitPath('ai-review-last.json')), reason: 'Empty diff.' };
|
|
438
477
|
}
|
|
439
478
|
const prompt = runtimeDeps.buildPrompt(diff, cwd);
|
|
440
479
|
if (verbose) {
|
|
@@ -447,26 +486,53 @@ export async function runReview(cwd = process.cwd(), options = {}, deps = {}) {
|
|
|
447
486
|
let copilot = { available: false };
|
|
448
487
|
if (reviewer === 'claude') {
|
|
449
488
|
runtimeDeps.log('Running Claude review (--claude)...');
|
|
450
|
-
claude = await runtimeDeps.runClaude(prompt, verbose);
|
|
489
|
+
claude = await runtimeDeps.runClaude(prompt, verbose, true);
|
|
451
490
|
}
|
|
452
491
|
else if (reviewer === 'copilot') {
|
|
453
492
|
runtimeDeps.log('Running Copilot review (--copilot)...');
|
|
454
|
-
copilot = await runtimeDeps.runCopilot(prompt, verbose);
|
|
493
|
+
copilot = await runtimeDeps.runCopilot(prompt, verbose, true);
|
|
455
494
|
}
|
|
456
495
|
else if (reviewer === 'codex') {
|
|
457
496
|
runtimeDeps.log('Running Codex review (--codex)...');
|
|
458
|
-
codex = await runtimeDeps.runCodex(prompt, verbose);
|
|
497
|
+
codex = await runtimeDeps.runCodex(prompt, verbose, true);
|
|
459
498
|
}
|
|
460
499
|
else {
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
runtimeDeps.
|
|
465
|
-
copilot
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
500
|
+
const lastUnavailable = readLastUnavailable(cwd);
|
|
501
|
+
const fallbackOrder = buildFallbackOrder(lastUnavailable);
|
|
502
|
+
const runners = {
|
|
503
|
+
codex: runtimeDeps.runCodex,
|
|
504
|
+
copilot: runtimeDeps.runCopilot,
|
|
505
|
+
claude: runtimeDeps.runClaude,
|
|
506
|
+
};
|
|
507
|
+
const results = {
|
|
508
|
+
codex: { available: false },
|
|
509
|
+
copilot: { available: false },
|
|
510
|
+
claude: { available: false },
|
|
511
|
+
};
|
|
512
|
+
const labels = { codex: 'Codex', copilot: 'Copilot', claude: 'Claude' };
|
|
513
|
+
let firstUnavailable = null;
|
|
514
|
+
for (let i = 0; i < fallbackOrder.length; i++) {
|
|
515
|
+
const name = fallbackOrder[i];
|
|
516
|
+
if (i === 0) {
|
|
517
|
+
runtimeDeps.log(`Running ${labels[name]} review...`);
|
|
518
|
+
}
|
|
519
|
+
else {
|
|
520
|
+
runtimeDeps.log(`${labels[fallbackOrder[i - 1]]} unavailable — falling back to ${labels[name]} review...`);
|
|
469
521
|
}
|
|
522
|
+
results[name] = await runners[name](prompt, verbose);
|
|
523
|
+
if (results[name].available)
|
|
524
|
+
break;
|
|
525
|
+
if (!firstUnavailable)
|
|
526
|
+
firstUnavailable = name;
|
|
527
|
+
}
|
|
528
|
+
claude = results.claude;
|
|
529
|
+
codex = results.codex;
|
|
530
|
+
copilot = results.copilot;
|
|
531
|
+
if (firstUnavailable) {
|
|
532
|
+
writeLastUnavailable(cwd, firstUnavailable);
|
|
533
|
+
}
|
|
534
|
+
else {
|
|
535
|
+
clearLastUnavailable(cwd);
|
|
470
536
|
}
|
|
471
537
|
}
|
|
472
538
|
const report = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "git-ai-review",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Review your git diff with local Codex CLI, Copilot CLI, or Claude CLI — run manually in one command or automatically as a git hook",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/cli.js",
|