kodevu 0.1.21 → 0.1.23
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 +4 -2
- package/package.json +1 -1
- package/src/config.js +4 -4
- package/src/review-runner.js +168 -5
package/README.md
CHANGED
|
@@ -76,7 +76,7 @@ npx kodevu /path/to/your/repo --config ./config.current.json
|
|
|
76
76
|
## Config
|
|
77
77
|
|
|
78
78
|
- `target`: required repository target; can be provided by config or as the CLI positional argument
|
|
79
|
-
- `reviewer`: `codex`, `gemini`, or `auto`; default `auto`
|
|
79
|
+
- `reviewer`: `codex`, `gemini`, `copilot`, or `auto`; default `auto`
|
|
80
80
|
- `prompt`: saved into the report as review context
|
|
81
81
|
- `outputDir`: report output directory; default `~/.kodevu`
|
|
82
82
|
- `outputFormats`: report formats to generate; supports `markdown` and `json`; default `["markdown"]`
|
|
@@ -100,7 +100,8 @@ Internal defaults:
|
|
|
100
100
|
|
|
101
101
|
- `reviewer: "codex"` uses `codex exec` with the diff embedded in the prompt.
|
|
102
102
|
- `reviewer: "gemini"` uses `gemini -p` in non-interactive mode.
|
|
103
|
-
- `reviewer: "
|
|
103
|
+
- `reviewer: "copilot"` uses `copilot -p` in non-interactive mode.
|
|
104
|
+
- `reviewer: "auto"` probes `codex`, `gemini`, and `copilot` in `PATH`, then randomly chooses one of the available CLIs for this run.
|
|
104
105
|
- Large diffs are truncated before being sent to the reviewer or written into the report once they exceed the configured line or character limits.
|
|
105
106
|
- For Git targets and local SVN working copies, the reviewer command runs from the repository workspace so it can inspect related files beyond the diff when needed.
|
|
106
107
|
- For remote SVN URLs without a local working copy, the review still relies on the diff and change metadata only.
|
|
@@ -109,3 +110,4 @@ Internal defaults:
|
|
|
109
110
|
- If `outputFormats` includes `json`, matching `.json` files are generated alongside Markdown reports.
|
|
110
111
|
- `~/.kodevu/state.json` stores per-project checkpoints keyed by repository identity; only the v2 multi-project structure is supported.
|
|
111
112
|
- If the reviewer command exits non-zero or times out, the report is still written, but the state is not advanced so the change can be retried later.
|
|
113
|
+
- Each report includes a `Token Usage` section recording token consumption for the review task. When the reviewer CLI outputs token statistics (via stderr), those are used directly (`source: "reviewer"`). Otherwise tokens are estimated at ~4 characters per token (`source: "estimate"`). The JSON report contains a `tokenUsage` object with `inputTokens`, `outputTokens`, `totalTokens`, and `source`.
|
package/package.json
CHANGED
package/src/config.js
CHANGED
|
@@ -4,7 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { findCommandOnPath } from "./shell.js";
|
|
5
5
|
|
|
6
6
|
const defaultStorageDir = path.join(os.homedir(), ".kodevu");
|
|
7
|
-
const SUPPORTED_REVIEWERS = ["codex", "gemini"];
|
|
7
|
+
const SUPPORTED_REVIEWERS = ["codex", "gemini", "copilot"];
|
|
8
8
|
|
|
9
9
|
const defaultConfig = {
|
|
10
10
|
reviewer: "auto",
|
|
@@ -223,7 +223,7 @@ export async function loadConfig(configPath, cliArgs = {}) {
|
|
|
223
223
|
config.reviewerWasAutoSelected = true;
|
|
224
224
|
} else if (!SUPPORTED_REVIEWERS.includes(config.reviewer)) {
|
|
225
225
|
throw new Error(
|
|
226
|
-
`"reviewer" must be one of "codex", "gemini", or "auto"${loadedConfigPath ? ` in ${loadedConfigPath}` : ""}`
|
|
226
|
+
`"reviewer" must be one of "codex", "gemini", "copilot", or "auto"${loadedConfigPath ? ` in ${loadedConfigPath}` : ""}`
|
|
227
227
|
);
|
|
228
228
|
}
|
|
229
229
|
|
|
@@ -259,13 +259,13 @@ Usage:
|
|
|
259
259
|
|
|
260
260
|
Options:
|
|
261
261
|
--config, -c Optional config json path. If omitted, ./config.json is loaded only when present
|
|
262
|
-
--reviewer, -r Override reviewer (codex | gemini | auto)
|
|
262
|
+
--reviewer, -r Override reviewer (codex | gemini | copilot | auto)
|
|
263
263
|
--prompt, -p Override prompt
|
|
264
264
|
--debug, -d Print extra debug information to the console
|
|
265
265
|
--help, -h Show help
|
|
266
266
|
|
|
267
267
|
Config highlights:
|
|
268
|
-
reviewer codex | gemini | auto
|
|
268
|
+
reviewer codex | gemini | copilot | auto
|
|
269
269
|
target Repository target path (Git) or SVN working copy / URL; CLI positional target overrides config
|
|
270
270
|
outputFormats ["markdown"] by default; set to include "json" when needed
|
|
271
271
|
`);
|
package/src/review-runner.js
CHANGED
|
@@ -23,6 +23,117 @@ function debugLog(config, message) {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
function estimateTokenCount(text) {
|
|
27
|
+
if (!text) {
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return Math.ceil(text.length / 4);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseGeminiTokenUsage(stderr) {
|
|
35
|
+
if (!stderr) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const patterns = [
|
|
40
|
+
/input[_ ]tokens?\s*[:=]\s*(\d+)/i,
|
|
41
|
+
/output[_ ]tokens?\s*[:=]\s*(\d+)/i,
|
|
42
|
+
/total[_ ]tokens?\s*[:=]\s*(\d+)/i
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
const inputMatch = stderr.match(patterns[0]);
|
|
46
|
+
const outputMatch = stderr.match(patterns[1]);
|
|
47
|
+
const totalMatch = stderr.match(patterns[2]);
|
|
48
|
+
|
|
49
|
+
if (!inputMatch && !outputMatch && !totalMatch) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const inputTokens = inputMatch ? Number(inputMatch[1]) : 0;
|
|
54
|
+
const outputTokens = outputMatch ? Number(outputMatch[1]) : 0;
|
|
55
|
+
const totalTokens = totalMatch ? Number(totalMatch[1]) : inputTokens + outputTokens;
|
|
56
|
+
|
|
57
|
+
return { inputTokens, outputTokens, totalTokens };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function parseCodexTokenUsage(stderr) {
|
|
61
|
+
if (!stderr) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const patterns = [
|
|
66
|
+
/input[_ ]tokens?\s*[:=]\s*(\d+)/i,
|
|
67
|
+
/output[_ ]tokens?\s*[:=]\s*(\d+)/i,
|
|
68
|
+
/total[_ ]tokens?\s*[:=]\s*(\d+)/i
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
const inputMatch = stderr.match(patterns[0]);
|
|
72
|
+
const outputMatch = stderr.match(patterns[1]);
|
|
73
|
+
const totalMatch = stderr.match(patterns[2]);
|
|
74
|
+
|
|
75
|
+
if (!inputMatch && !outputMatch && !totalMatch) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const inputTokens = inputMatch ? Number(inputMatch[1]) : 0;
|
|
80
|
+
const outputTokens = outputMatch ? Number(outputMatch[1]) : 0;
|
|
81
|
+
const totalTokens = totalMatch ? Number(totalMatch[1]) : inputTokens + outputTokens;
|
|
82
|
+
|
|
83
|
+
return { inputTokens, outputTokens, totalTokens };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function parseCopilotTokenUsage(stderr) {
|
|
87
|
+
if (!stderr) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const patterns = [
|
|
92
|
+
/input[_ ]tokens?\s*[:=]\s*(\d+)/i,
|
|
93
|
+
/output[_ ]tokens?\s*[:=]\s*(\d+)/i,
|
|
94
|
+
/total[_ ]tokens?\s*[:=]\s*(\d+)/i
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
const inputMatch = stderr.match(patterns[0]);
|
|
98
|
+
const outputMatch = stderr.match(patterns[1]);
|
|
99
|
+
const totalMatch = stderr.match(patterns[2]);
|
|
100
|
+
|
|
101
|
+
if (!inputMatch && !outputMatch && !totalMatch) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const inputTokens = inputMatch ? Number(inputMatch[1]) : 0;
|
|
106
|
+
const outputTokens = outputMatch ? Number(outputMatch[1]) : 0;
|
|
107
|
+
const totalTokens = totalMatch ? Number(totalMatch[1]) : inputTokens + outputTokens;
|
|
108
|
+
|
|
109
|
+
return { inputTokens, outputTokens, totalTokens };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const TOKEN_PARSERS = {
|
|
113
|
+
gemini: parseGeminiTokenUsage,
|
|
114
|
+
codex: parseCodexTokenUsage,
|
|
115
|
+
copilot: parseCopilotTokenUsage
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
function resolveTokenUsage(reviewerName, stderr, promptText, diffText, responseText) {
|
|
119
|
+
const parseFn = TOKEN_PARSERS[reviewerName] || parseCopilotTokenUsage;
|
|
120
|
+
const parsed = parseFn(stderr);
|
|
121
|
+
|
|
122
|
+
if (parsed && parsed.totalTokens > 0) {
|
|
123
|
+
return { ...parsed, source: "reviewer" };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const inputTokens = estimateTokenCount((promptText || "") + (diffText || ""));
|
|
127
|
+
const outputTokens = estimateTokenCount(responseText || "");
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
inputTokens,
|
|
131
|
+
outputTokens,
|
|
132
|
+
totalTokens: inputTokens + outputTokens,
|
|
133
|
+
source: "estimate"
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
26
137
|
const REVIEWERS = {
|
|
27
138
|
codex: {
|
|
28
139
|
displayName: "Codex",
|
|
@@ -82,6 +193,25 @@ const REVIEWERS = {
|
|
|
82
193
|
debug: config.debug
|
|
83
194
|
});
|
|
84
195
|
|
|
196
|
+
return {
|
|
197
|
+
...execResult,
|
|
198
|
+
message: execResult.stdout
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
copilot: {
|
|
203
|
+
displayName: "Copilot",
|
|
204
|
+
responseSectionTitle: "Copilot Response",
|
|
205
|
+
emptyResponseText: "_No final response returned from copilot._",
|
|
206
|
+
async run(config, workingDir, promptText, diffText) {
|
|
207
|
+
const execResult = await runCommand("copilot", ["-p", promptText], {
|
|
208
|
+
cwd: workingDir,
|
|
209
|
+
input: ["Unified diff:", diffText].join("\n\n"),
|
|
210
|
+
allowFailure: true,
|
|
211
|
+
timeoutMs: config.commandTimeoutMs,
|
|
212
|
+
debug: config.debug
|
|
213
|
+
});
|
|
214
|
+
|
|
85
215
|
return {
|
|
86
216
|
...execResult,
|
|
87
217
|
message: execResult.stdout
|
|
@@ -314,7 +444,17 @@ function buildPrompt(config, backend, targetInfo, details, reviewDiffPayload) {
|
|
|
314
444
|
].join("\n\n");
|
|
315
445
|
}
|
|
316
446
|
|
|
317
|
-
function
|
|
447
|
+
function formatTokenUsage(tokenUsage) {
|
|
448
|
+
const sourceLabel = tokenUsage.source === "reviewer" ? "reviewer reported" : "estimated (~4 chars/token)";
|
|
449
|
+
return [
|
|
450
|
+
`- Input Tokens: \`${tokenUsage.inputTokens}\``,
|
|
451
|
+
`- Output Tokens: \`${tokenUsage.outputTokens}\``,
|
|
452
|
+
`- Total Tokens: \`${tokenUsage.totalTokens}\``,
|
|
453
|
+
`- Token Source: \`${sourceLabel}\``
|
|
454
|
+
].join("\n");
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function buildReport(config, backend, targetInfo, details, diffPayloads, reviewer, reviewerResult, tokenUsage) {
|
|
318
458
|
const lines = [
|
|
319
459
|
`# ${backend.displayName} Review Report: ${details.displayId}`,
|
|
320
460
|
"",
|
|
@@ -328,6 +468,10 @@ function buildReport(config, backend, targetInfo, details, diffPayloads, reviewe
|
|
|
328
468
|
`- Reviewer Exit Code: \`${reviewerResult.code}\``,
|
|
329
469
|
`- Reviewer Timed Out: \`${reviewerResult.timedOut ? "yes" : "no"}\``,
|
|
330
470
|
"",
|
|
471
|
+
"## Token Usage",
|
|
472
|
+
"",
|
|
473
|
+
formatTokenUsage(tokenUsage),
|
|
474
|
+
"",
|
|
331
475
|
"## Changed Files",
|
|
332
476
|
"",
|
|
333
477
|
formatChangedPaths(details.changedPaths),
|
|
@@ -361,7 +505,7 @@ function buildReport(config, backend, targetInfo, details, diffPayloads, reviewe
|
|
|
361
505
|
return `${lines.join("\n")}\n`;
|
|
362
506
|
}
|
|
363
507
|
|
|
364
|
-
function buildJsonReport(config, backend, targetInfo, details, diffPayloads, reviewer, reviewerResult) {
|
|
508
|
+
function buildJsonReport(config, backend, targetInfo, details, diffPayloads, reviewer, reviewerResult, tokenUsage) {
|
|
365
509
|
return {
|
|
366
510
|
repositoryType: backend.displayName,
|
|
367
511
|
target: targetInfo.targetDisplay || config.target,
|
|
@@ -374,6 +518,12 @@ function buildJsonReport(config, backend, targetInfo, details, diffPayloads, rev
|
|
|
374
518
|
exitCode: reviewerResult.code,
|
|
375
519
|
timedOut: Boolean(reviewerResult.timedOut)
|
|
376
520
|
},
|
|
521
|
+
tokenUsage: {
|
|
522
|
+
inputTokens: tokenUsage.inputTokens,
|
|
523
|
+
outputTokens: tokenUsage.outputTokens,
|
|
524
|
+
totalTokens: tokenUsage.totalTokens,
|
|
525
|
+
source: tokenUsage.source
|
|
526
|
+
},
|
|
377
527
|
changedFiles: details.changedPaths.map((item) => ({
|
|
378
528
|
action: item.action,
|
|
379
529
|
path: item.relativePath,
|
|
@@ -407,10 +557,20 @@ async function runReviewerPrompt(config, backend, targetInfo, details, diffText)
|
|
|
407
557
|
const reviewWorkspaceRoot = getReviewWorkspaceRoot(config, backend, targetInfo);
|
|
408
558
|
const diffPayloads = prepareDiffPayloads(config, diffText);
|
|
409
559
|
const promptText = buildPrompt(config, backend, targetInfo, details, diffPayloads.review);
|
|
560
|
+
const result = await reviewer.run(config, reviewWorkspaceRoot, promptText, diffPayloads.review.text);
|
|
561
|
+
const tokenUsage = resolveTokenUsage(
|
|
562
|
+
config.reviewer,
|
|
563
|
+
result.stderr,
|
|
564
|
+
promptText,
|
|
565
|
+
diffPayloads.review.text,
|
|
566
|
+
result.message
|
|
567
|
+
);
|
|
568
|
+
|
|
410
569
|
return {
|
|
411
570
|
reviewer,
|
|
412
571
|
diffPayloads,
|
|
413
|
-
result
|
|
572
|
+
result,
|
|
573
|
+
tokenUsage
|
|
414
574
|
};
|
|
415
575
|
}
|
|
416
576
|
|
|
@@ -469,6 +629,7 @@ async function reviewChange(config, backend, targetInfo, changeId, progress) {
|
|
|
469
629
|
let reviewer;
|
|
470
630
|
let diffPayloads;
|
|
471
631
|
let reviewerResult;
|
|
632
|
+
let tokenUsage;
|
|
472
633
|
let currentReviewerConfig;
|
|
473
634
|
|
|
474
635
|
for (const reviewerName of reviewersToTry) {
|
|
@@ -486,6 +647,7 @@ async function reviewChange(config, backend, targetInfo, changeId, progress) {
|
|
|
486
647
|
reviewer = res.reviewer;
|
|
487
648
|
diffPayloads = res.diffPayloads;
|
|
488
649
|
reviewerResult = res.result;
|
|
650
|
+
tokenUsage = res.tokenUsage;
|
|
489
651
|
|
|
490
652
|
if (reviewerResult.code === 0 && !reviewerResult.timedOut) {
|
|
491
653
|
break;
|
|
@@ -497,7 +659,8 @@ async function reviewChange(config, backend, targetInfo, changeId, progress) {
|
|
|
497
659
|
}
|
|
498
660
|
|
|
499
661
|
progress?.update(0.82, "writing report");
|
|
500
|
-
|
|
662
|
+
debugLog(config, `Token usage: input=${tokenUsage.inputTokens} output=${tokenUsage.outputTokens} total=${tokenUsage.totalTokens} source=${tokenUsage.source}`);
|
|
663
|
+
const report = buildReport(currentReviewerConfig, backend, targetInfo, details, diffPayloads, reviewer, reviewerResult, tokenUsage);
|
|
501
664
|
const outputFile = path.join(config.outputDir, backend.getReportFileName(changeId));
|
|
502
665
|
const jsonOutputFile = outputFile.replace(/\.md$/i, ".json");
|
|
503
666
|
|
|
@@ -508,7 +671,7 @@ async function reviewChange(config, backend, targetInfo, changeId, progress) {
|
|
|
508
671
|
if (shouldWriteFormat(config, "json")) {
|
|
509
672
|
await writeJsonFile(
|
|
510
673
|
jsonOutputFile,
|
|
511
|
-
buildJsonReport(currentReviewerConfig, backend, targetInfo, details, diffPayloads, reviewer, reviewerResult)
|
|
674
|
+
buildJsonReport(currentReviewerConfig, backend, targetInfo, details, diffPayloads, reviewer, reviewerResult, tokenUsage)
|
|
512
675
|
);
|
|
513
676
|
}
|
|
514
677
|
|