git-ai-review 2.2.2 → 2.3.1
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 +14 -0
- package/dist/review.d.ts +3 -1
- package/dist/review.js +39 -8
- package/dist/types.d.ts +5 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -164,6 +164,7 @@ In each target repository:
|
|
|
164
164
|
- `CODEX_BIN`: custom Codex executable path/name.
|
|
165
165
|
- `COPILOT_BIN`: custom Copilot executable path/name.
|
|
166
166
|
- `CLAUDE_BIN`: custom Claude executable path/name.
|
|
167
|
+
- `CLAUDE_MAX_TURNS`: max agentic turns for Claude CLI; default `2`.
|
|
167
168
|
- `COPILOT_REVIEW_MODEL`: default `gpt-5.3-codex`.
|
|
168
169
|
- `AI_REVIEW_TIMEOUT_MS`: default `300000` (5 min).
|
|
169
170
|
- `AI_REVIEW_PREFLIGHT_TIMEOUT_SEC`: default `8`.
|
|
@@ -185,6 +186,19 @@ Use `--verbose` to print:
|
|
|
185
186
|
|
|
186
187
|
This is useful for debugging prompt behavior and model integration issues.
|
|
187
188
|
|
|
189
|
+
## Token usage
|
|
190
|
+
|
|
191
|
+
After each review, token usage is printed to stdout when available:
|
|
192
|
+
|
|
193
|
+
```
|
|
194
|
+
Tokens: input: 1234, output: 567
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
- **Input tokens**: tokens sent to the model (your prompt, diff, schema, AGENTS.md context).
|
|
198
|
+
- **Output tokens**: tokens generated by the model (the review JSON response).
|
|
199
|
+
|
|
200
|
+
For Claude, usage is reliably extracted from the CLI's JSON envelope. For Codex and Copilot, usage is extracted on a best-effort basis from CLI output and may not always be available.
|
|
201
|
+
|
|
188
202
|
## Example setup in a target repo
|
|
189
203
|
|
|
190
204
|
```bash
|
package/dist/review.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ParsedReview, ReviewReport, ReviewRunnerResult } from './types.js';
|
|
1
|
+
import type { ParsedReview, ReviewReport, ReviewRunnerResult, TokenUsage } from './types.js';
|
|
2
2
|
export type ReviewerName = 'codex' | 'copilot' | 'claude';
|
|
3
3
|
export declare function buildFallbackOrder(lastUnavailable: string | null): ReviewerName[];
|
|
4
4
|
export declare function readLastUnavailable(cwd: string): string | null;
|
|
@@ -36,6 +36,8 @@ export interface ResolveBinaryOptions {
|
|
|
36
36
|
}
|
|
37
37
|
export declare function resolveBinary(envName: string, candidates: string[], options?: ResolveBinaryOptions): string | null;
|
|
38
38
|
export declare function buildPrompt(diff: string, cwd?: string, diffLabel?: string): string;
|
|
39
|
+
export declare function extractTokenUsage(usage: unknown): TokenUsage | undefined;
|
|
40
|
+
export declare function extractTokenUsageFromText(text: string): TokenUsage | undefined;
|
|
39
41
|
export declare function parseSubagentOutput(raw: string): ParsedReview;
|
|
40
42
|
export declare function buildSpawnOptions(cwd: string, env: Record<string, string>, osPlatform?: NodeJS.Platform): {
|
|
41
43
|
cwd: string;
|
package/dist/review.js
CHANGED
|
@@ -183,6 +183,24 @@ function validateParsedReview(parsed) {
|
|
|
183
183
|
}
|
|
184
184
|
return parsed;
|
|
185
185
|
}
|
|
186
|
+
export function extractTokenUsage(usage) {
|
|
187
|
+
if (!usage || typeof usage !== 'object')
|
|
188
|
+
return undefined;
|
|
189
|
+
const u = usage;
|
|
190
|
+
const input = typeof u.input_tokens === 'number' ? u.input_tokens : undefined;
|
|
191
|
+
const output = typeof u.output_tokens === 'number' ? u.output_tokens : undefined;
|
|
192
|
+
if (input === undefined && output === undefined)
|
|
193
|
+
return undefined;
|
|
194
|
+
return { input_tokens: input, output_tokens: output };
|
|
195
|
+
}
|
|
196
|
+
export function extractTokenUsageFromText(text) {
|
|
197
|
+
// Find a JSON object that contains both input_tokens and output_tokens as siblings
|
|
198
|
+
const objectPattern = /\{[^{}]*"input_tokens"\s*:\s*(\d+)[^{}]*"output_tokens"\s*:\s*(\d+)[^{}]*\}/;
|
|
199
|
+
const match = text.match(objectPattern);
|
|
200
|
+
if (!match)
|
|
201
|
+
return undefined;
|
|
202
|
+
return { input_tokens: Number(match[1]), output_tokens: Number(match[2]) };
|
|
203
|
+
}
|
|
186
204
|
export function parseSubagentOutput(raw) {
|
|
187
205
|
const trimmed = String(raw || '').trim();
|
|
188
206
|
if (!trimmed) {
|
|
@@ -269,7 +287,7 @@ async function runClaude(prompt, verbose, skipPreflight = false) {
|
|
|
269
287
|
'--print',
|
|
270
288
|
'--output-format', 'json',
|
|
271
289
|
'--no-session-persistence',
|
|
272
|
-
'--max-turns', '
|
|
290
|
+
'--max-turns', process.env.CLAUDE_MAX_TURNS || '2',
|
|
273
291
|
'--allowedTools', '',
|
|
274
292
|
];
|
|
275
293
|
if (verbose) {
|
|
@@ -296,16 +314,18 @@ async function runClaude(prompt, verbose, skipPreflight = false) {
|
|
|
296
314
|
return { available: false };
|
|
297
315
|
}
|
|
298
316
|
let reviewPayload = trimmed;
|
|
317
|
+
let usage;
|
|
299
318
|
try {
|
|
300
319
|
const envelope = JSON.parse(trimmed);
|
|
301
320
|
if (envelope && typeof envelope === 'object' && typeof envelope.result === 'string') {
|
|
302
321
|
reviewPayload = envelope.result;
|
|
322
|
+
usage = extractTokenUsage(envelope.usage);
|
|
303
323
|
}
|
|
304
324
|
}
|
|
305
325
|
catch {
|
|
306
326
|
// not an envelope — use raw output
|
|
307
327
|
}
|
|
308
|
-
return { available: true, result: parseSubagentOutput(reviewPayload) };
|
|
328
|
+
return { available: true, result: parseSubagentOutput(reviewPayload), usage };
|
|
309
329
|
}
|
|
310
330
|
catch (error) {
|
|
311
331
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -360,7 +380,8 @@ function runCodex(prompt, verbose, skipPreflight = false) {
|
|
|
360
380
|
console.error(`Codex: execution failed${err ? `: ${err}` : ''} — skipping.`);
|
|
361
381
|
return { available: false };
|
|
362
382
|
}
|
|
363
|
-
|
|
383
|
+
const usage = extractTokenUsageFromText(run.stdout || '');
|
|
384
|
+
return { available: true, result: parseSubagentOutput(readFileSync(resultPath, 'utf8')), usage };
|
|
364
385
|
}
|
|
365
386
|
catch (error) {
|
|
366
387
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -405,7 +426,8 @@ function runCopilot(prompt, verbose, skipPreflight = false) {
|
|
|
405
426
|
console.error('Copilot: empty response — skipping.');
|
|
406
427
|
return { available: false };
|
|
407
428
|
}
|
|
408
|
-
|
|
429
|
+
const usage = extractTokenUsageFromText(run.stderr || '');
|
|
430
|
+
return { available: true, result: parseSubagentOutput(stdout), usage };
|
|
409
431
|
}
|
|
410
432
|
catch (error) {
|
|
411
433
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -441,7 +463,7 @@ export function evaluateResults(claude, codex, copilot) {
|
|
|
441
463
|
}
|
|
442
464
|
return { pass: true, reason: `AI review approved by: ${passed.join(', ')}.` };
|
|
443
465
|
}
|
|
444
|
-
function printReview(model, result) {
|
|
466
|
+
function printReview(model, result, usage) {
|
|
445
467
|
const label = result.status === 'fail'
|
|
446
468
|
? `${model} review FAILED`
|
|
447
469
|
: result.findings.length > 0
|
|
@@ -454,6 +476,15 @@ function printReview(model, result) {
|
|
|
454
476
|
console.log(`- [${finding.severity}] ${finding.title} (${at})`);
|
|
455
477
|
console.log(` ${finding.details}`);
|
|
456
478
|
}
|
|
479
|
+
if (usage) {
|
|
480
|
+
const parts = [];
|
|
481
|
+
if (usage.input_tokens !== undefined)
|
|
482
|
+
parts.push(`input: ${usage.input_tokens}`);
|
|
483
|
+
if (usage.output_tokens !== undefined)
|
|
484
|
+
parts.push(`output: ${usage.output_tokens}`);
|
|
485
|
+
if (parts.length > 0)
|
|
486
|
+
console.log(`Tokens: ${parts.join(', ')}`);
|
|
487
|
+
}
|
|
457
488
|
}
|
|
458
489
|
export async function runReview(cwd = process.cwd(), options = {}, deps = {}) {
|
|
459
490
|
const verbose = options.verbose === true;
|
|
@@ -544,19 +575,19 @@ export async function runReview(cwd = process.cwd(), options = {}, deps = {}) {
|
|
|
544
575
|
runtimeDeps.writeReport(reportPath, report);
|
|
545
576
|
runtimeDeps.log(`AI review raw report saved: ${reportPath}`);
|
|
546
577
|
if (claude.available) {
|
|
547
|
-
printReview('Claude', claude.result);
|
|
578
|
+
printReview('Claude', claude.result, claude.usage);
|
|
548
579
|
}
|
|
549
580
|
else {
|
|
550
581
|
console.log('\nClaude: unavailable — skipped.');
|
|
551
582
|
}
|
|
552
583
|
if (codex.available) {
|
|
553
|
-
printReview('Codex', codex.result);
|
|
584
|
+
printReview('Codex', codex.result, codex.usage);
|
|
554
585
|
}
|
|
555
586
|
else {
|
|
556
587
|
console.log('\nCodex: unavailable — skipped.');
|
|
557
588
|
}
|
|
558
589
|
if (copilot.available) {
|
|
559
|
-
printReview('Copilot', copilot.result);
|
|
590
|
+
printReview('Copilot', copilot.result, copilot.usage);
|
|
560
591
|
}
|
|
561
592
|
else {
|
|
562
593
|
console.log('\nCopilot: unavailable — skipped.');
|
package/dist/types.d.ts
CHANGED
|
@@ -10,11 +10,16 @@ export interface ParsedReview {
|
|
|
10
10
|
summary: string;
|
|
11
11
|
findings: ReviewFinding[];
|
|
12
12
|
}
|
|
13
|
+
export interface TokenUsage {
|
|
14
|
+
input_tokens?: number;
|
|
15
|
+
output_tokens?: number;
|
|
16
|
+
}
|
|
13
17
|
export type ReviewRunnerResult = {
|
|
14
18
|
available: false;
|
|
15
19
|
} | {
|
|
16
20
|
available: true;
|
|
17
21
|
result: ParsedReview;
|
|
22
|
+
usage?: TokenUsage;
|
|
18
23
|
};
|
|
19
24
|
export interface ReviewReport {
|
|
20
25
|
claude: ParsedReview | {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "git-ai-review",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.1",
|
|
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",
|