kodevu 0.1.26 → 0.1.28
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 +9 -1
- package/package.json +1 -1
- package/src/config.js +55 -2
- package/src/index.js +1 -0
- package/src/logger.js +7 -2
- package/src/progress-ui.js +12 -2
- package/src/review-runner.js +21 -7
- package/src/shell.js +15 -5
package/README.md
CHANGED
|
@@ -59,6 +59,12 @@ Run with debug logs:
|
|
|
59
59
|
npx kodevu /path/to/your/repo --debug
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
+
Specify the output language (e.g., Chinese):
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npx kodevu /path/to/your/repo --lang zh
|
|
66
|
+
```
|
|
67
|
+
|
|
62
68
|
Use a custom config path only when needed:
|
|
63
69
|
|
|
64
70
|
```bash
|
|
@@ -77,7 +83,8 @@ npx kodevu /path/to/your/repo --config ./config.current.json
|
|
|
77
83
|
|
|
78
84
|
- `target`: required repository target; can be provided by config or as the CLI positional argument
|
|
79
85
|
- `reviewer`: `codex`, `gemini`, `copilot`, or `auto`; default `auto`
|
|
80
|
-
- `prompt`:
|
|
86
|
+
- `prompt`: additional instructions for the reviewer; usually empty by default as the core instructions are built-in
|
|
87
|
+
- `lang`: output language for the review (e.g., `zh`, `en`, `auto`); default `auto`
|
|
81
88
|
- `outputDir`: report output directory; default `~/.kodevu`
|
|
82
89
|
- `outputFormats`: report formats to generate; supports `markdown` and `json`; default `["markdown"]`
|
|
83
90
|
- `stateFilePath`: review state file path; default `~/.kodevu/state.json`
|
|
@@ -110,6 +117,7 @@ Internal defaults:
|
|
|
110
117
|
- If `outputFormats` includes `json`, matching `.json` files are generated alongside Markdown reports.
|
|
111
118
|
- `~/.kodevu/state.json` stores per-project checkpoints keyed by repository identity; only the v2 multi-project structure is supported.
|
|
112
119
|
- 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.
|
|
120
|
+
- Review instructions are built into the tool in English to ensure consistent logic across reviewers. The `lang` setting (CLI `--lang` or config `lang`) determines the language AI uses for the resulting review. When set to `auto`, Kodevu detects the language from the system environment (`LANG`, `LC_ALL`, or system locale).
|
|
113
121
|
- 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`.
|
|
114
122
|
|
|
115
123
|
## License
|
package/package.json
CHANGED
package/src/config.js
CHANGED
|
@@ -6,15 +6,16 @@ import { findCommandOnPath } from "./shell.js";
|
|
|
6
6
|
const defaultStorageDir = path.join(os.homedir(), ".kodevu");
|
|
7
7
|
const SUPPORTED_REVIEWERS = ["codex", "gemini", "copilot"];
|
|
8
8
|
|
|
9
|
+
|
|
9
10
|
const defaultConfig = {
|
|
10
11
|
reviewer: "auto",
|
|
11
12
|
target: "",
|
|
13
|
+
lang: "auto",
|
|
12
14
|
outputDir: defaultStorageDir,
|
|
13
15
|
stateFilePath: path.join(defaultStorageDir, "state.json"),
|
|
14
16
|
logsDir: path.join(defaultStorageDir, "logs"),
|
|
15
17
|
commandTimeoutMs: 600000,
|
|
16
|
-
prompt:
|
|
17
|
-
"请严格审查当前变更,优先指出 bug、回归风险、兼容性问题、安全问题、边界条件缺陷和缺失测试。请使用简体中文输出 Markdown;如果没有明确缺陷,请写“未发现明确缺陷”,并补充剩余风险。",
|
|
18
|
+
prompt: "",
|
|
18
19
|
maxRevisionsPerRun: 5,
|
|
19
20
|
outputFormats: ["markdown"]
|
|
20
21
|
};
|
|
@@ -22,6 +23,7 @@ const defaultConfig = {
|
|
|
22
23
|
const configTemplate = {
|
|
23
24
|
target: "C:/path/to/your/repository-or-subdirectory",
|
|
24
25
|
reviewer: defaultConfig.reviewer,
|
|
26
|
+
lang: defaultConfig.lang,
|
|
25
27
|
prompt: defaultConfig.prompt,
|
|
26
28
|
outputDir: "~/.kodevu",
|
|
27
29
|
stateFilePath: "~/.kodevu/state.json",
|
|
@@ -74,6 +76,38 @@ function normalizeOutputFormats(outputFormats, loadedConfigPath) {
|
|
|
74
76
|
return normalized;
|
|
75
77
|
}
|
|
76
78
|
|
|
79
|
+
function detectLanguage() {
|
|
80
|
+
const envLang = (process.env.LANG || process.env.LC_ALL || process.env.LC_MESSAGES || "").toLowerCase();
|
|
81
|
+
const intlLocale = (() => {
|
|
82
|
+
try {
|
|
83
|
+
return Intl.DateTimeFormat().resolvedOptions().locale.toLowerCase();
|
|
84
|
+
} catch {
|
|
85
|
+
return "";
|
|
86
|
+
}
|
|
87
|
+
})();
|
|
88
|
+
|
|
89
|
+
// On Windows, the LANG environment variable is often set to 'en_US.UTF-8' by default
|
|
90
|
+
// in many shells (like Git Bash/MSYS2), regardless of the actual OS display language.
|
|
91
|
+
// We prefer the system locale (Intl) on Windows if it's Chinese.
|
|
92
|
+
if (os.platform() === "win32" && intlLocale.startsWith("zh")) {
|
|
93
|
+
return "zh";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (envLang) {
|
|
97
|
+
if (envLang.startsWith("zh")) return "zh";
|
|
98
|
+
if (envLang.startsWith("en")) return "en";
|
|
99
|
+
return envLang.split(/[._-]/)[0];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (intlLocale) {
|
|
103
|
+
if (intlLocale.startsWith("zh")) return "zh";
|
|
104
|
+
if (intlLocale.startsWith("en")) return "en";
|
|
105
|
+
return intlLocale.split("-")[0];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return "en";
|
|
109
|
+
}
|
|
110
|
+
|
|
77
111
|
async function resolveAutoReviewers(debug, loadedConfigPath) {
|
|
78
112
|
const availableReviewers = [];
|
|
79
113
|
|
|
@@ -109,6 +143,7 @@ export function parseCliArgs(argv) {
|
|
|
109
143
|
debug: false,
|
|
110
144
|
help: false,
|
|
111
145
|
reviewer: "",
|
|
146
|
+
lang: "",
|
|
112
147
|
prompt: "",
|
|
113
148
|
commandExplicitlySet: false
|
|
114
149
|
};
|
|
@@ -163,6 +198,16 @@ export function parseCliArgs(argv) {
|
|
|
163
198
|
continue;
|
|
164
199
|
}
|
|
165
200
|
|
|
201
|
+
if (value === "--lang" || value === "-l") {
|
|
202
|
+
const lang = argv[index + 1];
|
|
203
|
+
if (!lang || lang.startsWith("-")) {
|
|
204
|
+
throw new Error(`Missing value for ${value}`);
|
|
205
|
+
}
|
|
206
|
+
args.lang = lang;
|
|
207
|
+
index += 1;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
166
211
|
if (!value.startsWith("-") && args.command === "run" && !args.target) {
|
|
167
212
|
args.target = value;
|
|
168
213
|
continue;
|
|
@@ -209,12 +254,19 @@ export async function loadConfig(configPath, cliArgs = {}) {
|
|
|
209
254
|
config.prompt = cliArgs.prompt;
|
|
210
255
|
}
|
|
211
256
|
|
|
257
|
+
if (cliArgs.lang) {
|
|
258
|
+
config.lang = cliArgs.lang;
|
|
259
|
+
}
|
|
260
|
+
|
|
212
261
|
if (!config.target) {
|
|
213
262
|
throw new Error('Missing target. Pass `npx kodevu <repo-path>` or set "target" in config.json.');
|
|
214
263
|
}
|
|
215
264
|
|
|
216
265
|
config.debug = Boolean(cliArgs.debug);
|
|
217
266
|
config.reviewer = String(config.reviewer || "auto").toLowerCase();
|
|
267
|
+
config.lang = String(config.lang || "auto").toLowerCase();
|
|
268
|
+
|
|
269
|
+
config.resolvedLang = config.lang === "auto" ? detectLanguage() : config.lang;
|
|
218
270
|
|
|
219
271
|
if (config.reviewer === "auto") {
|
|
220
272
|
const availableReviewers = await resolveAutoReviewers(config.debug, loadedConfigPath);
|
|
@@ -264,6 +316,7 @@ Options:
|
|
|
264
316
|
--config, -c Optional config json path. If omitted, ./config.json is loaded only when present
|
|
265
317
|
--reviewer, -r Override reviewer (codex | gemini | copilot | auto)
|
|
266
318
|
--prompt, -p Override prompt
|
|
319
|
+
--lang, -l Override output language (e.g. zh, en, auto)
|
|
267
320
|
--debug, -d Print extra debug information to the console
|
|
268
321
|
--help, -h Show help
|
|
269
322
|
|
package/src/index.js
CHANGED
package/src/logger.js
CHANGED
|
@@ -38,6 +38,10 @@ class Logger {
|
|
|
38
38
|
this._log("INFO", message);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
warn(message) {
|
|
42
|
+
this._log("WARN", message);
|
|
43
|
+
}
|
|
44
|
+
|
|
41
45
|
error(message, error) {
|
|
42
46
|
let msg = message;
|
|
43
47
|
if (error) {
|
|
@@ -68,6 +72,7 @@ class Logger {
|
|
|
68
72
|
// Console output
|
|
69
73
|
const isDebug = level === "DEBUG";
|
|
70
74
|
const isError = level === "ERROR";
|
|
75
|
+
const isWarn = level === "WARN";
|
|
71
76
|
|
|
72
77
|
// If it's debug and debug mode is off, skip console
|
|
73
78
|
if (isDebug && !this.config?.debug) return;
|
|
@@ -75,10 +80,10 @@ class Logger {
|
|
|
75
80
|
if (this.progressDisplay) {
|
|
76
81
|
this.progressDisplay.log(logLine);
|
|
77
82
|
} else {
|
|
78
|
-
if (isError) {
|
|
83
|
+
if (isError || isWarn) {
|
|
79
84
|
console.error(logLine);
|
|
80
85
|
} else {
|
|
81
|
-
console.
|
|
86
|
+
console.log(logLine);
|
|
82
87
|
}
|
|
83
88
|
}
|
|
84
89
|
}
|
package/src/progress-ui.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { logger } from "./logger.js";
|
|
2
|
+
|
|
1
3
|
function clampProgress(value) {
|
|
2
4
|
if (!Number.isFinite(value)) {
|
|
3
5
|
return 0;
|
|
@@ -19,6 +21,7 @@ class ProgressItem {
|
|
|
19
21
|
start(stage = "starting") {
|
|
20
22
|
this.active = true;
|
|
21
23
|
this.stage = stage;
|
|
24
|
+
logger.debug(`${this.label} batch start: ${stage}`);
|
|
22
25
|
this.writeStatus();
|
|
23
26
|
}
|
|
24
27
|
|
|
@@ -27,21 +30,28 @@ class ProgressItem {
|
|
|
27
30
|
|
|
28
31
|
if (stage) {
|
|
29
32
|
this.stage = stage;
|
|
33
|
+
logger.debug(`${this.label} stage: ${stage} (${Math.round(this.progress * 100)}%)`);
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
this.writeStatus();
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
log(message) {
|
|
40
|
+
// We don't log to file here because usually progress.log() is called alongside logger.info()
|
|
41
|
+
// or we want the caller to decide whether it goes to the log file.
|
|
36
42
|
this.display.writeLine(message);
|
|
37
43
|
}
|
|
38
44
|
|
|
39
45
|
succeed(message) {
|
|
40
|
-
|
|
46
|
+
const finalMsg = message || `${this.label} complete`;
|
|
47
|
+
this.finish("[done]", 1, finalMsg);
|
|
48
|
+
logger.debug(`${this.label} batch succeed: ${finalMsg}`);
|
|
41
49
|
}
|
|
42
50
|
|
|
43
51
|
fail(message) {
|
|
44
|
-
|
|
52
|
+
const finalMsg = message || `${this.label} failed`;
|
|
53
|
+
this.finish("[fail]", this.progress, finalMsg);
|
|
54
|
+
logger.error(`${this.label} batch fail: ${finalMsg}`);
|
|
45
55
|
}
|
|
46
56
|
|
|
47
57
|
finish(prefix, progress, message) {
|
package/src/review-runner.js
CHANGED
|
@@ -18,6 +18,9 @@ const DIFF_LIMITS = {
|
|
|
18
18
|
tailLines: 200
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
+
const CORE_REVIEW_INSTRUCTION =
|
|
22
|
+
"Please strictly review the current changes, prioritizing bugs, regression risks, compatibility issues, security concerns, boundary condition flaws, and missing tests. Please use Markdown for your response. If no clear flaws are found, write \"No clear flaws found\" and supplement with residual risks.";
|
|
23
|
+
|
|
21
24
|
|
|
22
25
|
function estimateTokenCount(text) {
|
|
23
26
|
if (!text) {
|
|
@@ -419,7 +422,13 @@ function buildPrompt(config, backend, targetInfo, details, reviewDiffPayload) {
|
|
|
419
422
|
const workspaceRoot = getReviewWorkspaceRoot(config, backend, targetInfo);
|
|
420
423
|
const canReadRelatedFiles = backend.kind === "git" || Boolean(targetInfo.workingCopyPath);
|
|
421
424
|
|
|
425
|
+
const langInstruction = config.resolvedLang === "zh"
|
|
426
|
+
? "Please use Simplified Chinese for your response."
|
|
427
|
+
: `Please use ${config.resolvedLang || "English"} for your response.`;
|
|
428
|
+
|
|
422
429
|
return [
|
|
430
|
+
CORE_REVIEW_INSTRUCTION,
|
|
431
|
+
langInstruction,
|
|
423
432
|
config.prompt,
|
|
424
433
|
canReadRelatedFiles
|
|
425
434
|
? `You are running inside a read-only workspace rooted at: ${workspaceRoot}`
|
|
@@ -437,7 +446,7 @@ function buildPrompt(config, backend, targetInfo, details, reviewDiffPayload) {
|
|
|
437
446
|
reviewDiffPayload.wasTruncated
|
|
438
447
|
? `Diff delivery note: the diff was truncated before being sent to the reviewer to stay within configured size limits. Original diff size was ${reviewDiffPayload.originalLineCount} lines / ${reviewDiffPayload.originalCharCount} chars, and the included excerpt is ${reviewDiffPayload.outputLineCount} lines / ${reviewDiffPayload.outputCharCount} chars. Use the changed file list and inspect related workspace files when needed.`
|
|
439
448
|
: `Diff delivery note: the full diff is included. Size is ${reviewDiffPayload.originalLineCount} lines / ${reviewDiffPayload.originalCharCount} chars.`
|
|
440
|
-
].join("\n\n");
|
|
449
|
+
].filter(Boolean).join("\n\n");
|
|
441
450
|
}
|
|
442
451
|
|
|
443
452
|
function formatTokenUsage(tokenUsage) {
|
|
@@ -582,6 +591,8 @@ function buildStateSnapshot(backend, targetInfo, changeId) {
|
|
|
582
591
|
}
|
|
583
592
|
|
|
584
593
|
async function reviewChange(config, backend, targetInfo, changeId, progress) {
|
|
594
|
+
const displayId = backend.formatChangeId(changeId);
|
|
595
|
+
logger.info(`Starting review for ${backend.changeName} ${displayId}`);
|
|
585
596
|
progress?.update(0.05, "loading change details");
|
|
586
597
|
const details = await backend.getChangeDetails(config, targetInfo, changeId);
|
|
587
598
|
|
|
@@ -650,7 +661,8 @@ async function reviewChange(config, backend, targetInfo, changeId, progress) {
|
|
|
650
661
|
}
|
|
651
662
|
|
|
652
663
|
if (reviewerName !== reviewersToTry[reviewersToTry.length - 1]) {
|
|
653
|
-
|
|
664
|
+
const msg = `${reviewer.displayName} failed for ${details.displayId}; trying next reviewer...`;
|
|
665
|
+
logger.warn(msg);
|
|
654
666
|
}
|
|
655
667
|
}
|
|
656
668
|
|
|
@@ -679,6 +691,13 @@ async function reviewChange(config, backend, targetInfo, changeId, progress) {
|
|
|
679
691
|
);
|
|
680
692
|
}
|
|
681
693
|
|
|
694
|
+
const outputLabels = [
|
|
695
|
+
shouldWriteFormat(config, "markdown") ? `md: ${outputFile}` : null,
|
|
696
|
+
shouldWriteFormat(config, "json") ? `json: ${jsonOutputFile}` : null
|
|
697
|
+
].filter(Boolean);
|
|
698
|
+
|
|
699
|
+
logger.info(`Completed review for ${displayId}: ${outputLabels.join(" | ") || "(no report file generated)"}`);
|
|
700
|
+
|
|
682
701
|
return {
|
|
683
702
|
success: true,
|
|
684
703
|
outputFile: shouldWriteFormat(config, "markdown") ? outputFile : null,
|
|
@@ -772,15 +791,10 @@ export async function runReviewCycle(config) {
|
|
|
772
791
|
throw error;
|
|
773
792
|
}
|
|
774
793
|
|
|
775
|
-
const outputLabels = [
|
|
776
|
-
result.outputFile ? `md: ${result.outputFile}` : null,
|
|
777
|
-
result.jsonOutputFile ? `json: ${result.jsonOutputFile}` : null
|
|
778
|
-
].filter(Boolean);
|
|
779
794
|
const nextProjectState = buildStateSnapshot(backend, targetInfo, changeId);
|
|
780
795
|
await saveState(config.stateFilePath, updateProjectState(stateFile, targetInfo, nextProjectState));
|
|
781
796
|
stateFile.projects[targetInfo.stateKey] = nextProjectState;
|
|
782
797
|
logger.debug(`Saved checkpoint for ${backend.formatChangeId(changeId)} to ${config.stateFilePath}.`);
|
|
783
|
-
progress.log(`[done] reviewed ${displayId}: ${outputLabels.join(" | ") || "(no report file generated)"}`);
|
|
784
798
|
updateOverallProgress(progress, index + 1, changeIdsToReview.length, 0, `finished ${displayId}`);
|
|
785
799
|
}
|
|
786
800
|
|
package/src/shell.js
CHANGED
|
@@ -25,7 +25,9 @@ export async function runCommand(command, args = [], options = {}) {
|
|
|
25
25
|
} = options;
|
|
26
26
|
|
|
27
27
|
logger.debug(
|
|
28
|
-
`run: ${command} ${args.join(" ")}${cwd ? ` | cwd=${cwd}` : ""}${timeoutMs > 0 ? ` | timeoutMs=${timeoutMs}` : ""}
|
|
28
|
+
`run: ${command} ${args.join(" ")}${cwd ? ` | cwd=${cwd}` : ""}${timeoutMs > 0 ? ` | timeoutMs=${timeoutMs}` : ""}${
|
|
29
|
+
input ? ` | input=${summarizeOutput(input)}` : ""
|
|
30
|
+
}`
|
|
29
31
|
);
|
|
30
32
|
|
|
31
33
|
return await new Promise((resolve, reject) => {
|
|
@@ -51,7 +53,10 @@ export async function runCommand(command, args = [], options = {}) {
|
|
|
51
53
|
stderrChunks.push(Buffer.from(chunk));
|
|
52
54
|
});
|
|
53
55
|
|
|
54
|
-
child.on("error",
|
|
56
|
+
child.on("error", (err) => {
|
|
57
|
+
logger.error(`spawn error: ${command}`, err);
|
|
58
|
+
reject(err);
|
|
59
|
+
});
|
|
55
60
|
|
|
56
61
|
child.on("close", (code) => {
|
|
57
62
|
if (timer) {
|
|
@@ -67,9 +72,14 @@ export async function runCommand(command, args = [], options = {}) {
|
|
|
67
72
|
stderr: trim ? stderr.trim() : stderr
|
|
68
73
|
};
|
|
69
74
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
75
|
+
const level = (result.code !== 0 || result.timedOut) && !allowFailure ? "ERROR" : "DEBUG";
|
|
76
|
+
const exitMsg = `exit: ${command} code=${result.code} timedOut=${result.timedOut} stdout=${summarizeOutput(result.stdout)} stderr=${summarizeOutput(result.stderr)}`;
|
|
77
|
+
|
|
78
|
+
if (level === "ERROR") {
|
|
79
|
+
logger.error(exitMsg);
|
|
80
|
+
} else {
|
|
81
|
+
logger.debug(exitMsg);
|
|
82
|
+
}
|
|
73
83
|
|
|
74
84
|
if ((result.code !== 0 || result.timedOut) && !allowFailure) {
|
|
75
85
|
const error = new Error(
|