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 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`: saved into the report as review context
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kodevu",
3
- "version": "0.1.26",
3
+ "version": "0.1.28",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "Poll SVN revisions or Git commits, send each change diff to a reviewer CLI, and write configurable review reports.",
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
@@ -49,6 +49,7 @@ try {
49
49
  reviewerWasAutoSelected: config.reviewerWasAutoSelected,
50
50
  target: config.target,
51
51
  outputDir: config.outputDir,
52
+ lang: config.lang,
52
53
  debug: config.debug
53
54
  })}`
54
55
  );
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.error(logLine);
86
+ console.log(logLine);
82
87
  }
83
88
  }
84
89
  }
@@ -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
- this.finish("[done]", 1, message || `${this.label} complete`);
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
- this.finish("[fail]", this.progress, message || `${this.label} failed`);
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) {
@@ -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
- progress?.log(`${reviewer.displayName} failed for ${details.displayId}; trying next reviewer...`);
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", reject);
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
- logger.debug(
71
- `exit: ${command} code=${result.code} timedOut=${result.timedOut} stdout=${summarizeOutput(result.stdout)} stderr=${summarizeOutput(result.stderr)}`
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(