kodevu 0.1.20 → 0.1.22
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 +2 -1
- package/package.json +2 -3
- package/src/config.js +15 -6
- package/src/review-runner.js +117 -5
- package/config.example.json +0 -12
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ If you want a config file, run `npx kodevu init` to create `./config.json` in th
|
|
|
32
32
|
npx kodevu init
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
This creates `config.json` in the current directory from
|
|
35
|
+
This creates `config.json` in the current directory from built-in defaults.
|
|
36
36
|
You only need this when you want to override defaults such as `reviewer` or output paths.
|
|
37
37
|
|
|
38
38
|
If you want a different path:
|
|
@@ -109,3 +109,4 @@ Internal defaults:
|
|
|
109
109
|
- If `outputFormats` includes `json`, matching `.json` files are generated alongside Markdown reports.
|
|
110
110
|
- `~/.kodevu/state.json` stores per-project checkpoints keyed by repository identity; only the v2 multi-project structure is supported.
|
|
111
111
|
- 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.
|
|
112
|
+
- 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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kodevu",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.22",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Poll SVN revisions or Git commits, send each change diff to a reviewer CLI, and write configurable review reports.",
|
|
6
6
|
"bin": {
|
|
@@ -8,8 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"src",
|
|
11
|
-
"README.md"
|
|
12
|
-
"config.example.json"
|
|
11
|
+
"README.md"
|
|
13
12
|
],
|
|
14
13
|
"scripts": {
|
|
15
14
|
"start": "node src/index.js",
|
package/src/config.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
|
-
import { constants as fsConstants } from "node:fs";
|
|
3
2
|
import os from "node:os";
|
|
4
3
|
import path from "node:path";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
4
|
import { findCommandOnPath } from "./shell.js";
|
|
7
5
|
|
|
8
6
|
const defaultStorageDir = path.join(os.homedir(), ".kodevu");
|
|
@@ -16,10 +14,21 @@ const defaultConfig = {
|
|
|
16
14
|
commandTimeoutMs: 600000,
|
|
17
15
|
prompt:
|
|
18
16
|
"请严格审查当前变更,优先指出 bug、回归风险、兼容性问题、安全问题、边界条件缺陷和缺失测试。请使用简体中文输出 Markdown;如果没有明确缺陷,请写“未发现明确缺陷”,并补充剩余风险。",
|
|
19
|
-
maxRevisionsPerRun:
|
|
17
|
+
maxRevisionsPerRun: 5,
|
|
20
18
|
outputFormats: ["markdown"]
|
|
21
19
|
};
|
|
22
20
|
|
|
21
|
+
const configTemplate = {
|
|
22
|
+
target: "C:/path/to/your/repository-or-subdirectory",
|
|
23
|
+
reviewer: defaultConfig.reviewer,
|
|
24
|
+
prompt: defaultConfig.prompt,
|
|
25
|
+
outputDir: "~/.kodevu",
|
|
26
|
+
stateFilePath: "~/.kodevu/state.json",
|
|
27
|
+
commandTimeoutMs: defaultConfig.commandTimeoutMs,
|
|
28
|
+
maxRevisionsPerRun: defaultConfig.maxRevisionsPerRun,
|
|
29
|
+
outputFormats: defaultConfig.outputFormats
|
|
30
|
+
};
|
|
31
|
+
|
|
23
32
|
function resolveConfigPath(baseDir, value) {
|
|
24
33
|
if (!value) {
|
|
25
34
|
return value;
|
|
@@ -264,12 +273,12 @@ Config highlights:
|
|
|
264
273
|
|
|
265
274
|
export async function initConfig(targetPath = "config.json") {
|
|
266
275
|
const absoluteTargetPath = path.resolve(targetPath);
|
|
267
|
-
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
268
|
-
const templatePath = path.join(packageRoot, "config.example.json");
|
|
269
276
|
|
|
270
277
|
await fs.mkdir(path.dirname(absoluteTargetPath), { recursive: true });
|
|
278
|
+
|
|
279
|
+
const content = JSON.stringify(configTemplate, null, 2) + "\n";
|
|
271
280
|
try {
|
|
272
|
-
await fs.
|
|
281
|
+
await fs.writeFile(absoluteTargetPath, content, { flag: "wx" });
|
|
273
282
|
} catch (error) {
|
|
274
283
|
if (error?.code === "EEXIST") {
|
|
275
284
|
throw new Error(`Config file already exists: ${absoluteTargetPath}`);
|
package/src/review-runner.js
CHANGED
|
@@ -23,6 +23,85 @@ 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 resolveTokenUsage(reviewerName, stderr, promptText, diffText, responseText) {
|
|
87
|
+
const parseFn = reviewerName === "gemini" ? parseGeminiTokenUsage : parseCodexTokenUsage;
|
|
88
|
+
const parsed = parseFn(stderr);
|
|
89
|
+
|
|
90
|
+
if (parsed && parsed.totalTokens > 0) {
|
|
91
|
+
return { ...parsed, source: "reviewer" };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const inputTokens = estimateTokenCount((promptText || "") + (diffText || ""));
|
|
95
|
+
const outputTokens = estimateTokenCount(responseText || "");
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
inputTokens,
|
|
99
|
+
outputTokens,
|
|
100
|
+
totalTokens: inputTokens + outputTokens,
|
|
101
|
+
source: "estimate"
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
26
105
|
const REVIEWERS = {
|
|
27
106
|
codex: {
|
|
28
107
|
displayName: "Codex",
|
|
@@ -314,7 +393,17 @@ function buildPrompt(config, backend, targetInfo, details, reviewDiffPayload) {
|
|
|
314
393
|
].join("\n\n");
|
|
315
394
|
}
|
|
316
395
|
|
|
317
|
-
function
|
|
396
|
+
function formatTokenUsage(tokenUsage) {
|
|
397
|
+
const sourceLabel = tokenUsage.source === "reviewer" ? "reviewer reported" : "estimated (~4 chars/token)";
|
|
398
|
+
return [
|
|
399
|
+
`- Input Tokens: \`${tokenUsage.inputTokens}\``,
|
|
400
|
+
`- Output Tokens: \`${tokenUsage.outputTokens}\``,
|
|
401
|
+
`- Total Tokens: \`${tokenUsage.totalTokens}\``,
|
|
402
|
+
`- Token Source: \`${sourceLabel}\``
|
|
403
|
+
].join("\n");
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function buildReport(config, backend, targetInfo, details, diffPayloads, reviewer, reviewerResult, tokenUsage) {
|
|
318
407
|
const lines = [
|
|
319
408
|
`# ${backend.displayName} Review Report: ${details.displayId}`,
|
|
320
409
|
"",
|
|
@@ -328,6 +417,10 @@ function buildReport(config, backend, targetInfo, details, diffPayloads, reviewe
|
|
|
328
417
|
`- Reviewer Exit Code: \`${reviewerResult.code}\``,
|
|
329
418
|
`- Reviewer Timed Out: \`${reviewerResult.timedOut ? "yes" : "no"}\``,
|
|
330
419
|
"",
|
|
420
|
+
"## Token Usage",
|
|
421
|
+
"",
|
|
422
|
+
formatTokenUsage(tokenUsage),
|
|
423
|
+
"",
|
|
331
424
|
"## Changed Files",
|
|
332
425
|
"",
|
|
333
426
|
formatChangedPaths(details.changedPaths),
|
|
@@ -361,7 +454,7 @@ function buildReport(config, backend, targetInfo, details, diffPayloads, reviewe
|
|
|
361
454
|
return `${lines.join("\n")}\n`;
|
|
362
455
|
}
|
|
363
456
|
|
|
364
|
-
function buildJsonReport(config, backend, targetInfo, details, diffPayloads, reviewer, reviewerResult) {
|
|
457
|
+
function buildJsonReport(config, backend, targetInfo, details, diffPayloads, reviewer, reviewerResult, tokenUsage) {
|
|
365
458
|
return {
|
|
366
459
|
repositoryType: backend.displayName,
|
|
367
460
|
target: targetInfo.targetDisplay || config.target,
|
|
@@ -374,6 +467,12 @@ function buildJsonReport(config, backend, targetInfo, details, diffPayloads, rev
|
|
|
374
467
|
exitCode: reviewerResult.code,
|
|
375
468
|
timedOut: Boolean(reviewerResult.timedOut)
|
|
376
469
|
},
|
|
470
|
+
tokenUsage: {
|
|
471
|
+
inputTokens: tokenUsage.inputTokens,
|
|
472
|
+
outputTokens: tokenUsage.outputTokens,
|
|
473
|
+
totalTokens: tokenUsage.totalTokens,
|
|
474
|
+
source: tokenUsage.source
|
|
475
|
+
},
|
|
377
476
|
changedFiles: details.changedPaths.map((item) => ({
|
|
378
477
|
action: item.action,
|
|
379
478
|
path: item.relativePath,
|
|
@@ -407,10 +506,20 @@ async function runReviewerPrompt(config, backend, targetInfo, details, diffText)
|
|
|
407
506
|
const reviewWorkspaceRoot = getReviewWorkspaceRoot(config, backend, targetInfo);
|
|
408
507
|
const diffPayloads = prepareDiffPayloads(config, diffText);
|
|
409
508
|
const promptText = buildPrompt(config, backend, targetInfo, details, diffPayloads.review);
|
|
509
|
+
const result = await reviewer.run(config, reviewWorkspaceRoot, promptText, diffPayloads.review.text);
|
|
510
|
+
const tokenUsage = resolveTokenUsage(
|
|
511
|
+
config.reviewer,
|
|
512
|
+
result.stderr,
|
|
513
|
+
promptText,
|
|
514
|
+
diffPayloads.review.text,
|
|
515
|
+
result.message
|
|
516
|
+
);
|
|
517
|
+
|
|
410
518
|
return {
|
|
411
519
|
reviewer,
|
|
412
520
|
diffPayloads,
|
|
413
|
-
result
|
|
521
|
+
result,
|
|
522
|
+
tokenUsage
|
|
414
523
|
};
|
|
415
524
|
}
|
|
416
525
|
|
|
@@ -469,6 +578,7 @@ async function reviewChange(config, backend, targetInfo, changeId, progress) {
|
|
|
469
578
|
let reviewer;
|
|
470
579
|
let diffPayloads;
|
|
471
580
|
let reviewerResult;
|
|
581
|
+
let tokenUsage;
|
|
472
582
|
let currentReviewerConfig;
|
|
473
583
|
|
|
474
584
|
for (const reviewerName of reviewersToTry) {
|
|
@@ -486,6 +596,7 @@ async function reviewChange(config, backend, targetInfo, changeId, progress) {
|
|
|
486
596
|
reviewer = res.reviewer;
|
|
487
597
|
diffPayloads = res.diffPayloads;
|
|
488
598
|
reviewerResult = res.result;
|
|
599
|
+
tokenUsage = res.tokenUsage;
|
|
489
600
|
|
|
490
601
|
if (reviewerResult.code === 0 && !reviewerResult.timedOut) {
|
|
491
602
|
break;
|
|
@@ -497,7 +608,8 @@ async function reviewChange(config, backend, targetInfo, changeId, progress) {
|
|
|
497
608
|
}
|
|
498
609
|
|
|
499
610
|
progress?.update(0.82, "writing report");
|
|
500
|
-
|
|
611
|
+
debugLog(config, `Token usage: input=${tokenUsage.inputTokens} output=${tokenUsage.outputTokens} total=${tokenUsage.totalTokens} source=${tokenUsage.source}`);
|
|
612
|
+
const report = buildReport(currentReviewerConfig, backend, targetInfo, details, diffPayloads, reviewer, reviewerResult, tokenUsage);
|
|
501
613
|
const outputFile = path.join(config.outputDir, backend.getReportFileName(changeId));
|
|
502
614
|
const jsonOutputFile = outputFile.replace(/\.md$/i, ".json");
|
|
503
615
|
|
|
@@ -508,7 +620,7 @@ async function reviewChange(config, backend, targetInfo, changeId, progress) {
|
|
|
508
620
|
if (shouldWriteFormat(config, "json")) {
|
|
509
621
|
await writeJsonFile(
|
|
510
622
|
jsonOutputFile,
|
|
511
|
-
buildJsonReport(currentReviewerConfig, backend, targetInfo, details, diffPayloads, reviewer, reviewerResult)
|
|
623
|
+
buildJsonReport(currentReviewerConfig, backend, targetInfo, details, diffPayloads, reviewer, reviewerResult, tokenUsage)
|
|
512
624
|
);
|
|
513
625
|
}
|
|
514
626
|
|
package/config.example.json
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"target": "C:/path/to/your/repository-or-subdirectory",
|
|
3
|
-
"reviewer": "auto",
|
|
4
|
-
"prompt": "请严格审查当前变更,优先指出 bug、回归风险、兼容性问题、安全问题、边界条件缺陷和缺失测试。请使用简体中文输出 Markdown;如果没有明确缺陷,请写“未发现明确缺陷”,并补充剩余风险。",
|
|
5
|
-
"outputDir": "~/.kodevu",
|
|
6
|
-
"stateFilePath": "~/.kodevu/state.json",
|
|
7
|
-
"commandTimeoutMs": 600000,
|
|
8
|
-
"maxRevisionsPerRun": 5,
|
|
9
|
-
"outputFormats": [
|
|
10
|
-
"markdown"
|
|
11
|
-
]
|
|
12
|
-
}
|