kodevu 0.1.63 → 0.1.65

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
@@ -23,6 +23,16 @@ npx kodevu .
23
23
  Review reports are saved to `~/.kodevu/` by default.
24
24
  Console output is intentionally concise by default; detailed execution logs are written to `~/.kodevu/logs/`.
25
25
 
26
+ ## Install as an AI Agent Skill
27
+
28
+ Kodevu includes a natively supported `SKILL.md` file, which allows it to be installed as a specialized skill in AI agent coding assistants.
29
+
30
+ To install:
31
+
32
+ ```bash
33
+ npx skills add gyteng/kodevu
34
+ ```
35
+
26
36
  ## Usage
27
37
 
28
38
  ```bash
@@ -32,7 +42,7 @@ npx kodevu [target] [options]
32
42
  ### Options
33
43
 
34
44
  - `target`: Repository path (Git) or SVN URL/Working copy (default: `.`).
35
- - `--reviewer, -r`: `codex`, `gemini`, `copilot`, `openai`, or `auto` (default: `auto`).
45
+ - `--reviewer, -r`: `codex`, `gemini`, `copilot`, `openai`, `opencode` or `auto` (default: `auto`).
36
46
  - `--rev, -v`: A specific revision or commit hash to review.
37
47
  - `--last, -n`: Number of latest revisions to review (default: 1). Use negative values (e.g., `-3`) to review only the 3rd commit from the top.
38
48
  - `--uncommitted, -u`: Review current uncommitted changes in the target working tree.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kodevu",
3
- "version": "0.1.63",
3
+ "version": "0.1.65",
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
@@ -8,8 +8,8 @@ const require = createRequire(import.meta.url);
8
8
  const { version: packageVersion } = require("../package.json");
9
9
 
10
10
  const defaultStorageDir = path.join(os.homedir(), ".kodevu");
11
- const SUPPORTED_REVIEWERS = ["codex", "gemini", "copilot", "openai"];
12
- const AUTO_SUPPORTED_REVIEWERS = ["codex", "gemini", "copilot"];
11
+ const SUPPORTED_REVIEWERS = ["codex", "gemini", "copilot", "openai", "opencode"];
12
+ const AUTO_SUPPORTED_REVIEWERS = ["codex", "gemini", "copilot", "opencode"];
13
13
 
14
14
  const defaultConfig = {
15
15
  reviewer: "auto",
@@ -405,7 +405,7 @@ Usage:
405
405
 
406
406
  Options:
407
407
  --target, <path> Target repository path (default: current directory)
408
- --reviewer, -r Reviewer (codex | gemini | copilot | openai | auto, default: auto)
408
+ --reviewer, -r Reviewer (codex | gemini | copilot | openai | opencode | auto, default: auto)
409
409
  --prompt, -p Additional instructions or @file.txt to read from file
410
410
  --lang, -l Output language (e.g. zh, en, auto)
411
411
  --rev, -v Review specific revision(s), hashes, branches or ranges (comma-separated)
package/src/logger.js CHANGED
@@ -2,87 +2,73 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { formatDate } from "./utils.js";
4
4
 
5
- function formatValue(value) {
6
- if (value == null) {
7
- return "";
8
- }
5
+ // Top-level helpers moved into Logger as private methods (#name).
9
6
 
10
- if (typeof value === "string") {
11
- return value.trim();
7
+ class Logger {
8
+ constructor () {
9
+ this.config = null;
10
+ this.logFile = null;
11
+ this.sessionId = null;
12
+ this.initialized = false;
12
13
  }
13
14
 
14
- if (typeof value === "number" || typeof value === "boolean") {
15
- return String(value);
16
- }
15
+ #formatValue(value) {
16
+ if (value == null) return "";
17
17
 
18
- try {
19
- return JSON.stringify(value);
20
- } catch {
21
- return String(value);
22
- }
23
- }
18
+ if (typeof value === "string") return value.trim();
24
19
 
25
- function formatMeta(meta = {}) {
26
- const parts = [];
20
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
27
21
 
28
- for (const [key, rawValue] of Object.entries(meta)) {
29
- if (rawValue == null || rawValue === "") {
30
- continue;
22
+ try {
23
+ return JSON.stringify(value);
24
+ } catch {
25
+ return String(value);
31
26
  }
27
+ }
32
28
 
33
- const value = formatValue(rawValue);
34
- if (!value) {
35
- continue;
36
- }
29
+ #formatMeta(meta = {}) {
30
+ const parts = [];
37
31
 
38
- if (typeof rawValue === "string") {
39
- parts.push(`${key}=${JSON.stringify(value)}`);
40
- continue;
41
- }
32
+ for (const [key, rawValue] of Object.entries(meta || {})) {
33
+ if (rawValue == null || rawValue === "") continue;
42
34
 
43
- if (typeof rawValue === "number" || typeof rawValue === "boolean") {
44
- parts.push(`${key}=${String(rawValue)}`);
45
- continue;
46
- }
35
+ const value = this.#formatValue(rawValue);
36
+ if (!value) continue;
47
37
 
48
- parts.push(`${key}=${value}`);
49
- }
38
+ if (typeof rawValue === "string") {
39
+ parts.push(`${key}=${JSON.stringify(value)}`);
40
+ continue;
41
+ }
50
42
 
51
- return parts.join(" ");
52
- }
43
+ if (typeof rawValue === "number" || typeof rawValue === "boolean") {
44
+ parts.push(`${key}=${rawValue}`);
45
+ continue;
46
+ }
53
47
 
54
- function formatErrorDetails(error) {
55
- if (!error) {
56
- return "";
57
- }
48
+ parts.push(`${key}=${value}`);
49
+ }
58
50
 
59
- if (error instanceof Error) {
60
- return error.stack || error.message || String(error);
51
+ return parts.join(" ");
61
52
  }
62
53
 
63
- if (typeof error === "string") {
64
- return error;
65
- }
54
+ #formatErrorDetails(error) {
55
+ if (!error) return "";
66
56
 
67
- try {
68
- return JSON.stringify(error, null, 2);
69
- } catch {
70
- return String(error);
71
- }
72
- }
57
+ if (error instanceof Error) return error.stack ?? error.message ?? String(error);
73
58
 
74
- class Logger {
75
- constructor() {
76
- this.config = null;
77
- this.logFile = null;
78
- this.sessionId = null;
79
- this.initialized = false;
59
+ if (typeof error === "string") return error;
60
+
61
+ try {
62
+ return JSON.stringify(error, null, 2);
63
+ } catch {
64
+ return String(error);
65
+ }
80
66
  }
81
67
 
82
68
  init(config) {
83
69
  if (this.initialized) return;
84
70
  this.config = config;
85
- this.sessionId = this._createSessionId();
71
+ this.sessionId = this.#createSessionId();
86
72
 
87
73
  if (config.logsDir) {
88
74
  try {
@@ -93,7 +79,7 @@ class Logger {
93
79
  this.logFile = path.join(config.logsDir, `run-${date}.log`);
94
80
 
95
81
  // Simple rotation: Clean up logs older than 7 days
96
- this._cleanupOldLogs(config.logsDir);
82
+ this.#cleanupOldLogs(config.logsDir);
97
83
  this.initialized = true;
98
84
  } catch (err) {
99
85
  console.error(`[logger] Failed to initialize log file: ${err.message}`);
@@ -102,33 +88,33 @@ class Logger {
102
88
  }
103
89
 
104
90
  info(message, meta) {
105
- this._log("INFO", message, meta);
91
+ this.#log("INFO", message, meta);
106
92
  }
107
93
 
108
94
  warn(message, meta) {
109
- this._log("WARN", message, { ...meta, console: meta?.console ?? true });
95
+ this.#log("WARN", message, { ...meta, console: meta?.console ?? true });
110
96
  }
111
97
 
112
98
  error(message, error, meta) {
113
- this._log("ERROR", message, {
99
+ this.#log("ERROR", message, {
114
100
  ...meta,
115
101
  console: meta?.console ?? true,
116
- error: formatErrorDetails(error)
102
+ error: this.#formatErrorDetails(error)
117
103
  });
118
104
  }
119
105
 
120
106
  debug(message, meta) {
121
- this._log("DEBUG", message, meta);
107
+ this.#log("DEBUG", message, meta);
122
108
  }
123
109
 
124
- _log(level, message, meta = {}) {
110
+ #log(level, message, meta = {}) {
125
111
  const timestamp = formatDate(new Date());
126
112
  const { console: consoleMode, ...details } = meta;
127
113
  const fields = {
128
114
  session: this.sessionId || "uninitialized",
129
115
  ...details
130
116
  };
131
- const metaSuffix = formatMeta(fields);
117
+ const metaSuffix = this.#formatMeta(fields);
132
118
  const logLine = `[${timestamp}] [${level}] ${message}${metaSuffix ? ` | ${metaSuffix}` : ""}`;
133
119
 
134
120
  if (this.logFile) {
@@ -139,7 +125,7 @@ class Logger {
139
125
  }
140
126
  }
141
127
 
142
- if (!this._shouldWriteToConsole(level, consoleMode)) {
128
+ if (!this.#shouldWriteToConsole(level, consoleMode)) {
143
129
  return;
144
130
  }
145
131
 
@@ -150,34 +136,24 @@ class Logger {
150
136
  }
151
137
  }
152
138
 
153
- _shouldWriteToConsole(level, consoleMode) {
154
- if (consoleMode === false) {
155
- return false;
156
- }
139
+ #shouldWriteToConsole(level, consoleMode) {
140
+ if (consoleMode === false) return false;
157
141
 
158
- if (level === "ERROR" || level === "WARN") {
159
- return true;
160
- }
142
+ if (level === "ERROR" || level === "WARN") return true;
161
143
 
162
144
  if (level === "DEBUG") {
163
- if (consoleMode === true) {
164
- return Boolean(this.config?.debug);
165
- }
145
+ if (consoleMode === true) return Boolean(this.config?.debug);
166
146
  return false;
167
147
  }
168
148
 
169
- if (consoleMode === true) {
170
- return true;
171
- }
149
+ if (consoleMode === true) return true;
172
150
 
173
- if (consoleMode === "debug") {
174
- return Boolean(this.config?.debug);
175
- }
151
+ if (consoleMode === "debug") return Boolean(this.config?.debug);
176
152
 
177
153
  return false;
178
154
  }
179
155
 
180
- _createSessionId() {
156
+ #createSessionId() {
181
157
  return [
182
158
  Date.now().toString(36),
183
159
  process.pid.toString(36),
@@ -185,7 +161,9 @@ class Logger {
185
161
  ].join("-");
186
162
  }
187
163
 
188
- _cleanupOldLogs(logsDir) {
164
+
165
+
166
+ #cleanupOldLogs(logsDir) {
189
167
  try {
190
168
  const files = fs.readdirSync(logsDir);
191
169
  const now = Date.now();
package/src/reviewers.js CHANGED
@@ -165,6 +165,45 @@ export const REVIEWERS = {
165
165
  }
166
166
  }
167
167
  },
168
+ opencode: {
169
+ displayName: "OpenCode",
170
+ responseSectionTitle: "OpenCode Response",
171
+ emptyResponseText: "_No final response returned from opencode._",
172
+ async run(config, workingDir, promptText, diffText) {
173
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "kodevu-opencode-"));
174
+ const reviewInputFile = path.join(tempDir, "review-input.md");
175
+
176
+ try {
177
+ await fs.writeFile(
178
+ reviewInputFile,
179
+ [promptText, "### Unified Diff", "```diff", diffText, "```"].join("\n\n"),
180
+ "utf8"
181
+ );
182
+
183
+ const args = [
184
+ "run",
185
+ "Please perform the code review strictly following the instructions and unified diff provided in the attached file.",
186
+ "-f",
187
+ reviewInputFile,
188
+ "--pure"
189
+ ];
190
+
191
+ const execResult = await runCommand("opencode", args, {
192
+ cwd: workingDir,
193
+ allowFailure: true,
194
+ timeoutMs: config.commandTimeoutMs,
195
+ debug: config.debug
196
+ });
197
+
198
+ return {
199
+ ...execResult,
200
+ message: execResult.stdout
201
+ };
202
+ } finally {
203
+ await fs.rm(tempDir, { recursive: true, force: true });
204
+ }
205
+ }
206
+ },
168
207
  openai: {
169
208
  displayName: "OpenAI API",
170
209
  responseSectionTitle: "OpenAI Response",
package/src/utils.js CHANGED
@@ -55,6 +55,7 @@ export function formatDate(dateInput) {
55
55
 
56
56
  return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds} ${offset}`;
57
57
  }
58
+
58
59
  export function getTimestampPrefix() {
59
60
  const now = new Date();
60
61
  const pad = (n) => String(n).padStart(2, "0");