kodevu 0.1.54 → 0.1.56
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 +4 -8
- package/SKILL.md +27 -9
- package/package.json +1 -1
- package/src/config.js +1 -1
- package/src/index.js +45 -21
- package/src/logger.js +131 -26
- package/src/review-runner.js +67 -12
- package/src/shell.js +34 -12
- package/src/utils.js +2 -1
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Kodevu
|
|
2
2
|
|
|
3
|
+
> The name **Kodevu** is a phonetic play on "code review".
|
|
4
|
+
|
|
3
5
|
A Node.js tool that fetches Git commits or SVN revisions, sends the diff to a supported AI reviewer CLI, and writes review results to report files.
|
|
4
6
|
|
|
5
7
|
## Pure & Zero Config
|
|
@@ -19,6 +21,7 @@ npx kodevu .
|
|
|
19
21
|
```
|
|
20
22
|
|
|
21
23
|
Review reports are saved to `~/.kodevu/` by default.
|
|
24
|
+
Console output is intentionally concise by default; detailed execution logs are written to `~/.kodevu/logs/`.
|
|
22
25
|
|
|
23
26
|
## Usage
|
|
24
27
|
|
|
@@ -42,7 +45,7 @@ npx kodevu [target] [options]
|
|
|
42
45
|
- `--openai-model`: Model used when `--reviewer openai` (default: `gpt-5-mini`).
|
|
43
46
|
- `--openai-org`: Optional OpenAI organization ID.
|
|
44
47
|
- `--openai-project`: Optional OpenAI project ID.
|
|
45
|
-
- `--debug, -d`:
|
|
48
|
+
- `--debug, -d`: Show extra debug information on the console.
|
|
46
49
|
- `--version, -V`: Print the current version and exit.
|
|
47
50
|
|
|
48
51
|
> [!IMPORTANT]
|
|
@@ -127,13 +130,6 @@ npx kodevu . \
|
|
|
127
130
|
--openai-model gpt-5-mini
|
|
128
131
|
```
|
|
129
132
|
|
|
130
|
-
## How it Works
|
|
131
|
-
|
|
132
|
-
- **Git Targets**: `target` must be a local repository or subdirectory.
|
|
133
|
-
- **SVN Targets**: `target` can be a working copy path or repository URL.
|
|
134
|
-
- **Reviewer "auto"**: Probes `codex`, `gemini`, and `copilot` in your `PATH` and selects one.
|
|
135
|
-
- **Reviewer "openai"**: Calls the OpenAI Chat Completions API directly. `auto` does not select `openai`, so API-based use stays explicit.
|
|
136
|
-
- **Contextual Review**: For local repositories, the reviewer can inspect related files beyond the diff to provide deeper insights.
|
|
137
133
|
|
|
138
134
|
## License
|
|
139
135
|
|
package/SKILL.md
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: kodevu
|
|
3
|
-
description: A tool to fetch Git
|
|
3
|
+
description: A tool to fetch Git/SVN diffs, send them to an AI reviewer, and generate configurable code review reports.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Kodevu Skill
|
|
7
7
|
|
|
8
|
-
Kodevu is a Node.js tool that fetches Git commits or SVN revisions, sends the diff to a supported AI reviewer CLI, and writes review results to report files.
|
|
8
|
+
Kodevu is a Node.js tool that fetches Git commits or SVN revisions, sends the diff to a supported AI reviewer CLI, and writes review results to report files. It is designed to be **stateless** and requires **no configuration files**.
|
|
9
9
|
|
|
10
10
|
## Usage
|
|
11
11
|
|
|
12
|
-
Use `npx kodevu` to review a codebase.
|
|
12
|
+
Use `npx kodevu` to review a codebase.
|
|
13
13
|
|
|
14
14
|
### Reviewing the latest commit
|
|
15
15
|
|
|
@@ -29,13 +29,19 @@ npx kodevu . --rev <commit-hash>
|
|
|
29
29
|
npx kodevu . --last 3
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
+
### Reviewing uncommitted changes
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npx kodevu . --uncommitted
|
|
36
|
+
```
|
|
37
|
+
|
|
32
38
|
### Supported Reviewers
|
|
33
39
|
|
|
34
|
-
`kodevu` supports several AI reviewer
|
|
40
|
+
`kodevu` supports several AI reviewer backends: `auto`, `openai`, `gemini`, `codex`, `copilot`. The default is `auto`, which probes available CLI tools in your `PATH`.
|
|
35
41
|
|
|
36
42
|
Example using OpenAI:
|
|
37
43
|
```bash
|
|
38
|
-
npx kodevu . --reviewer openai --openai-api-key <YOUR_API_KEY> --openai-model gpt-
|
|
44
|
+
npx kodevu . --reviewer openai --openai-api-key <YOUR_API_KEY> --openai-model gpt-5-mini
|
|
39
45
|
```
|
|
40
46
|
|
|
41
47
|
### Generating JSON Reports
|
|
@@ -45,19 +51,31 @@ By default, review reports are generated as Markdown files in `~/.kodevu/`. You
|
|
|
45
51
|
npx kodevu . --format json --output ./reports
|
|
46
52
|
```
|
|
47
53
|
|
|
48
|
-
###
|
|
54
|
+
### Custom Prompts
|
|
49
55
|
|
|
50
|
-
You can provide
|
|
56
|
+
You can provide additional instructions to the reviewer using `--prompt`:
|
|
51
57
|
```bash
|
|
52
58
|
npx kodevu . --prompt "Focus on security issues and suggest optimizations."
|
|
53
59
|
```
|
|
54
60
|
Or from a file: `--prompt @my-rules.txt`
|
|
55
61
|
|
|
62
|
+
### Environment Variables
|
|
63
|
+
|
|
64
|
+
All options can also be set via environment variables to avoid repetitive flags:
|
|
65
|
+
|
|
66
|
+
- `KODEVU_REVIEWER` – Default reviewer.
|
|
67
|
+
- `KODEVU_LANG` – Default output language.
|
|
68
|
+
- `KODEVU_OUTPUT_DIR` – Default output directory.
|
|
69
|
+
- `KODEVU_PROMPT` – Default prompt instructions.
|
|
70
|
+
- `KODEVU_OPENAI_API_KEY` – API key for `openai`.
|
|
71
|
+
- `KODEVU_OPENAI_BASE_URL` – Base URL for `openai`.
|
|
72
|
+
- `KODEVU_OPENAI_MODEL` – Model for `openai`.
|
|
73
|
+
|
|
56
74
|
## Working with Target Repositories
|
|
57
75
|
|
|
58
|
-
- `target
|
|
76
|
+
- **Git**: `target` must be a local repository or subdirectory.
|
|
77
|
+
- **SVN**: `target` can be a working copy path or repository URL.
|
|
59
78
|
|
|
60
|
-
For example, to run on a specific path, you can use:
|
|
61
79
|
```bash
|
|
62
80
|
npx kodevu /path/to/project --last 1
|
|
63
81
|
```
|
package/package.json
CHANGED
package/src/config.js
CHANGED
|
@@ -373,7 +373,7 @@ Options:
|
|
|
373
373
|
--openai-model Model used when reviewer=openai (default: gpt-5-mini)
|
|
374
374
|
--openai-org Optional OpenAI organization ID
|
|
375
375
|
--openai-project Optional OpenAI project ID
|
|
376
|
-
--debug, -d
|
|
376
|
+
--debug, -d Show extra debug information on the console
|
|
377
377
|
--help, -h Show help
|
|
378
378
|
--version, -V Show version
|
|
379
379
|
|
package/src/index.js
CHANGED
|
@@ -30,33 +30,57 @@ try {
|
|
|
30
30
|
|
|
31
31
|
if (config.reviewerWasAutoSelected) {
|
|
32
32
|
logger.info(
|
|
33
|
-
`Reviewer "auto" selected ${config.reviewer}${config.reviewerCommandPath ? ` (${config.reviewerCommandPath})` : ""}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (config.debug) {
|
|
38
|
-
logger.debug(
|
|
39
|
-
`Resolved config: ${JSON.stringify({
|
|
33
|
+
`Reviewer "auto" selected ${config.reviewer}${config.reviewerCommandPath ? ` (${config.reviewerCommandPath})` : ""}.`,
|
|
34
|
+
{
|
|
35
|
+
scope: "session",
|
|
40
36
|
reviewer: config.reviewer,
|
|
41
|
-
reviewerCommandPath: config.reviewerCommandPath,
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
openaiModel: config.openaiModel,
|
|
45
|
-
openaiOrganization: config.openaiOrganization,
|
|
46
|
-
openaiProject: config.openaiProject,
|
|
47
|
-
target: config.target,
|
|
48
|
-
outputDir: config.outputDir,
|
|
49
|
-
lang: config.lang,
|
|
50
|
-
debug: config.debug
|
|
51
|
-
})}`
|
|
37
|
+
reviewerCommandPath: config.reviewerCommandPath || "",
|
|
38
|
+
console: true
|
|
39
|
+
}
|
|
52
40
|
);
|
|
53
41
|
}
|
|
54
42
|
|
|
55
|
-
logger.
|
|
43
|
+
logger.debug("Resolved config", {
|
|
44
|
+
scope: "session",
|
|
45
|
+
reviewer: config.reviewer,
|
|
46
|
+
reviewerCommandPath: config.reviewerCommandPath || "",
|
|
47
|
+
reviewerWasAutoSelected: config.reviewerWasAutoSelected || false,
|
|
48
|
+
openaiBaseUrl: config.openaiBaseUrl,
|
|
49
|
+
openaiModel: config.openaiModel,
|
|
50
|
+
openaiOrganization: config.openaiOrganization || "",
|
|
51
|
+
openaiProject: config.openaiProject || "",
|
|
52
|
+
target: config.target,
|
|
53
|
+
outputDir: config.outputDir,
|
|
54
|
+
lang: config.lang,
|
|
55
|
+
resolvedLang: config.resolvedLang,
|
|
56
|
+
debug: config.debug,
|
|
57
|
+
outputFormats: config.outputFormats,
|
|
58
|
+
mode: config.uncommitted ? "uncommitted" : config.rev ? "rev" : "last",
|
|
59
|
+
rev: config.rev || "",
|
|
60
|
+
last: config.last || 0,
|
|
61
|
+
console: "debug"
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
logger.info("Session started", {
|
|
65
|
+
scope: "session",
|
|
66
|
+
target: config.target,
|
|
67
|
+
reviewer: config.reviewer,
|
|
68
|
+
outputDir: config.outputDir,
|
|
69
|
+
mode: config.uncommitted ? "uncommitted" : config.rev ? "rev" : "last",
|
|
70
|
+
rev: config.rev || "",
|
|
71
|
+
last: config.last || 0
|
|
72
|
+
});
|
|
56
73
|
await runReviewCycle(config);
|
|
57
|
-
logger.info("Session completed successfully
|
|
74
|
+
logger.info("Session completed successfully", {
|
|
75
|
+
scope: "session",
|
|
76
|
+
target: config.target,
|
|
77
|
+
reviewer: config.reviewer
|
|
78
|
+
});
|
|
58
79
|
} catch (error) {
|
|
59
|
-
logger.error("Session failed
|
|
80
|
+
logger.error("Session failed", error, {
|
|
81
|
+
scope: "session",
|
|
82
|
+
console: true
|
|
83
|
+
});
|
|
60
84
|
process.exitCode = 1;
|
|
61
85
|
}
|
|
62
86
|
process.exit(process.exitCode || 0);
|
package/src/logger.js
CHANGED
|
@@ -2,16 +2,87 @@ 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
|
+
}
|
|
9
|
+
|
|
10
|
+
if (typeof value === "string") {
|
|
11
|
+
return value.trim();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
15
|
+
return String(value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
return JSON.stringify(value);
|
|
20
|
+
} catch {
|
|
21
|
+
return String(value);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function formatMeta(meta = {}) {
|
|
26
|
+
const parts = [];
|
|
27
|
+
|
|
28
|
+
for (const [key, rawValue] of Object.entries(meta)) {
|
|
29
|
+
if (rawValue == null || rawValue === "") {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const value = formatValue(rawValue);
|
|
34
|
+
if (!value) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (typeof rawValue === "string") {
|
|
39
|
+
parts.push(`${key}=${JSON.stringify(value)}`);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (typeof rawValue === "number" || typeof rawValue === "boolean") {
|
|
44
|
+
parts.push(`${key}=${String(rawValue)}`);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
parts.push(`${key}=${value}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return parts.join(" ");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function formatErrorDetails(error) {
|
|
55
|
+
if (!error) {
|
|
56
|
+
return "";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (error instanceof Error) {
|
|
60
|
+
return error.stack || error.message || String(error);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (typeof error === "string") {
|
|
64
|
+
return error;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
return JSON.stringify(error, null, 2);
|
|
69
|
+
} catch {
|
|
70
|
+
return String(error);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
5
74
|
class Logger {
|
|
6
75
|
constructor() {
|
|
7
76
|
this.config = null;
|
|
8
77
|
this.logFile = null;
|
|
78
|
+
this.sessionId = null;
|
|
9
79
|
this.initialized = false;
|
|
10
80
|
}
|
|
11
81
|
|
|
12
82
|
init(config) {
|
|
13
83
|
if (this.initialized) return;
|
|
14
84
|
this.config = config;
|
|
85
|
+
this.sessionId = this._createSessionId();
|
|
15
86
|
|
|
16
87
|
if (config.logsDir) {
|
|
17
88
|
try {
|
|
@@ -19,7 +90,7 @@ class Logger {
|
|
|
19
90
|
fs.mkdirSync(config.logsDir, { recursive: true });
|
|
20
91
|
}
|
|
21
92
|
const date = formatDate(new Date()).split(" ")[0];
|
|
22
|
-
this.logFile = path.join(config.logsDir, `run-${date}.log`);
|
|
93
|
+
this.logFile = path.join(config.logsDir, `run-${date}-${this.sessionId}.log`);
|
|
23
94
|
|
|
24
95
|
// Simple rotation: Clean up logs older than 7 days
|
|
25
96
|
this._cleanupOldLogs(config.logsDir);
|
|
@@ -30,33 +101,36 @@ class Logger {
|
|
|
30
101
|
}
|
|
31
102
|
}
|
|
32
103
|
|
|
33
|
-
info(message) {
|
|
34
|
-
this._log("INFO", message);
|
|
104
|
+
info(message, meta) {
|
|
105
|
+
this._log("INFO", message, meta);
|
|
35
106
|
}
|
|
36
107
|
|
|
37
|
-
warn(message) {
|
|
38
|
-
this._log("WARN", message);
|
|
108
|
+
warn(message, meta) {
|
|
109
|
+
this._log("WARN", message, { ...meta, console: meta?.console ?? true });
|
|
39
110
|
}
|
|
40
111
|
|
|
41
|
-
error(message, error) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
112
|
+
error(message, error, meta) {
|
|
113
|
+
this._log("ERROR", message, {
|
|
114
|
+
...meta,
|
|
115
|
+
console: meta?.console ?? true,
|
|
116
|
+
error: formatErrorDetails(error)
|
|
117
|
+
});
|
|
47
118
|
}
|
|
48
119
|
|
|
49
|
-
debug(message) {
|
|
50
|
-
|
|
51
|
-
this._log("DEBUG", message);
|
|
52
|
-
}
|
|
120
|
+
debug(message, meta) {
|
|
121
|
+
this._log("DEBUG", message, meta);
|
|
53
122
|
}
|
|
54
123
|
|
|
55
|
-
_log(level, message) {
|
|
124
|
+
_log(level, message, meta = {}) {
|
|
56
125
|
const timestamp = formatDate(new Date());
|
|
57
|
-
const
|
|
126
|
+
const { console: consoleMode, ...details } = meta;
|
|
127
|
+
const fields = {
|
|
128
|
+
session: this.sessionId || "uninitialized",
|
|
129
|
+
...details
|
|
130
|
+
};
|
|
131
|
+
const metaSuffix = formatMeta(fields);
|
|
132
|
+
const logLine = `[${timestamp}] [${level}] ${message}${metaSuffix ? ` | ${metaSuffix}` : ""}`;
|
|
58
133
|
|
|
59
|
-
// Write to file
|
|
60
134
|
if (this.logFile) {
|
|
61
135
|
try {
|
|
62
136
|
fs.appendFileSync(this.logFile, logLine + "\n");
|
|
@@ -65,21 +139,52 @@ class Logger {
|
|
|
65
139
|
}
|
|
66
140
|
}
|
|
67
141
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const isWarn = level === "WARN";
|
|
72
|
-
|
|
73
|
-
// If it's debug and debug mode is off, skip console
|
|
74
|
-
if (isDebug && !this.config?.debug) return;
|
|
142
|
+
if (!this._shouldWriteToConsole(level, consoleMode)) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
75
145
|
|
|
76
|
-
if (
|
|
146
|
+
if (level === "ERROR" || level === "WARN") {
|
|
77
147
|
console.error(logLine);
|
|
78
148
|
} else {
|
|
79
149
|
console.log(logLine);
|
|
80
150
|
}
|
|
81
151
|
}
|
|
82
152
|
|
|
153
|
+
_shouldWriteToConsole(level, consoleMode) {
|
|
154
|
+
if (consoleMode === false) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (level === "ERROR" || level === "WARN") {
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (level === "DEBUG") {
|
|
163
|
+
if (consoleMode === true) {
|
|
164
|
+
return Boolean(this.config?.debug);
|
|
165
|
+
}
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (consoleMode === true) {
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (consoleMode === "debug") {
|
|
174
|
+
return Boolean(this.config?.debug);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
_createSessionId() {
|
|
181
|
+
return [
|
|
182
|
+
Date.now().toString(36),
|
|
183
|
+
process.pid.toString(36),
|
|
184
|
+
Math.random().toString(36).slice(2, 8)
|
|
185
|
+
].join("-");
|
|
186
|
+
}
|
|
187
|
+
|
|
83
188
|
_cleanupOldLogs(logsDir) {
|
|
84
189
|
try {
|
|
85
190
|
const files = fs.readdirSync(logsDir);
|
package/src/review-runner.js
CHANGED
|
@@ -18,7 +18,11 @@ import { runReviewerPrompt } from "./reviewers.js";
|
|
|
18
18
|
|
|
19
19
|
async function reviewChange(config, backend, targetInfo, changeId, progress) {
|
|
20
20
|
const displayId = backend.formatChangeId(changeId);
|
|
21
|
-
logger.info(`Starting review for ${backend.changeName} ${displayId}
|
|
21
|
+
logger.info(`Starting review for ${backend.changeName} ${displayId}`, {
|
|
22
|
+
scope: "review",
|
|
23
|
+
repository: backend.kind,
|
|
24
|
+
changeId: displayId
|
|
25
|
+
});
|
|
22
26
|
progress?.update(0.05, "loading change details");
|
|
23
27
|
const details = await backend.getChangeDetails(config, targetInfo, changeId);
|
|
24
28
|
const resolvedChangeId = details.id;
|
|
@@ -68,7 +72,13 @@ async function reviewChange(config, backend, targetInfo, changeId, progress) {
|
|
|
68
72
|
|
|
69
73
|
for (const reviewerName of reviewersToTry) {
|
|
70
74
|
currentReviewerConfig = { ...config, reviewer: reviewerName };
|
|
71
|
-
logger.debug(`Trying reviewer: ${reviewerName}
|
|
75
|
+
logger.debug(`Trying reviewer: ${reviewerName}`, {
|
|
76
|
+
scope: "review",
|
|
77
|
+
repository: backend.kind,
|
|
78
|
+
changeId: details.displayId,
|
|
79
|
+
reviewer: reviewerName,
|
|
80
|
+
console: "debug"
|
|
81
|
+
});
|
|
72
82
|
progress?.update(0.45, `running reviewer ${reviewerName}`);
|
|
73
83
|
|
|
74
84
|
try {
|
|
@@ -88,13 +98,24 @@ async function reviewChange(config, backend, targetInfo, changeId, progress) {
|
|
|
88
98
|
break;
|
|
89
99
|
}
|
|
90
100
|
} catch (err) {
|
|
91
|
-
logger.error(`Reviewer prompt failed for ${reviewerName}
|
|
101
|
+
logger.error(`Reviewer prompt failed for ${reviewerName}`, err, {
|
|
102
|
+
scope: "review",
|
|
103
|
+
repository: backend.kind,
|
|
104
|
+
changeId: details.displayId,
|
|
105
|
+
reviewer: reviewerName,
|
|
106
|
+
console: false
|
|
107
|
+
});
|
|
92
108
|
// If it's the last one, it will throw below or break loop anyway
|
|
93
109
|
}
|
|
94
110
|
|
|
95
111
|
if (reviewerName !== reviewersToTry[reviewersToTry.length - 1]) {
|
|
96
112
|
const msg = `${reviewer?.displayName || reviewerName} failed for ${details.displayId}; trying next reviewer...`;
|
|
97
|
-
logger.warn(msg
|
|
113
|
+
logger.warn(msg, {
|
|
114
|
+
scope: "review",
|
|
115
|
+
repository: backend.kind,
|
|
116
|
+
changeId: details.displayId,
|
|
117
|
+
reviewer: reviewerName
|
|
118
|
+
});
|
|
98
119
|
}
|
|
99
120
|
}
|
|
100
121
|
|
|
@@ -105,7 +126,17 @@ async function reviewChange(config, backend, targetInfo, changeId, progress) {
|
|
|
105
126
|
}
|
|
106
127
|
|
|
107
128
|
progress?.update(0.82, "writing report");
|
|
108
|
-
logger.debug(
|
|
129
|
+
logger.debug("Token usage recorded", {
|
|
130
|
+
scope: "review",
|
|
131
|
+
repository: backend.kind,
|
|
132
|
+
changeId: details.displayId,
|
|
133
|
+
reviewer: reviewer.displayName,
|
|
134
|
+
inputTokens: tokenUsage.inputTokens,
|
|
135
|
+
outputTokens: tokenUsage.outputTokens,
|
|
136
|
+
totalTokens: tokenUsage.totalTokens,
|
|
137
|
+
source: tokenUsage.source,
|
|
138
|
+
console: "debug"
|
|
139
|
+
});
|
|
109
140
|
const report = buildReport(currentReviewerConfig, backend, targetInfo, details, diffPayloads, reviewer, reviewerResult, tokenUsage);
|
|
110
141
|
const outputFile = path.join(config.outputDir, backend.getReportFileName(resolvedChangeId));
|
|
111
142
|
const jsonOutputFile = outputFile.replace(/\.md$/i, ".json");
|
|
@@ -126,7 +157,13 @@ async function reviewChange(config, backend, targetInfo, changeId, progress) {
|
|
|
126
157
|
shouldWriteFormat(config, "json") ? `json: ${jsonOutputFile}` : null
|
|
127
158
|
].filter(Boolean);
|
|
128
159
|
|
|
129
|
-
logger.info(`Completed review for ${displayId}: ${outputLabels.join(" | ") || "(no report file generated)"}
|
|
160
|
+
logger.info(`Completed review for ${displayId}: ${outputLabels.join(" | ") || "(no report file generated)"}`, {
|
|
161
|
+
scope: "review",
|
|
162
|
+
repository: backend.kind,
|
|
163
|
+
changeId: displayId,
|
|
164
|
+
reviewer: reviewer.displayName,
|
|
165
|
+
console: true
|
|
166
|
+
});
|
|
130
167
|
|
|
131
168
|
return {
|
|
132
169
|
success: true,
|
|
@@ -149,9 +186,12 @@ export async function runReviewCycle(config) {
|
|
|
149
186
|
await ensureDir(config.outputDir);
|
|
150
187
|
|
|
151
188
|
const { backend, targetInfo } = await resolveRepositoryContext(config);
|
|
152
|
-
logger.debug(
|
|
153
|
-
|
|
154
|
-
|
|
189
|
+
logger.debug("Resolved repository context", {
|
|
190
|
+
scope: "session",
|
|
191
|
+
backend: backend.kind,
|
|
192
|
+
target: targetInfo.targetDisplay || config.target,
|
|
193
|
+
console: "debug"
|
|
194
|
+
});
|
|
155
195
|
|
|
156
196
|
let changeIdsToReview = [];
|
|
157
197
|
|
|
@@ -167,7 +207,12 @@ export async function runReviewCycle(config) {
|
|
|
167
207
|
}
|
|
168
208
|
|
|
169
209
|
if (changeIdsToReview.length === 0) {
|
|
170
|
-
logger.info("No changes found to review."
|
|
210
|
+
logger.info("No changes found to review.", {
|
|
211
|
+
scope: "session",
|
|
212
|
+
backend: backend.kind,
|
|
213
|
+
target: targetInfo.targetDisplay || config.target,
|
|
214
|
+
console: true
|
|
215
|
+
});
|
|
171
216
|
return;
|
|
172
217
|
}
|
|
173
218
|
|
|
@@ -176,12 +221,22 @@ export async function runReviewCycle(config) {
|
|
|
176
221
|
const batchSummary = isUncommittedBatch
|
|
177
222
|
? `Reviewing ${backend.displayName} uncommitted changes`
|
|
178
223
|
: `Reviewing ${backend.displayName} ${backend.changeName}s ${formatChangeList(backend, changeIdsToReview)}`;
|
|
179
|
-
logger.info(batchSummary
|
|
224
|
+
logger.info(batchSummary, {
|
|
225
|
+
scope: "session",
|
|
226
|
+
backend: backend.kind,
|
|
227
|
+
count: changeIdsToReview.length,
|
|
228
|
+
console: true
|
|
229
|
+
});
|
|
180
230
|
const progress = createProgressReporter(`${backend.displayName} ${backend.changeName} batch`);
|
|
181
231
|
progress.update(0, `0/${changeIdsToReview.length} completed`);
|
|
182
232
|
|
|
183
233
|
for (const [index, changeId] of changeIdsToReview.entries()) {
|
|
184
|
-
logger.debug(`Starting review for ${backend.formatChangeId(changeId)}
|
|
234
|
+
logger.debug(`Starting review for ${backend.formatChangeId(changeId)}`, {
|
|
235
|
+
scope: "review",
|
|
236
|
+
repository: backend.kind,
|
|
237
|
+
changeId: backend.formatChangeId(changeId),
|
|
238
|
+
console: "debug"
|
|
239
|
+
});
|
|
185
240
|
const displayId = backend.formatChangeId(changeId);
|
|
186
241
|
updateOverallProgress(progress, index, changeIdsToReview.length, 0, `starting ${displayId}`);
|
|
187
242
|
|
package/src/shell.js
CHANGED
|
@@ -24,13 +24,18 @@ export async function runCommand(command, args = [], options = {}) {
|
|
|
24
24
|
debug = false
|
|
25
25
|
} = options;
|
|
26
26
|
|
|
27
|
-
logger.debug(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
logger.debug(`run: ${command}`, {
|
|
28
|
+
scope: "command",
|
|
29
|
+
command,
|
|
30
|
+
args,
|
|
31
|
+
cwd: cwd || "",
|
|
32
|
+
timeoutMs: timeoutMs || 0,
|
|
33
|
+
input: input ? summarizeOutput(input) : "",
|
|
34
|
+
console: debug ? "debug" : false
|
|
35
|
+
});
|
|
32
36
|
|
|
33
37
|
return await new Promise((resolve, reject) => {
|
|
38
|
+
const startedAt = Date.now();
|
|
34
39
|
const child = spawn(command, args, {
|
|
35
40
|
cwd,
|
|
36
41
|
env: {
|
|
@@ -54,7 +59,12 @@ export async function runCommand(command, args = [], options = {}) {
|
|
|
54
59
|
});
|
|
55
60
|
|
|
56
61
|
child.on("error", (err) => {
|
|
57
|
-
logger.error(`spawn error: ${command}`, err
|
|
62
|
+
logger.error(`spawn error: ${command}`, err, {
|
|
63
|
+
scope: "command",
|
|
64
|
+
command,
|
|
65
|
+
args,
|
|
66
|
+
cwd: cwd || ""
|
|
67
|
+
});
|
|
58
68
|
reject(err);
|
|
59
69
|
});
|
|
60
70
|
|
|
@@ -71,14 +81,26 @@ export async function runCommand(command, args = [], options = {}) {
|
|
|
71
81
|
stdout: trim ? stdout.trim() : stdout,
|
|
72
82
|
stderr: trim ? stderr.trim() : stderr
|
|
73
83
|
};
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const
|
|
84
|
+
const durationMs = Date.now() - startedAt;
|
|
85
|
+
|
|
86
|
+
const exitMeta = {
|
|
87
|
+
scope: "command",
|
|
88
|
+
command,
|
|
89
|
+
args,
|
|
90
|
+
cwd: cwd || "",
|
|
91
|
+
code: result.code,
|
|
92
|
+
timedOut: result.timedOut,
|
|
93
|
+
allowFailure,
|
|
94
|
+
durationMs,
|
|
95
|
+
stdout: summarizeOutput(result.stdout),
|
|
96
|
+
stderr: summarizeOutput(result.stderr),
|
|
97
|
+
console: debug ? "debug" : false
|
|
98
|
+
};
|
|
77
99
|
|
|
78
|
-
if (
|
|
79
|
-
logger.error(
|
|
100
|
+
if ((result.code !== 0 || result.timedOut) && !allowFailure) {
|
|
101
|
+
logger.error(`exit: ${command}`, null, exitMeta);
|
|
80
102
|
} else {
|
|
81
|
-
logger.debug(
|
|
103
|
+
logger.debug(`exit: ${command}`, exitMeta);
|
|
82
104
|
}
|
|
83
105
|
|
|
84
106
|
if ((result.code !== 0 || result.timedOut) && !allowFailure) {
|
package/src/utils.js
CHANGED
|
@@ -50,9 +50,10 @@ export function formatDate(dateInput) {
|
|
|
50
50
|
const hours = pad(d.getHours());
|
|
51
51
|
const minutes = pad(d.getMinutes());
|
|
52
52
|
const seconds = pad(d.getSeconds());
|
|
53
|
+
const milliseconds = String(d.getMilliseconds()).padStart(3, "0");
|
|
53
54
|
const offset = `${sign}${pad(offsetHours)}:${pad(offsetMins)}`;
|
|
54
55
|
|
|
55
|
-
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds} ${offset}`;
|
|
56
|
+
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds} ${offset}`;
|
|
56
57
|
}
|
|
57
58
|
export function getTimestampPrefix() {
|
|
58
59
|
const now = new Date();
|