kodevu 0.1.62 → 0.1.64
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 +10 -0
- package/package.json +1 -1
- package/src/config.js +6 -6
- package/src/logger.js +63 -85
- package/src/utils.js +1 -0
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
|
package/package.json
CHANGED
package/src/config.js
CHANGED
|
@@ -139,7 +139,7 @@ async function resolveAutoReviewers(debug) {
|
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
if (availableReviewers.length === 0) {
|
|
142
|
-
throw
|
|
142
|
+
throw mkCliError(`No reviewer CLI found in PATH. Install one of: ${AUTO_SUPPORTED_REVIEWERS.join(", ")}`);
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
// Shuffle for variety
|
|
@@ -335,11 +335,11 @@ export async function resolveConfig(cliArgs = {}) {
|
|
|
335
335
|
}
|
|
336
336
|
|
|
337
337
|
if (cliArgs.rev && cliArgs.last) {
|
|
338
|
-
throw
|
|
338
|
+
throw mkCliError("Parameters --rev and --last are mutually exclusive. Please specify only one.");
|
|
339
339
|
}
|
|
340
340
|
|
|
341
341
|
if (cliArgs.uncommitted && (cliArgs.rev || cliArgs.last)) {
|
|
342
|
-
throw
|
|
342
|
+
throw mkCliError("Parameter --uncommitted is mutually exclusive with --rev and --last.");
|
|
343
343
|
}
|
|
344
344
|
|
|
345
345
|
if (!config.target) {
|
|
@@ -358,7 +358,7 @@ export async function resolveConfig(cliArgs = {}) {
|
|
|
358
358
|
try {
|
|
359
359
|
config.prompt = await fs.readFile(promptPath, "utf8");
|
|
360
360
|
} catch (err) {
|
|
361
|
-
throw
|
|
361
|
+
throw mkCliError(`Failed to read prompt file: ${promptPath} (${err.message})`);
|
|
362
362
|
}
|
|
363
363
|
}
|
|
364
364
|
|
|
@@ -370,7 +370,7 @@ export async function resolveConfig(cliArgs = {}) {
|
|
|
370
370
|
config.fallbackReviewers = availableReviewers.map(r => r.reviewerName).slice(1);
|
|
371
371
|
config.reviewerWasAutoSelected = true;
|
|
372
372
|
} else if (!SUPPORTED_REVIEWERS.includes(config.reviewer)) {
|
|
373
|
-
throw
|
|
373
|
+
throw mkCliError(`"reviewer" must be one of: ${SUPPORTED_REVIEWERS.join(", ")}, or "auto"`);
|
|
374
374
|
}
|
|
375
375
|
|
|
376
376
|
config.outputDir = resolvePath(config.outputDir);
|
|
@@ -391,7 +391,7 @@ export async function resolveConfig(cliArgs = {}) {
|
|
|
391
391
|
}
|
|
392
392
|
|
|
393
393
|
if (config.reviewer === "openai" && !config.openaiApiKey) {
|
|
394
|
-
throw
|
|
394
|
+
throw mkCliError('Reviewer "openai" requires an API key. Set KODEVU_OPENAI_API_KEY or pass --openai-api-key.');
|
|
395
395
|
}
|
|
396
396
|
|
|
397
397
|
return config;
|
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
|
-
|
|
6
|
-
if (value == null) {
|
|
7
|
-
return "";
|
|
8
|
-
}
|
|
5
|
+
// Top-level helpers moved into Logger as private methods (#name).
|
|
9
6
|
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
15
|
+
#formatValue(value) {
|
|
16
|
+
if (value == null) return "";
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
return JSON.stringify(value);
|
|
20
|
-
} catch {
|
|
21
|
-
return String(value);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
18
|
+
if (typeof value === "string") return value.trim();
|
|
24
19
|
|
|
25
|
-
|
|
26
|
-
const parts = [];
|
|
20
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
27
21
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
22
|
+
try {
|
|
23
|
+
return JSON.stringify(value);
|
|
24
|
+
} catch {
|
|
25
|
+
return String(value);
|
|
31
26
|
}
|
|
27
|
+
}
|
|
32
28
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
continue;
|
|
36
|
-
}
|
|
29
|
+
#formatMeta(meta = {}) {
|
|
30
|
+
const parts = [];
|
|
37
31
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
continue;
|
|
41
|
-
}
|
|
32
|
+
for (const [key, rawValue] of Object.entries(meta || {})) {
|
|
33
|
+
if (rawValue == null || rawValue === "") continue;
|
|
42
34
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
continue;
|
|
46
|
-
}
|
|
35
|
+
const value = this.#formatValue(rawValue);
|
|
36
|
+
if (!value) continue;
|
|
47
37
|
|
|
48
|
-
|
|
49
|
-
|
|
38
|
+
if (typeof rawValue === "string") {
|
|
39
|
+
parts.push(`${key}=${JSON.stringify(value)}`);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
50
42
|
|
|
51
|
-
|
|
52
|
-
}
|
|
43
|
+
if (typeof rawValue === "number" || typeof rawValue === "boolean") {
|
|
44
|
+
parts.push(`${key}=${rawValue}`);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
53
47
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return "";
|
|
57
|
-
}
|
|
48
|
+
parts.push(`${key}=${value}`);
|
|
49
|
+
}
|
|
58
50
|
|
|
59
|
-
|
|
60
|
-
return error.stack || error.message || String(error);
|
|
51
|
+
return parts.join(" ");
|
|
61
52
|
}
|
|
62
53
|
|
|
63
|
-
|
|
64
|
-
return
|
|
65
|
-
}
|
|
54
|
+
#formatErrorDetails(error) {
|
|
55
|
+
if (!error) return "";
|
|
66
56
|
|
|
67
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
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
|
|
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
|
|
91
|
+
this.#log("INFO", message, meta);
|
|
106
92
|
}
|
|
107
93
|
|
|
108
94
|
warn(message, meta) {
|
|
109
|
-
this
|
|
95
|
+
this.#log("WARN", message, { ...meta, console: meta?.console ?? true });
|
|
110
96
|
}
|
|
111
97
|
|
|
112
98
|
error(message, error, meta) {
|
|
113
|
-
this
|
|
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
|
|
107
|
+
this.#log("DEBUG", message, meta);
|
|
122
108
|
}
|
|
123
109
|
|
|
124
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
#cleanupOldLogs(logsDir) {
|
|
189
167
|
try {
|
|
190
168
|
const files = fs.readdirSync(logsDir);
|
|
191
169
|
const now = Date.now();
|
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");
|