kodevu 0.1.60 → 0.1.62
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/package.json +1 -1
- package/src/config.js +24 -17
- package/src/index.js +5 -2
- package/src/review-runner.js +8 -1
- package/src/reviewers.js +65 -16
- package/src/shell.js +9 -0
package/package.json
CHANGED
package/src/config.js
CHANGED
|
@@ -31,6 +31,13 @@ const defaultConfig = {
|
|
|
31
31
|
openaiProject: ""
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
+
function mkCliError(message) {
|
|
35
|
+
const e = new Error(message);
|
|
36
|
+
// exit code 2 = usage / configuration errors
|
|
37
|
+
e.exitCode = 2;
|
|
38
|
+
return e;
|
|
39
|
+
}
|
|
40
|
+
|
|
34
41
|
const ENV_MAP = {
|
|
35
42
|
KODEVU_REVIEWER: "reviewer",
|
|
36
43
|
KODEVU_LANG: "lang",
|
|
@@ -65,16 +72,16 @@ async function loadConfigFile(configPath = defaultConfigFilePath) {
|
|
|
65
72
|
content = await fs.readFile(resolvedPath, "utf8");
|
|
66
73
|
} catch (err) {
|
|
67
74
|
if (err.code === "ENOENT") return {};
|
|
68
|
-
throw
|
|
75
|
+
throw mkCliError(`Failed to read config file ${resolvedPath}: ${err.message}`);
|
|
69
76
|
}
|
|
70
77
|
let parsed;
|
|
71
78
|
try {
|
|
72
79
|
parsed = JSON.parse(content);
|
|
73
80
|
} catch (err) {
|
|
74
|
-
throw
|
|
81
|
+
throw mkCliError(`Invalid JSON in config file ${resolvedPath}: ${err.message}`);
|
|
75
82
|
}
|
|
76
83
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
77
|
-
throw
|
|
84
|
+
throw mkCliError(`Config file ${resolvedPath} must contain a JSON object`);
|
|
78
85
|
}
|
|
79
86
|
const result = {};
|
|
80
87
|
for (const [key, value] of Object.entries(parsed)) {
|
|
@@ -93,7 +100,7 @@ function normalizeOutputFormats(outputFormats) {
|
|
|
93
100
|
const invalid = normalized.filter((item) => !supported.includes(item));
|
|
94
101
|
|
|
95
102
|
if (invalid.length > 0) {
|
|
96
|
-
throw
|
|
103
|
+
throw mkCliError(`Unsupported output format(s): ${invalid.join(", ")}. Use: ${supported.join(", ")}`);
|
|
97
104
|
}
|
|
98
105
|
return normalized.length === 0 ? ["markdown"] : normalized;
|
|
99
106
|
}
|
|
@@ -187,28 +194,28 @@ export function parseCliArgs(argv) {
|
|
|
187
194
|
const hasNextValue = nextValue && !nextValue.startsWith("-");
|
|
188
195
|
|
|
189
196
|
if (value === "--reviewer" || value === "-r") {
|
|
190
|
-
if (!hasNextValue) throw
|
|
197
|
+
if (!hasNextValue) throw mkCliError(`Missing value for ${value}`);
|
|
191
198
|
args.reviewer = nextValue;
|
|
192
199
|
index += 1;
|
|
193
200
|
continue;
|
|
194
201
|
}
|
|
195
202
|
|
|
196
203
|
if (value === "--prompt" || value === "-p") {
|
|
197
|
-
if (!hasNextValue) throw
|
|
204
|
+
if (!hasNextValue) throw mkCliError(`Missing value for ${value}`);
|
|
198
205
|
args.prompt = nextValue;
|
|
199
206
|
index += 1;
|
|
200
207
|
continue;
|
|
201
208
|
}
|
|
202
209
|
|
|
203
210
|
if (value === "--lang" || value === "-l") {
|
|
204
|
-
if (!hasNextValue) throw
|
|
211
|
+
if (!hasNextValue) throw mkCliError(`Missing value for ${value}`);
|
|
205
212
|
args.lang = nextValue;
|
|
206
213
|
index += 1;
|
|
207
214
|
continue;
|
|
208
215
|
}
|
|
209
216
|
|
|
210
217
|
if (value === "--rev" || value === "-v") {
|
|
211
|
-
if (!hasNextValue) throw
|
|
218
|
+
if (!hasNextValue) throw mkCliError(`Missing value for ${value}`);
|
|
212
219
|
args.rev = nextValue;
|
|
213
220
|
index += 1;
|
|
214
221
|
continue;
|
|
@@ -216,7 +223,7 @@ export function parseCliArgs(argv) {
|
|
|
216
223
|
|
|
217
224
|
if (value === "--last" || value === "-n") {
|
|
218
225
|
const hasLastValue = nextValue !== undefined && /^-?\d+$/.test(nextValue);
|
|
219
|
-
if (!hasLastValue) throw
|
|
226
|
+
if (!hasLastValue) throw mkCliError(`Missing value for ${value}`);
|
|
220
227
|
args.last = nextValue;
|
|
221
228
|
index += 1;
|
|
222
229
|
continue;
|
|
@@ -228,49 +235,49 @@ export function parseCliArgs(argv) {
|
|
|
228
235
|
}
|
|
229
236
|
|
|
230
237
|
if (value === "--output" || value === "-o") {
|
|
231
|
-
if (!hasNextValue) throw
|
|
238
|
+
if (!hasNextValue) throw mkCliError(`Missing value for ${value}`);
|
|
232
239
|
args.outputDir = nextValue;
|
|
233
240
|
index += 1;
|
|
234
241
|
continue;
|
|
235
242
|
}
|
|
236
243
|
|
|
237
244
|
if (value === "--format" || value === "-f") {
|
|
238
|
-
if (!hasNextValue) throw
|
|
245
|
+
if (!hasNextValue) throw mkCliError(`Missing value for ${value}`);
|
|
239
246
|
args.outputFormats = nextValue;
|
|
240
247
|
index += 1;
|
|
241
248
|
continue;
|
|
242
249
|
}
|
|
243
250
|
|
|
244
251
|
if (value === "--openai-api-key") {
|
|
245
|
-
if (!hasNextValue) throw
|
|
252
|
+
if (!hasNextValue) throw mkCliError(`Missing value for ${value}`);
|
|
246
253
|
args.openaiApiKey = nextValue;
|
|
247
254
|
index += 1;
|
|
248
255
|
continue;
|
|
249
256
|
}
|
|
250
257
|
|
|
251
258
|
if (value === "--openai-base-url") {
|
|
252
|
-
if (!hasNextValue) throw
|
|
259
|
+
if (!hasNextValue) throw mkCliError(`Missing value for ${value}`);
|
|
253
260
|
args.openaiBaseUrl = nextValue;
|
|
254
261
|
index += 1;
|
|
255
262
|
continue;
|
|
256
263
|
}
|
|
257
264
|
|
|
258
265
|
if (value === "--openai-model") {
|
|
259
|
-
if (!hasNextValue) throw
|
|
266
|
+
if (!hasNextValue) throw mkCliError(`Missing value for ${value}`);
|
|
260
267
|
args.openaiModel = nextValue;
|
|
261
268
|
index += 1;
|
|
262
269
|
continue;
|
|
263
270
|
}
|
|
264
271
|
|
|
265
272
|
if (value === "--openai-org") {
|
|
266
|
-
if (!hasNextValue) throw
|
|
273
|
+
if (!hasNextValue) throw mkCliError(`Missing value for ${value}`);
|
|
267
274
|
args.openaiOrganization = nextValue;
|
|
268
275
|
index += 1;
|
|
269
276
|
continue;
|
|
270
277
|
}
|
|
271
278
|
|
|
272
279
|
if (value === "--openai-project") {
|
|
273
|
-
if (!hasNextValue) throw
|
|
280
|
+
if (!hasNextValue) throw mkCliError(`Missing value for ${value}`);
|
|
274
281
|
args.openaiProject = nextValue;
|
|
275
282
|
index += 1;
|
|
276
283
|
continue;
|
|
@@ -281,7 +288,7 @@ export function parseCliArgs(argv) {
|
|
|
281
288
|
continue;
|
|
282
289
|
}
|
|
283
290
|
|
|
284
|
-
throw
|
|
291
|
+
throw mkCliError(`Unexpected argument: ${value}`);
|
|
285
292
|
}
|
|
286
293
|
|
|
287
294
|
return args;
|
package/src/index.js
CHANGED
|
@@ -11,7 +11,9 @@ try {
|
|
|
11
11
|
} catch (error) {
|
|
12
12
|
console.error(error?.message || String(error));
|
|
13
13
|
printHelp();
|
|
14
|
-
|
|
14
|
+
// CLI/usage errors -> exit code 2
|
|
15
|
+
const code = Number(error?.exitCode) || 2;
|
|
16
|
+
process.exit(code);
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
if (cliArgs.help) {
|
|
@@ -81,6 +83,7 @@ try {
|
|
|
81
83
|
scope: "session",
|
|
82
84
|
console: true
|
|
83
85
|
});
|
|
84
|
-
process
|
|
86
|
+
// Map any attached exitCode (from thrown errors) to process exitCode, fallback to 1
|
|
87
|
+
process.exitCode = Number(error?.exitCode) || 1;
|
|
85
88
|
}
|
|
86
89
|
process.exit(process.exitCode || 0);
|
package/src/review-runner.js
CHANGED
|
@@ -137,7 +137,14 @@ async function reviewChange(config, backend, targetInfo, changeId, progress) {
|
|
|
137
137
|
reasonParts.push(`\n${detail}`);
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
|
|
140
|
+
const err = new Error(reasonParts.join(" "));
|
|
141
|
+
// Prefer reviewer-provided exit code when available (HTTP status or child exit code), otherwise 3
|
|
142
|
+
try {
|
|
143
|
+
err.exitCode = reviewerResult?.code || 3;
|
|
144
|
+
} catch {
|
|
145
|
+
err.exitCode = 3;
|
|
146
|
+
}
|
|
147
|
+
throw err;
|
|
141
148
|
}
|
|
142
149
|
|
|
143
150
|
progress?.update(0.82, "writing report");
|
package/src/reviewers.js
CHANGED
|
@@ -172,6 +172,8 @@ export const REVIEWERS = {
|
|
|
172
172
|
async run(config, workingDir, promptText, diffText) {
|
|
173
173
|
const requestBody = {
|
|
174
174
|
model: config.openaiModel,
|
|
175
|
+
stream: true,
|
|
176
|
+
stream_options: { include_usage: true },
|
|
175
177
|
messages: [
|
|
176
178
|
{
|
|
177
179
|
role: "user",
|
|
@@ -188,17 +190,16 @@ export const REVIEWERS = {
|
|
|
188
190
|
signal: AbortSignal.timeout(config.commandTimeoutMs)
|
|
189
191
|
});
|
|
190
192
|
|
|
191
|
-
const responseText = await response.text();
|
|
192
|
-
let payload;
|
|
193
|
-
|
|
194
|
-
try {
|
|
195
|
-
payload = responseText ? JSON.parse(responseText) : {};
|
|
196
|
-
} catch {
|
|
197
|
-
payload = null;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
193
|
if (!response.ok) {
|
|
201
|
-
|
|
194
|
+
let errorMessage = `HTTP ${response.status}`;
|
|
195
|
+
let responseText = "";
|
|
196
|
+
try {
|
|
197
|
+
responseText = await response.text();
|
|
198
|
+
const payload = JSON.parse(responseText);
|
|
199
|
+
errorMessage = payload?.error?.message || responseText;
|
|
200
|
+
} catch {
|
|
201
|
+
if (responseText) errorMessage = responseText;
|
|
202
|
+
}
|
|
202
203
|
return {
|
|
203
204
|
code: response.status,
|
|
204
205
|
timedOut: false,
|
|
@@ -208,19 +209,67 @@ export const REVIEWERS = {
|
|
|
208
209
|
};
|
|
209
210
|
}
|
|
210
211
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
212
|
+
let message = "";
|
|
213
|
+
let usage = null;
|
|
214
|
+
let stdoutText = "";
|
|
215
|
+
const contentType = response.headers.get("content-type") || "";
|
|
216
|
+
|
|
217
|
+
if (contentType.includes("text/event-stream")) {
|
|
218
|
+
const decoder = new TextDecoder("utf8");
|
|
219
|
+
let buffer = "";
|
|
220
|
+
|
|
221
|
+
for await (const chunk of response.body) {
|
|
222
|
+
const textChunk = decoder.decode(chunk, { stream: true });
|
|
223
|
+
stdoutText += textChunk;
|
|
224
|
+
buffer += textChunk;
|
|
225
|
+
const parts = buffer.split("\n");
|
|
226
|
+
buffer = parts.pop() || "";
|
|
227
|
+
for (const line of parts) {
|
|
228
|
+
const trimmed = line.trim();
|
|
229
|
+
if (trimmed.startsWith("data: ")) {
|
|
230
|
+
if (trimmed === "data: [DONE]") continue;
|
|
231
|
+
try {
|
|
232
|
+
const data = JSON.parse(trimmed.slice(6));
|
|
233
|
+
if (data.choices?.[0]?.delta?.content) {
|
|
234
|
+
message += data.choices[0].delta.content;
|
|
235
|
+
}
|
|
236
|
+
if (data.usage) {
|
|
237
|
+
usage = {
|
|
238
|
+
inputTokens: Number(data.usage.prompt_tokens || 0),
|
|
239
|
+
outputTokens: Number(data.usage.completion_tokens || 0),
|
|
240
|
+
totalTokens: Number(data.usage.total_tokens || 0)
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
} catch (e) {
|
|
244
|
+
// Ignore JSON parse errors for individual chunks
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
} else {
|
|
250
|
+
stdoutText = await response.text();
|
|
251
|
+
let payload;
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
payload = stdoutText ? JSON.parse(stdoutText) : {};
|
|
255
|
+
} catch {
|
|
256
|
+
payload = null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
message = extractOpenAiMessageContent(payload?.choices?.[0]?.message?.content);
|
|
260
|
+
if (payload?.usage) {
|
|
261
|
+
usage = {
|
|
214
262
|
inputTokens: Number(payload.usage.prompt_tokens || 0),
|
|
215
263
|
outputTokens: Number(payload.usage.completion_tokens || 0),
|
|
216
264
|
totalTokens: Number(payload.usage.total_tokens || 0)
|
|
217
|
-
}
|
|
218
|
-
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
}
|
|
219
268
|
|
|
220
269
|
return {
|
|
221
270
|
code: 0,
|
|
222
271
|
timedOut: false,
|
|
223
|
-
stdout:
|
|
272
|
+
stdout: stdoutText,
|
|
224
273
|
stderr: "",
|
|
225
274
|
message,
|
|
226
275
|
usage
|
package/src/shell.js
CHANGED
|
@@ -65,6 +65,13 @@ export async function runCommand(command, args = [], options = {}) {
|
|
|
65
65
|
args,
|
|
66
66
|
cwd: cwd || ""
|
|
67
67
|
});
|
|
68
|
+
// Mark spawn/runtime errors as runtime failures (exit code 3)
|
|
69
|
+
try {
|
|
70
|
+
if (!err || typeof err !== "object") err = new Error(String(err));
|
|
71
|
+
if (err.exitCode == null) err.exitCode = 3;
|
|
72
|
+
} catch (e) {
|
|
73
|
+
// ignore
|
|
74
|
+
}
|
|
68
75
|
reject(err);
|
|
69
76
|
});
|
|
70
77
|
|
|
@@ -108,6 +115,8 @@ export async function runCommand(command, args = [], options = {}) {
|
|
|
108
115
|
`Command failed: ${command} ${args.join(" ")}\n${result.stderr || result.stdout}`.trim()
|
|
109
116
|
);
|
|
110
117
|
error.result = result;
|
|
118
|
+
// Map command failures to exit code 3 (runtime / external command failure)
|
|
119
|
+
error.exitCode = 3;
|
|
111
120
|
reject(error);
|
|
112
121
|
return;
|
|
113
122
|
}
|