kodevu 0.1.23 → 0.1.25

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 gyt
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -111,3 +111,7 @@ Internal defaults:
111
111
  - `~/.kodevu/state.json` stores per-project checkpoints keyed by repository identity; only the v2 multi-project structure is supported.
112
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
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`.
114
+
115
+ ## License
116
+
117
+ MIT
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "kodevu",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
+ "license": "MIT",
4
5
  "type": "module",
5
6
  "description": "Poll SVN revisions or Git commits, send each change diff to a reviewer CLI, and write configurable review reports.",
6
7
  "bin": {
package/src/config.js CHANGED
@@ -11,6 +11,7 @@ const defaultConfig = {
11
11
  target: "",
12
12
  outputDir: defaultStorageDir,
13
13
  stateFilePath: path.join(defaultStorageDir, "state.json"),
14
+ logsDir: path.join(defaultStorageDir, "logs"),
14
15
  commandTimeoutMs: 600000,
15
16
  prompt:
16
17
  "请严格审查当前变更,优先指出 bug、回归风险、兼容性问题、安全问题、边界条件缺陷和缺失测试。请使用简体中文输出 Markdown;如果没有明确缺陷,请写“未发现明确缺陷”,并补充剩余风险。",
@@ -24,6 +25,7 @@ const configTemplate = {
24
25
  prompt: defaultConfig.prompt,
25
26
  outputDir: "~/.kodevu",
26
27
  stateFilePath: "~/.kodevu/state.json",
28
+ logsDir: "~/.kodevu/logs",
27
29
  commandTimeoutMs: defaultConfig.commandTimeoutMs,
28
30
  maxRevisionsPerRun: defaultConfig.maxRevisionsPerRun,
29
31
  outputFormats: defaultConfig.outputFormats
@@ -231,6 +233,7 @@ export async function loadConfig(configPath, cliArgs = {}) {
231
233
  config.baseDir = baseDir;
232
234
  config.outputDir = resolveConfigPath(config.baseDir, config.outputDir);
233
235
  config.stateFilePath = resolveConfigPath(config.baseDir, config.stateFilePath);
236
+ config.logsDir = resolveConfigPath(config.baseDir, config.logsDir);
234
237
  config.maxRevisionsPerRun = Number(config.maxRevisionsPerRun);
235
238
  config.commandTimeoutMs = Number(config.commandTimeoutMs);
236
239
  config.outputFormats = normalizeOutputFormats(config.outputFormats, loadedConfigPath);
package/src/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { initConfig, loadConfig, parseCliArgs, printHelp } from "./config.js";
4
4
  import { runReviewCycle } from "./review-runner.js";
5
+ import { logger } from "./logger.js";
5
6
 
6
7
  let cliArgs;
7
8
 
@@ -29,32 +30,36 @@ if (cliArgs.command === "init") {
29
30
  }
30
31
  }
31
32
 
32
- const config = await loadConfig(cliArgs.configPath, cliArgs);
33
+ try {
34
+ const config = await loadConfig(cliArgs.configPath, cliArgs);
35
+ logger.init(config);
33
36
 
34
- if (config.reviewerWasAutoSelected) {
35
- console.log(
36
- `Reviewer "auto" selected ${config.reviewer}${config.reviewerCommandPath ? ` (${config.reviewerCommandPath})` : ""}.`
37
- );
38
- }
37
+ if (config.reviewerWasAutoSelected) {
38
+ logger.info(
39
+ `Reviewer "auto" selected ${config.reviewer}${config.reviewerCommandPath ? ` (${config.reviewerCommandPath})` : ""}.`
40
+ );
41
+ }
39
42
 
40
- if (config.debug) {
41
- console.error(
42
- `[debug] Loaded config: ${JSON.stringify({
43
- configPath: config.configPath,
44
- reviewer: config.reviewer,
45
- reviewerCommandPath: config.reviewerCommandPath,
46
- reviewerWasAutoSelected: config.reviewerWasAutoSelected,
47
- target: config.target,
48
- outputDir: config.outputDir,
49
- debug: config.debug
50
- })}`
51
- );
52
- }
43
+ if (config.debug) {
44
+ logger.debug(
45
+ `Loaded config: ${JSON.stringify({
46
+ configPath: config.configPath,
47
+ reviewer: config.reviewer,
48
+ reviewerCommandPath: config.reviewerCommandPath,
49
+ reviewerWasAutoSelected: config.reviewerWasAutoSelected,
50
+ target: config.target,
51
+ outputDir: config.outputDir,
52
+ debug: config.debug
53
+ })}`
54
+ );
55
+ }
53
56
 
54
- try {
57
+ logger.info(`Session started. Target: ${config.target}`);
55
58
  await runReviewCycle(config);
59
+ logger.info("Session completed successfully.");
56
60
  } catch (error) {
57
- console.error(error?.stack || String(error));
61
+ // If config was loaded, logger might be initialized, otherwise it will fall back to stderr
62
+ logger.error("Session failed with error", error);
58
63
  process.exitCode = 1;
59
64
  }
60
65
  process.exit(process.exitCode || 0);
package/src/logger.js ADDED
@@ -0,0 +1,107 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ class Logger {
5
+ constructor() {
6
+ this.config = null;
7
+ this.logFile = null;
8
+ this.progressDisplay = null;
9
+ this.initialized = false;
10
+ }
11
+
12
+ init(config) {
13
+ if (this.initialized) return;
14
+ this.config = config;
15
+
16
+ if (config.logsDir) {
17
+ try {
18
+ if (!fs.existsSync(config.logsDir)) {
19
+ fs.mkdirSync(config.logsDir, { recursive: true });
20
+ }
21
+ const date = new Date().toISOString().split("T")[0];
22
+ this.logFile = path.join(config.logsDir, `run-${date}.log`);
23
+
24
+ // Simple rotation: Clean up logs older than 7 days
25
+ this._cleanupOldLogs(config.logsDir);
26
+ this.initialized = true;
27
+ } catch (err) {
28
+ console.error(`[logger] Failed to initialize log file: ${err.message}`);
29
+ }
30
+ }
31
+ }
32
+
33
+ setProgressDisplay(pd) {
34
+ this.progressDisplay = pd;
35
+ }
36
+
37
+ info(message) {
38
+ this._log("INFO", message);
39
+ }
40
+
41
+ error(message, error) {
42
+ let msg = message;
43
+ if (error) {
44
+ msg += `\n${error.stack || error}`;
45
+ }
46
+ this._log("ERROR", msg);
47
+ }
48
+
49
+ debug(message) {
50
+ if (this.config?.debug) {
51
+ this._log("DEBUG", message);
52
+ }
53
+ }
54
+
55
+ _log(level, message) {
56
+ const timestamp = new Date().toISOString();
57
+ const logLine = `[${timestamp}] [${level}] ${message}`;
58
+
59
+ // Write to file
60
+ if (this.logFile) {
61
+ try {
62
+ fs.appendFileSync(this.logFile, logLine + "\n");
63
+ } catch (err) {
64
+ // Ignore file errors during logging to prevent crashes
65
+ }
66
+ }
67
+
68
+ // Console output
69
+ const isDebug = level === "DEBUG";
70
+ const isError = level === "ERROR";
71
+
72
+ // If it's debug and debug mode is off, skip console
73
+ if (isDebug && !this.config?.debug) return;
74
+
75
+ if (this.progressDisplay) {
76
+ this.progressDisplay.log(logLine);
77
+ } else {
78
+ if (isError) {
79
+ console.error(logLine);
80
+ } else {
81
+ console.error(logLine);
82
+ }
83
+ }
84
+ }
85
+
86
+ _cleanupOldLogs(logsDir) {
87
+ try {
88
+ const files = fs.readdirSync(logsDir);
89
+ const now = Date.now();
90
+ const MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
91
+
92
+ for (const file of files) {
93
+ if (file.startsWith("run-") && file.endsWith(".log")) {
94
+ const filePath = path.join(logsDir, file);
95
+ const stats = fs.statSync(filePath);
96
+ if (now - stats.mtimeMs > MAX_AGE_MS) {
97
+ fs.unlinkSync(filePath);
98
+ }
99
+ }
100
+ }
101
+ } catch (err) {
102
+ // Ignore cleanup errors
103
+ }
104
+ }
105
+ }
106
+
107
+ export const logger = new Logger();
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
  import { ProgressDisplay } from "./progress-ui.js";
5
5
  import { runCommand } from "./shell.js";
6
6
  import { resolveRepositoryContext } from "./vcs-client.js";
7
+ import { logger } from "./logger.js";
7
8
 
8
9
  const DIFF_LIMITS = {
9
10
  review: {
@@ -17,11 +18,6 @@ const DIFF_LIMITS = {
17
18
  tailLines: 200
18
19
  };
19
20
 
20
- function debugLog(config, message) {
21
- if (config.debug) {
22
- console.error(`[debug] ${message}`);
23
- }
24
- }
25
21
 
26
22
  function estimateTokenCount(text) {
27
23
  if (!text) {
@@ -634,7 +630,7 @@ async function reviewChange(config, backend, targetInfo, changeId, progress) {
634
630
 
635
631
  for (const reviewerName of reviewersToTry) {
636
632
  currentReviewerConfig = { ...config, reviewer: reviewerName };
637
- debugLog(config, `Trying reviewer: ${reviewerName}`);
633
+ logger.debug(`Trying reviewer: ${reviewerName}`);
638
634
  progress?.update(0.45, `running reviewer ${reviewerName}`);
639
635
 
640
636
  const res = await runReviewerPrompt(
@@ -659,7 +655,7 @@ async function reviewChange(config, backend, targetInfo, changeId, progress) {
659
655
  }
660
656
 
661
657
  progress?.update(0.82, "writing report");
662
- debugLog(config, `Token usage: input=${tokenUsage.inputTokens} output=${tokenUsage.outputTokens} total=${tokenUsage.totalTokens} source=${tokenUsage.source}`);
658
+ logger.debug(`Token usage: input=${tokenUsage.inputTokens} output=${tokenUsage.outputTokens} total=${tokenUsage.totalTokens} source=${tokenUsage.source}`);
663
659
  const report = buildReport(currentReviewerConfig, backend, targetInfo, details, diffPayloads, reviewer, reviewerResult, tokenUsage);
664
660
  const outputFile = path.join(config.outputDir, backend.getReportFileName(changeId));
665
661
  const jsonOutputFile = outputFile.replace(/\.md$/i, ".json");
@@ -708,16 +704,14 @@ export async function runReviewCycle(config) {
708
704
  await ensureDir(config.outputDir);
709
705
 
710
706
  const { backend, targetInfo } = await resolveRepositoryContext(config);
711
- debugLog(
712
- config,
707
+ logger.debug(
713
708
  `Resolved repository context: backend=${backend.kind} target=${targetInfo.targetDisplay || config.target} stateKey=${targetInfo.stateKey}`
714
709
  );
715
710
  const latestChangeId = await backend.getLatestChangeId(config, targetInfo);
716
711
  const stateFile = await loadState(config.stateFilePath);
717
712
  const projectState = getProjectState(stateFile, targetInfo);
718
713
  let lastReviewedId = readLastReviewedId(projectState, backend, targetInfo);
719
- debugLog(
720
- config,
714
+ logger.debug(
721
715
  `Checkpoint status: latest=${backend.formatChangeId(latestChangeId)} lastReviewed=${lastReviewedId ? backend.formatChangeId(lastReviewedId) : "(none)"}`
722
716
  );
723
717
 
@@ -725,7 +719,7 @@ export async function runReviewCycle(config) {
725
719
  const checkpointIsValid = await backend.isValidCheckpoint(config, targetInfo, lastReviewedId, latestChangeId);
726
720
 
727
721
  if (!checkpointIsValid) {
728
- console.log("Saved review state no longer matches repository history. Resetting checkpoint.");
722
+ logger.info("Saved review state no longer matches repository history. Resetting checkpoint.");
729
723
  lastReviewedId = null;
730
724
  }
731
725
  }
@@ -734,7 +728,7 @@ export async function runReviewCycle(config) {
734
728
 
735
729
  if (!lastReviewedId) {
736
730
  changeIdsToReview = [latestChangeId];
737
- console.log(`Initialized state to review the latest ${backend.changeName} ${backend.formatChangeId(latestChangeId)} first.`);
731
+ logger.info(`Initialized state to review the latest ${backend.changeName} ${backend.formatChangeId(latestChangeId)} first.`);
738
732
  } else {
739
733
  changeIdsToReview = await backend.getPendingChangeIds(
740
734
  config,
@@ -745,21 +739,22 @@ export async function runReviewCycle(config) {
745
739
  );
746
740
  }
747
741
 
748
- debugLog(config, `Planned ${changeIdsToReview.length} ${backend.changeName}(s) for this cycle.`);
742
+ logger.debug(`Planned ${changeIdsToReview.length} ${backend.changeName}(s) for this cycle.`);
749
743
 
750
744
  if (changeIdsToReview.length === 0) {
751
745
  const lastKnownId = lastReviewedId ? backend.formatChangeId(lastReviewedId) : "(none)";
752
- console.log(`No new ${backend.changeName}s. Last reviewed: ${lastKnownId}`);
746
+ logger.info(`No new ${backend.changeName}s. Last reviewed: ${lastKnownId}`);
753
747
  return;
754
748
  }
755
749
 
756
- console.log(`Reviewing ${backend.displayName} ${backend.changeName}s ${formatChangeList(backend, changeIdsToReview)}`);
750
+ logger.info(`Reviewing ${backend.displayName} ${backend.changeName}s ${formatChangeList(backend, changeIdsToReview)}`);
757
751
  const progressDisplay = new ProgressDisplay();
752
+ logger.setProgressDisplay(progressDisplay);
758
753
  const progress = progressDisplay.createItem(`${backend.displayName} ${backend.changeName} batch`);
759
754
  progress.start("0/" + changeIdsToReview.length + " completed");
760
755
 
761
756
  for (const [index, changeId] of changeIdsToReview.entries()) {
762
- debugLog(config, `Starting review for ${backend.formatChangeId(changeId)}.`);
757
+ logger.debug(`Starting review for ${backend.formatChangeId(changeId)}.`);
763
758
  const displayId = backend.formatChangeId(changeId);
764
759
  updateOverallProgress(progress, index, changeIdsToReview.length, 0, `starting ${displayId}`);
765
760
 
@@ -784,7 +779,7 @@ export async function runReviewCycle(config) {
784
779
  const nextProjectState = buildStateSnapshot(backend, targetInfo, changeId);
785
780
  await saveState(config.stateFilePath, updateProjectState(stateFile, targetInfo, nextProjectState));
786
781
  stateFile.projects[targetInfo.stateKey] = nextProjectState;
787
- debugLog(config, `Saved checkpoint for ${backend.formatChangeId(changeId)} to ${config.stateFilePath}.`);
782
+ logger.debug(`Saved checkpoint for ${backend.formatChangeId(changeId)} to ${config.stateFilePath}.`);
788
783
  progress.log(`[done] reviewed ${displayId}: ${outputLabels.join(" | ") || "(no report file generated)"}`);
789
784
  updateOverallProgress(progress, index + 1, changeIdsToReview.length, 0, `finished ${displayId}`);
790
785
  }
@@ -800,6 +795,6 @@ export async function runReviewCycle(config) {
800
795
  );
801
796
 
802
797
  if (remainingChanges.length > 0) {
803
- console.log(`Backlog remains. Latest ${backend.changeName} is ${backend.formatChangeId(latestChangeId)}.`);
798
+ logger.info(`Backlog remains. Latest ${backend.changeName} is ${backend.formatChangeId(latestChangeId)}.`);
804
799
  }
805
800
  }
package/src/shell.js CHANGED
@@ -1,11 +1,7 @@
1
1
  import spawn from "cross-spawn";
2
2
  import iconv from "iconv-lite";
3
+ import { logger } from "./logger.js";
3
4
 
4
- function debugLog(enabled, message) {
5
- if (enabled) {
6
- console.error(`[debug] ${message}`);
7
- }
8
- }
9
5
 
10
6
  function summarizeOutput(text) {
11
7
  if (!text) {
@@ -28,8 +24,7 @@ export async function runCommand(command, args = [], options = {}) {
28
24
  debug = false
29
25
  } = options;
30
26
 
31
- debugLog(
32
- debug,
27
+ logger.debug(
33
28
  `run: ${command} ${args.join(" ")}${cwd ? ` | cwd=${cwd}` : ""}${timeoutMs > 0 ? ` | timeoutMs=${timeoutMs}` : ""}`
34
29
  );
35
30
 
@@ -72,8 +67,7 @@ export async function runCommand(command, args = [], options = {}) {
72
67
  stderr: trim ? stderr.trim() : stderr
73
68
  };
74
69
 
75
- debugLog(
76
- debug,
70
+ logger.debug(
77
71
  `exit: ${command} code=${result.code} timedOut=${result.timedOut} stdout=${summarizeOutput(result.stdout)} stderr=${summarizeOutput(result.stderr)}`
78
72
  );
79
73