kodevu 0.1.27 → 0.1.29

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.27",
3
+ "version": "0.1.29",
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
 
@@ -0,0 +1,95 @@
1
+ import { countLines } from "./utils.js";
2
+
3
+ export const DIFF_LIMITS = {
4
+ review: {
5
+ maxLines: 4000,
6
+ maxChars: 120000
7
+ },
8
+ report: {
9
+ maxLines: 1500,
10
+ maxChars: 40000
11
+ },
12
+ tailLines: 200
13
+ };
14
+
15
+ export function trimBlockToChars(text, maxChars, keepTail = false) {
16
+ if (text.length <= maxChars) {
17
+ return text;
18
+ }
19
+
20
+ if (maxChars <= 3) {
21
+ return ".".repeat(Math.max(maxChars, 0));
22
+ }
23
+
24
+ return keepTail ? `...${text.slice(-(maxChars - 3))}` : `${text.slice(0, maxChars - 3)}...`;
25
+ }
26
+
27
+ export function truncateDiffText(diffText, maxLines, maxChars, tailLines, purposeLabel) {
28
+ const normalizedDiff = diffText.replace(/\r\n/g, "\n");
29
+ const originalLineCount = countLines(normalizedDiff);
30
+ const originalCharCount = normalizedDiff.length;
31
+
32
+ if (originalLineCount <= maxLines && originalCharCount <= maxChars) {
33
+ return {
34
+ text: diffText,
35
+ wasTruncated: false,
36
+ originalLineCount,
37
+ originalCharCount,
38
+ outputLineCount: originalLineCount,
39
+ outputCharCount: originalCharCount
40
+ };
41
+ }
42
+
43
+ const lines = normalizedDiff.split("\n");
44
+ const safeTailLines = Math.min(Math.max(tailLines, 0), Math.max(maxLines - 2, 0));
45
+ const headLineCount = Math.max(maxLines - safeTailLines - 1, 1);
46
+ let headBlock = lines.slice(0, headLineCount).join("\n");
47
+ let tailBlock = safeTailLines > 0 ? lines.slice(-safeTailLines).join("\n") : "";
48
+ const omittedLineCount = Math.max(originalLineCount - headLineCount - safeTailLines, 0);
49
+ const markerBlock = [
50
+ `... diff truncated for ${purposeLabel} ...`,
51
+ `original lines: ${originalLineCount}, original chars: ${originalCharCount}`,
52
+ `omitted lines: ${omittedLineCount}`
53
+ ].join("\n");
54
+
55
+ let truncatedText = [headBlock, markerBlock, tailBlock].filter(Boolean).join("\n");
56
+
57
+ if (truncatedText.length > maxChars) {
58
+ const reservedChars = markerBlock.length + (tailBlock ? 2 : 1);
59
+ const remainingChars = Math.max(maxChars - reservedChars, 0);
60
+ const headBudget = tailBlock ? Math.floor(remainingChars * 0.7) : remainingChars;
61
+ const tailBudget = tailBlock ? Math.max(remainingChars - headBudget, 0) : 0;
62
+ headBlock = trimBlockToChars(headBlock, headBudget, false);
63
+ tailBlock = trimBlockToChars(tailBlock, tailBudget, true);
64
+ truncatedText = [headBlock, markerBlock, tailBlock].filter(Boolean).join("\n");
65
+ }
66
+
67
+ return {
68
+ text: truncatedText,
69
+ wasTruncated: true,
70
+ originalLineCount,
71
+ originalCharCount,
72
+ outputLineCount: countLines(truncatedText),
73
+ outputCharCount: truncatedText.length
74
+ };
75
+ }
76
+
77
+ export function prepareDiffPayloads(config, diffText) {
78
+ return {
79
+ review: truncateDiffText(
80
+ diffText,
81
+ DIFF_LIMITS.review.maxLines,
82
+ DIFF_LIMITS.review.maxChars,
83
+ DIFF_LIMITS.tailLines,
84
+ "reviewer input"
85
+ ),
86
+ report: truncateDiffText(
87
+ diffText,
88
+ DIFF_LIMITS.report.maxLines,
89
+ DIFF_LIMITS.report.maxChars,
90
+ Math.min(DIFF_LIMITS.tailLines, DIFF_LIMITS.report.maxLines),
91
+ "report output"
92
+ )
93
+ };
94
+ }
95
+
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
  );
@@ -0,0 +1,185 @@
1
+ export const CORE_REVIEW_INSTRUCTION =
2
+ "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.";
3
+
4
+ export function getReviewWorkspaceRoot(config, backend, targetInfo) {
5
+ if (backend.kind === "git" && targetInfo.repoRootPath) {
6
+ return targetInfo.repoRootPath;
7
+ }
8
+
9
+ if (backend.kind === "svn" && targetInfo.workingCopyPath) {
10
+ return targetInfo.workingCopyPath;
11
+ }
12
+
13
+ return config.baseDir;
14
+ }
15
+
16
+ export function buildPrompt(config, backend, targetInfo, details, reviewDiffPayload) {
17
+ const fileList = details.changedPaths.map((item) => `${item.action} ${item.relativePath}`).join("\n");
18
+ const workspaceRoot = getReviewWorkspaceRoot(config, backend, targetInfo);
19
+ const canReadRelatedFiles = backend.kind === "git" || Boolean(targetInfo.workingCopyPath);
20
+
21
+ const langInstruction = config.resolvedLang === "zh"
22
+ ? "Please use Simplified Chinese for your response."
23
+ : `Please use ${config.resolvedLang || "English"} for your response.`;
24
+
25
+ return [
26
+ CORE_REVIEW_INSTRUCTION,
27
+ langInstruction,
28
+ config.prompt,
29
+ canReadRelatedFiles
30
+ ? `You are running inside a read-only workspace rooted at: ${workspaceRoot}`
31
+ : "No local repository workspace is available for this review run.",
32
+ canReadRelatedFiles
33
+ ? "Besides the diff below, you may read other related files in the workspace when needed to understand call sites, shared utilities, configuration, tests, or data flow. Do not modify files or rely on shell commands."
34
+ : "Review primarily from the diff below. Do not assume access to other local files, shell commands, or repository history.",
35
+ "Use plain text file references like path/to/file.js:123. Do not emit clickable workspace links.",
36
+ `Repository Type: ${backend.displayName}`,
37
+ `Change ID: ${details.displayId}`,
38
+ `Author: ${details.author}`,
39
+ `Date: ${details.date || "unknown"}`,
40
+ `Changed files:\n${fileList || "(none)"}`,
41
+ `Commit message:\n${details.message || "(empty)"}`,
42
+ reviewDiffPayload.wasTruncated
43
+ ? `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.`
44
+ : `Diff delivery note: the full diff is included. Size is ${reviewDiffPayload.originalLineCount} lines / ${reviewDiffPayload.originalCharCount} chars.`
45
+ ].filter(Boolean).join("\n\n");
46
+ }
47
+
48
+ export function formatTokenUsage(tokenUsage) {
49
+ const sourceLabel = tokenUsage.source === "reviewer" ? "reviewer reported" : "estimated (~4 chars/token)";
50
+ return [
51
+ `- Input Tokens: \`${tokenUsage.inputTokens}\``,
52
+ `- Output Tokens: \`${tokenUsage.outputTokens}\``,
53
+ `- Total Tokens: \`${tokenUsage.totalTokens}\``,
54
+ `- Token Source: \`${sourceLabel}\``
55
+ ].join("\n");
56
+ }
57
+
58
+ export function formatDiffHandling(diffPayload, label) {
59
+ return [
60
+ `- ${label} Original Lines: \`${diffPayload.originalLineCount}\``,
61
+ `- ${label} Original Chars: \`${diffPayload.originalCharCount}\``,
62
+ `- ${label} Included Lines: \`${diffPayload.outputLineCount}\``,
63
+ `- ${label} Included Chars: \`${diffPayload.outputCharCount}\``,
64
+ `- ${label} Truncated: \`${diffPayload.wasTruncated ? "yes" : "no"}\``
65
+ ].join("\n");
66
+ }
67
+
68
+ export function formatChangedPaths(changedPaths) {
69
+ if (changedPaths.length === 0) {
70
+ return "_No changed files captured._";
71
+ }
72
+
73
+ return changedPaths
74
+ .map((item) => {
75
+ const renameSuffix = item.previousPath ? ` (from ${item.previousPath})` : "";
76
+ return `- \`${item.action}\` ${item.relativePath}${renameSuffix}`;
77
+ })
78
+ .join("\n");
79
+ }
80
+
81
+ export function formatChangeList(backend, changeIds) {
82
+ return changeIds.map((changeId) => backend.formatChangeId(changeId)).join(", ");
83
+ }
84
+
85
+ export function shouldWriteFormat(config, format) {
86
+ return Array.isArray(config.outputFormats) && config.outputFormats.includes(format);
87
+ }
88
+
89
+ export function buildReport(config, backend, targetInfo, details, diffPayloads, reviewer, reviewerResult, tokenUsage) {
90
+ const lines = [
91
+ `# ${backend.displayName} Review Report: ${details.displayId}`,
92
+ "",
93
+ `- Repository Type: \`${backend.displayName}\``,
94
+ `- Target: \`${targetInfo.targetDisplay || config.target}\``,
95
+ `- Change ID: \`${details.displayId}\``,
96
+ `- Author: \`${details.author}\``,
97
+ `- Commit Date: \`${details.date || "unknown"}\``,
98
+ `- Generated At: \`${new Date().toISOString()}\``,
99
+ `- Reviewer: \`${reviewer.displayName}\``,
100
+ `- Reviewer Exit Code: \`${reviewerResult.code}\``,
101
+ `- Reviewer Timed Out: \`${reviewerResult.timedOut ? "yes" : "no"}\``,
102
+ "",
103
+ "## Token Usage",
104
+ "",
105
+ formatTokenUsage(tokenUsage),
106
+ "",
107
+ "## Changed Files",
108
+ "",
109
+ formatChangedPaths(details.changedPaths),
110
+ "",
111
+ "## Commit Message",
112
+ "",
113
+ details.message ? "```text\n" + details.message + "\n```" : "_Empty_",
114
+ "",
115
+ "## Review Context",
116
+ "",
117
+ "```text",
118
+ buildPrompt(config, backend, targetInfo, details, diffPayloads.review),
119
+ "```",
120
+ "",
121
+ "## Diff Handling",
122
+ "",
123
+ formatDiffHandling(diffPayloads.review, "Reviewer Input"),
124
+ formatDiffHandling(diffPayloads.report, "Report Diff"),
125
+ "",
126
+ "## Diff",
127
+ "",
128
+ "```diff",
129
+ diffPayloads.report.text.trim() || "(empty diff)",
130
+ "```",
131
+ "",
132
+ `## ${reviewer.responseSectionTitle}`,
133
+ "",
134
+ reviewerResult.message?.trim() ? reviewerResult.message.trim() : reviewer.emptyResponseText
135
+ ];
136
+
137
+ return `${lines.join("\n")}\n`;
138
+ }
139
+
140
+ export function buildJsonReport(config, backend, targetInfo, details, diffPayloads, reviewer, reviewerResult, tokenUsage) {
141
+ return {
142
+ repositoryType: backend.displayName,
143
+ target: targetInfo.targetDisplay || config.target,
144
+ changeId: details.displayId,
145
+ author: details.author,
146
+ commitDate: details.date || "unknown",
147
+ generatedAt: new Date().toISOString(),
148
+ reviewer: {
149
+ name: reviewer.displayName,
150
+ exitCode: reviewerResult.code,
151
+ timedOut: Boolean(reviewerResult.timedOut)
152
+ },
153
+ tokenUsage: {
154
+ inputTokens: tokenUsage.inputTokens,
155
+ outputTokens: tokenUsage.outputTokens,
156
+ totalTokens: tokenUsage.totalTokens,
157
+ source: tokenUsage.source
158
+ },
159
+ changedFiles: details.changedPaths.map((item) => ({
160
+ action: item.action,
161
+ path: item.relativePath,
162
+ previousPath: item.previousPath || null
163
+ })),
164
+ commitMessage: details.message || "",
165
+ reviewContext: buildPrompt(config, backend, targetInfo, details, diffPayloads.review),
166
+ diffHandling: {
167
+ reviewerInput: {
168
+ originalLines: diffPayloads.review.originalLineCount,
169
+ originalChars: diffPayloads.review.originalCharCount,
170
+ includedLines: diffPayloads.review.outputLineCount,
171
+ includedChars: diffPayloads.review.outputCharCount,
172
+ truncated: diffPayloads.review.wasTruncated
173
+ },
174
+ reportDiff: {
175
+ originalLines: diffPayloads.report.originalLineCount,
176
+ originalChars: diffPayloads.report.originalCharCount,
177
+ includedLines: diffPayloads.report.outputLineCount,
178
+ includedChars: diffPayloads.report.outputCharCount,
179
+ truncated: diffPayloads.report.wasTruncated
180
+ }
181
+ },
182
+ diff: diffPayloads.report.text.trim(),
183
+ reviewerResponse: reviewerResult.message?.trim() ? reviewerResult.message.trim() : reviewer.emptyResponseText
184
+ };
185
+ }