openairev 0.2.4 → 0.3.1
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/bin/openairev.js +2 -1
- package/package.json +1 -1
- package/src/agents/claude-code.js +2 -2
- package/src/agents/codex.js +13 -6
- package/src/agents/exec-helper.js +45 -7
- package/src/agents/stream-summarizer.js +139 -0
- package/src/cli/review.js +2 -2
- package/src/mcp/mcp-server.js +13 -3
- package/src/orchestrator/orchestrator.js +1 -1
- package/src/review/review-runner.js +21 -0
- package/src/review/review-runner.test.js +42 -0
- package/src/tools/git-tools.js +1 -1
- package/src/version.js +8 -0
package/bin/openairev.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { Command } from 'commander';
|
|
4
|
+
import { VERSION } from '../src/version.js';
|
|
4
5
|
import { initCommand } from '../src/cli/init.js';
|
|
5
6
|
import { reviewCommand } from '../src/cli/review.js';
|
|
6
7
|
import { resumeCommand } from '../src/cli/resume.js';
|
|
@@ -12,7 +13,7 @@ const program = new Command();
|
|
|
12
13
|
program
|
|
13
14
|
.name('openairev')
|
|
14
15
|
.description('OpenAIRev — cross-model AI code reviewer')
|
|
15
|
-
.version(
|
|
16
|
+
.version(VERSION);
|
|
16
17
|
|
|
17
18
|
program
|
|
18
19
|
.command('init')
|
package/package.json
CHANGED
|
@@ -38,9 +38,9 @@ export class ClaudeCodeAdapter {
|
|
|
38
38
|
if (!this.sessionName && parsed.session_id) {
|
|
39
39
|
this.sessionName = parsed.session_id;
|
|
40
40
|
}
|
|
41
|
-
return parsed;
|
|
41
|
+
return { ...parsed, raw_output: result.stdout };
|
|
42
42
|
} catch {
|
|
43
|
-
return { raw: result.stdout, error: 'Failed to parse JSON output' };
|
|
43
|
+
return { raw: result.stdout, raw_output: result.stdout, error: 'Failed to parse JSON output' };
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
}
|
package/src/agents/codex.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { join, dirname } from 'path';
|
|
2
2
|
import { fileURLToPath } from 'url';
|
|
3
3
|
import { exec } from './exec-helper.js';
|
|
4
|
+
import { createCodexSummarizer } from './stream-summarizer.js';
|
|
4
5
|
|
|
5
6
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
7
|
|
|
@@ -15,7 +16,7 @@ export class CodexAdapter {
|
|
|
15
16
|
this.sessionId = id;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
async run(prompt, { useSchema = false, schemaFile = 'verdict-schema.json', continueSession = false, sessionName = null } = {}) {
|
|
19
|
+
async run(prompt, { useSchema = false, schemaFile = 'verdict-schema.json', continueSession = false, sessionName = null, stream = false } = {}) {
|
|
19
20
|
const args = ['exec'];
|
|
20
21
|
|
|
21
22
|
if (continueSession && this.sessionId) {
|
|
@@ -30,7 +31,11 @@ export class CodexAdapter {
|
|
|
30
31
|
args.push('--output-schema', schemaPath);
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
const
|
|
34
|
+
const summarizer = stream ? createCodexSummarizer({
|
|
35
|
+
reviewerName: stream.reviewerName || 'codex',
|
|
36
|
+
tty: stream.tty !== false,
|
|
37
|
+
}) : undefined;
|
|
38
|
+
const result = await exec(this.cmd, args, { onData: summarizer });
|
|
34
39
|
|
|
35
40
|
try {
|
|
36
41
|
const lines = result.stdout.trim().split('\n');
|
|
@@ -60,17 +65,19 @@ export class CodexAdapter {
|
|
|
60
65
|
this.sessionId = sessionId;
|
|
61
66
|
}
|
|
62
67
|
|
|
68
|
+
const progress = summarizer?.getProgress() || [];
|
|
69
|
+
|
|
63
70
|
if (agentMessage) {
|
|
64
71
|
try {
|
|
65
|
-
return { result: JSON.parse(agentMessage), session_id: this.sessionId };
|
|
72
|
+
return { result: JSON.parse(agentMessage), raw_output: result.stdout, progress, session_id: this.sessionId };
|
|
66
73
|
} catch {
|
|
67
|
-
return { result: agentMessage, session_id: this.sessionId };
|
|
74
|
+
return { result: agentMessage, raw_output: result.stdout, progress, session_id: this.sessionId };
|
|
68
75
|
}
|
|
69
76
|
}
|
|
70
77
|
|
|
71
|
-
return { raw: result.stdout, session_id: this.sessionId };
|
|
78
|
+
return { raw: result.stdout, raw_output: result.stdout, progress, session_id: this.sessionId };
|
|
72
79
|
} catch {
|
|
73
|
-
return { raw: result.stdout, error: 'Failed to parse output' };
|
|
80
|
+
return { raw: result.stdout, raw_output: result.stdout, error: 'Failed to parse output' };
|
|
74
81
|
}
|
|
75
82
|
}
|
|
76
83
|
}
|
|
@@ -1,13 +1,51 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
const MAX_BUFFER = 10 * 1024 * 1024;
|
|
4
|
+
|
|
5
|
+
export function exec(cmd, args, { onData } = {}) {
|
|
4
6
|
return new Promise((resolve, reject) => {
|
|
5
|
-
|
|
6
|
-
maxBuffer: 10 * 1024 * 1024,
|
|
7
|
+
const child = spawn(cmd, args, {
|
|
7
8
|
timeout: 300_000,
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const stdoutChunks = [];
|
|
12
|
+
const stderrChunks = [];
|
|
13
|
+
let stdoutLen = 0;
|
|
14
|
+
let stderrLen = 0;
|
|
15
|
+
let killed = false;
|
|
16
|
+
|
|
17
|
+
child.stdout.on('data', (chunk) => {
|
|
18
|
+
if (onData) onData(chunk.toString());
|
|
19
|
+
stdoutLen += chunk.length;
|
|
20
|
+
if (stdoutLen <= MAX_BUFFER) {
|
|
21
|
+
stdoutChunks.push(chunk);
|
|
22
|
+
} else if (!killed) {
|
|
23
|
+
killed = true;
|
|
24
|
+
child.kill();
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
child.stderr.on('data', (chunk) => {
|
|
29
|
+
stderrLen += chunk.length;
|
|
30
|
+
if (stderrLen <= MAX_BUFFER) {
|
|
31
|
+
stderrChunks.push(chunk);
|
|
32
|
+
} else if (!killed) {
|
|
33
|
+
killed = true;
|
|
34
|
+
child.kill();
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
child.on('error', (err) => {
|
|
39
|
+
reject(new Error(`${cmd} failed: ${err.message}`));
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
child.on('close', (code) => {
|
|
43
|
+
const stdout = Buffer.concat(stdoutChunks).toString();
|
|
44
|
+
const stderr = Buffer.concat(stderrChunks).toString();
|
|
45
|
+
if (killed) {
|
|
46
|
+
reject(new Error(`${cmd} output exceeded ${MAX_BUFFER} bytes`));
|
|
47
|
+
} else if (code !== 0 && !stdout) {
|
|
48
|
+
reject(new Error(`${cmd} failed (exit ${code}): ${stderr}`));
|
|
11
49
|
} else {
|
|
12
50
|
resolve({ stdout, stderr });
|
|
13
51
|
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates a summarizer callback for Codex NDJSON event streams.
|
|
5
|
+
* tty=true: prints colored progress to stderr.
|
|
6
|
+
* tty=false: collects plain-text progress lines silently.
|
|
7
|
+
* Both modes always collect lines for the final summary.
|
|
8
|
+
*/
|
|
9
|
+
export function createCodexSummarizer({ reviewerName, tty = true } = {}) {
|
|
10
|
+
const seenFiles = new Set();
|
|
11
|
+
const progressLines = [];
|
|
12
|
+
let buffer = '';
|
|
13
|
+
let started = false;
|
|
14
|
+
|
|
15
|
+
const summarizer = (chunk) => {
|
|
16
|
+
if (!started) {
|
|
17
|
+
started = true;
|
|
18
|
+
if (reviewerName) emit(`reviewer: ${reviewerName}`, 'cyan');
|
|
19
|
+
}
|
|
20
|
+
buffer += chunk;
|
|
21
|
+
const lines = buffer.split('\n');
|
|
22
|
+
buffer = lines.pop();
|
|
23
|
+
|
|
24
|
+
for (const line of lines) {
|
|
25
|
+
if (!line.trim()) continue;
|
|
26
|
+
try {
|
|
27
|
+
const event = JSON.parse(line);
|
|
28
|
+
summarizeCodexEvent(event, seenFiles, { emit });
|
|
29
|
+
} catch {
|
|
30
|
+
// skip non-JSON
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
summarizer.getProgress = () => progressLines;
|
|
36
|
+
|
|
37
|
+
function emit(msg, color) {
|
|
38
|
+
if (progressLines.length < 200) progressLines.push(msg);
|
|
39
|
+
if (tty) {
|
|
40
|
+
const colorFn = color === 'cyan' ? chalk.cyan
|
|
41
|
+
: color === 'green' ? chalk.green
|
|
42
|
+
: color === 'yellow' ? chalk.yellow
|
|
43
|
+
: color === 'red' ? chalk.red
|
|
44
|
+
: chalk.dim;
|
|
45
|
+
process.stderr.write(` ${colorFn(msg)}\n`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return summarizer;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function summarizeCodexEvent(event, seenFiles, { emit }) {
|
|
53
|
+
const type = event.type;
|
|
54
|
+
|
|
55
|
+
if (type === 'thread.started') {
|
|
56
|
+
emit(`session: ${event.thread_id}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (type === 'item.started' && event.item?.type === 'todo_list') {
|
|
60
|
+
const items = event.item.items?.map(i => i.text) || [];
|
|
61
|
+
emit('plan:', 'cyan');
|
|
62
|
+
for (const item of items) emit(` • ${item}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (type === 'item.completed' && event.item?.type === 'todo_list') {
|
|
66
|
+
const items = event.item.items || [];
|
|
67
|
+
const done = items.filter(i => i.completed).length;
|
|
68
|
+
emit(`progress: ${done}/${items.length} tasks done`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (type === 'item.started' && event.item?.type === 'command_execution') {
|
|
72
|
+
const cmd = event.item.command || '';
|
|
73
|
+
if (isInternalCmd(cmd)) return;
|
|
74
|
+
const file = extractFileFromCmd(cmd);
|
|
75
|
+
if (file && !seenFiles.has(file)) {
|
|
76
|
+
seenFiles.add(file);
|
|
77
|
+
emit(`reading: ${file}`);
|
|
78
|
+
} else if (!file) {
|
|
79
|
+
const short = summarizeCmd(cmd);
|
|
80
|
+
if (short) emit(`running: ${short}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (type === 'item.completed' && event.item?.type === 'command_execution') {
|
|
85
|
+
const exit = event.item.exit_code;
|
|
86
|
+
if (exit !== null && exit !== 0) {
|
|
87
|
+
const cmd = summarizeCmd(event.item.command || '');
|
|
88
|
+
emit(`command failed (exit ${exit}): ${cmd}`, 'yellow');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (type === 'item.started' && event.item?.type === 'agent_message') {
|
|
93
|
+
emit('generating verdict...', 'cyan');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (type === 'item.completed' && event.item?.type === 'agent_message') {
|
|
97
|
+
emit('verdict ready', 'green');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (type === 'turn.completed' && event.usage) {
|
|
101
|
+
const { input_tokens, output_tokens } = event.usage;
|
|
102
|
+
const total = input_tokens + output_tokens;
|
|
103
|
+
emit(`tokens: ${fmt(total)} total (${fmt(input_tokens)} in / ${fmt(output_tokens)} out)`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (type === 'error' || type === 'turn.failed') {
|
|
107
|
+
const msg = event.message || event.error?.message || 'unknown error';
|
|
108
|
+
emit(`error: ${msg}`, 'red');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function isInternalCmd(cmd) {
|
|
113
|
+
return /node\s+-e\s/.test(cmd) ||
|
|
114
|
+
/require\(['"]child_process['"]\)/.test(cmd) ||
|
|
115
|
+
/spawn\(/.test(cmd) ||
|
|
116
|
+
/execFile\(/.test(cmd) ||
|
|
117
|
+
/process\.exec/.test(cmd) ||
|
|
118
|
+
/<<'NODE'/.test(cmd) ||
|
|
119
|
+
/echo\s/.test(cmd);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function extractFileFromCmd(cmd) {
|
|
123
|
+
const match = cmd.match(/(?:cat|sed|nl|head|tail|less)\s+(?:-[^\s]*\s+)*([^\s|>"']+\.\w+)/);
|
|
124
|
+
if (match) return match[1];
|
|
125
|
+
const match2 = cmd.match(/\s([a-zA-Z][\w/.-]+\.\w{1,5})(?:\s|$|\|)/);
|
|
126
|
+
return match2 ? match2[1] : null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function summarizeCmd(cmd) {
|
|
130
|
+
const inner = cmd.replace(/^\/bin\/\w+\s+-\w+\s+["'](.+)["']$/, '$1');
|
|
131
|
+
const clean = inner.replace(/\\"/g, '"').trim();
|
|
132
|
+
return clean.length > 80 ? clean.slice(0, 77) + '...' : clean;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function fmt(n) {
|
|
136
|
+
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M';
|
|
137
|
+
if (n >= 1_000) return (n / 1_000).toFixed(1) + 'k';
|
|
138
|
+
return String(n);
|
|
139
|
+
}
|
package/src/cli/review.js
CHANGED
|
@@ -25,8 +25,8 @@ export async function reviewCommand(options) {
|
|
|
25
25
|
const skipPlan = options.once || options.quick || !options.plan;
|
|
26
26
|
|
|
27
27
|
console.log(chalk.bold(`\nOpenAIRev\n`));
|
|
28
|
-
console.log(` Executor: ${chalk.cyan(executor)}`);
|
|
29
28
|
console.log(` Reviewer: ${chalk.cyan(reviewerName)}`);
|
|
29
|
+
console.log(` Executor: ${chalk.dim(executor)}`);
|
|
30
30
|
console.log(` Max rounds: ${chalk.cyan(maxRounds)}`);
|
|
31
31
|
|
|
32
32
|
if (options.once) {
|
|
@@ -62,7 +62,7 @@ export async function reviewCommand(options) {
|
|
|
62
62
|
}
|
|
63
63
|
try {
|
|
64
64
|
console.log(chalk.dim('Starting review...\n'));
|
|
65
|
-
const review = await runReview(diff, { config, reviewerName, cwd });
|
|
65
|
+
const review = await runReview(diff, { config, reviewerName, cwd, stream: true });
|
|
66
66
|
|
|
67
67
|
const session = createSession({ executor, reviewer: reviewerName, diff_ref: options.diff || 'auto' });
|
|
68
68
|
session.iterations.push({ round: 1, review, timestamp: new Date().toISOString() });
|
package/src/mcp/mcp-server.js
CHANGED
|
@@ -8,13 +8,14 @@ import { getDiff } from '../tools/git-tools.js';
|
|
|
8
8
|
import { runToolGates } from '../tools/tool-runner.js';
|
|
9
9
|
import { runReview } from '../review/review-runner.js';
|
|
10
10
|
import { createSession, saveSession } from '../session/session-manager.js';
|
|
11
|
+
import { VERSION } from '../version.js';
|
|
11
12
|
|
|
12
13
|
const cwd = process.cwd();
|
|
13
14
|
const config = loadConfig(cwd);
|
|
14
15
|
|
|
15
16
|
const server = new McpServer({
|
|
16
17
|
name: 'openairev',
|
|
17
|
-
version:
|
|
18
|
+
version: VERSION,
|
|
18
19
|
});
|
|
19
20
|
|
|
20
21
|
server.tool(
|
|
@@ -52,6 +53,7 @@ server.tool(
|
|
|
52
53
|
reviewerName,
|
|
53
54
|
taskDescription: task_description,
|
|
54
55
|
cwd,
|
|
56
|
+
stream: 'silent',
|
|
55
57
|
});
|
|
56
58
|
|
|
57
59
|
const session = createSession({ executor: execAgent, reviewer: reviewerName });
|
|
@@ -60,8 +62,16 @@ server.tool(
|
|
|
60
62
|
session.status = 'completed';
|
|
61
63
|
saveSession(session, cwd);
|
|
62
64
|
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
+
const parts = [];
|
|
66
|
+
|
|
67
|
+
if (review.progress?.length > 0) {
|
|
68
|
+
parts.push({ type: 'text', text: `Review progress:\n${review.progress.map(l => ` ${l}`).join('\n')}` });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const feedback = review.executor_feedback || JSON.stringify(review.verdict || review, null, 2);
|
|
72
|
+
parts.push({ type: 'text', text: feedback });
|
|
73
|
+
|
|
74
|
+
return { content: parts };
|
|
65
75
|
}
|
|
66
76
|
);
|
|
67
77
|
|
|
@@ -263,7 +263,7 @@ async function runReviewRound(reviewerName, config, content, { kind, chain, spec
|
|
|
263
263
|
config, reviewerName, promptFile,
|
|
264
264
|
taskDescription: chain.task?.user_request,
|
|
265
265
|
specRef: specRef || chain.task?.spec_ref,
|
|
266
|
-
cwd, sessionId,
|
|
266
|
+
cwd, sessionId, stream: true,
|
|
267
267
|
});
|
|
268
268
|
}
|
|
269
269
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
1
3
|
import { createAdapter } from '../agents/registry.js';
|
|
2
4
|
import { stageInput, buildInputReference } from './input-stager.js';
|
|
3
5
|
import { loadPromptFile } from './prompt-loader.js';
|
|
@@ -10,6 +12,7 @@ export async function runReview(content, {
|
|
|
10
12
|
specRef,
|
|
11
13
|
cwd = process.cwd(),
|
|
12
14
|
sessionId = null,
|
|
15
|
+
stream = false,
|
|
13
16
|
}) {
|
|
14
17
|
const adapter = createAdapter(reviewerName, config, { cwd });
|
|
15
18
|
|
|
@@ -37,19 +40,37 @@ export async function runReview(content, {
|
|
|
37
40
|
schemaFile,
|
|
38
41
|
continueSession: !!sessionId,
|
|
39
42
|
sessionName: sessionId ? undefined : `review-${Date.now()}`,
|
|
43
|
+
stream: stream ? { reviewerName, tty: stream === true } : false,
|
|
40
44
|
});
|
|
41
45
|
|
|
42
46
|
const verdict = extractVerdict(result);
|
|
43
47
|
const executorFeedback = buildExecutorFeedback(verdict, cwd);
|
|
48
|
+
const rawOutput = result?.raw_output || result?.raw || '';
|
|
49
|
+
|
|
50
|
+
logReviewerOutput(rawOutput, reviewerName, cwd);
|
|
44
51
|
|
|
45
52
|
return {
|
|
46
53
|
reviewer: reviewerName,
|
|
47
54
|
verdict,
|
|
48
55
|
executor_feedback: executorFeedback,
|
|
56
|
+
reviewer_output: rawOutput,
|
|
57
|
+
progress: result?.progress || [],
|
|
49
58
|
session_id: adapter.sessionName || adapter.sessionId,
|
|
50
59
|
};
|
|
51
60
|
}
|
|
52
61
|
|
|
62
|
+
function logReviewerOutput(rawOutput, reviewerName, cwd) {
|
|
63
|
+
if (!rawOutput) return;
|
|
64
|
+
try {
|
|
65
|
+
const logDir = join(cwd, '.openairev', 'logs');
|
|
66
|
+
mkdirSync(logDir, { recursive: true });
|
|
67
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
68
|
+
writeFileSync(join(logDir, `review-${reviewerName}-${ts}.log`), rawOutput);
|
|
69
|
+
} catch {
|
|
70
|
+
// non-critical, don't fail the review
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
53
74
|
function buildExecutorFeedback(verdict, cwd) {
|
|
54
75
|
const feedbackPrompt = loadPromptFile('executor-feedback.md', cwd);
|
|
55
76
|
if (!verdict) return null;
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { describe, it } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
|
+
import { existsSync, readdirSync, readFileSync, mkdtempSync, rmSync, mkdirSync, writeFileSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { tmpdir } from 'os';
|
|
3
6
|
|
|
4
7
|
// Test verdict extraction logic (mirrors the function in review-runner.js)
|
|
5
8
|
|
|
@@ -77,3 +80,42 @@ describe('verdict extraction', () => {
|
|
|
77
80
|
assert.equal(verdict, null);
|
|
78
81
|
});
|
|
79
82
|
});
|
|
83
|
+
|
|
84
|
+
describe('reviewer output logging', () => {
|
|
85
|
+
function logReviewerOutput(rawOutput, reviewerName, cwd) {
|
|
86
|
+
if (!rawOutput) return;
|
|
87
|
+
try {
|
|
88
|
+
const logDir = join(cwd, '.openairev', 'logs');
|
|
89
|
+
mkdirSync(logDir, { recursive: true });
|
|
90
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
91
|
+
writeFileSync(join(logDir, `review-${reviewerName}-${ts}.log`), rawOutput);
|
|
92
|
+
} catch {
|
|
93
|
+
// non-critical
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
it('writes reviewer output to log file', () => {
|
|
98
|
+
const tmp = mkdtempSync(join(tmpdir(), 'orev-log-'));
|
|
99
|
+
try {
|
|
100
|
+
logReviewerOutput('raw reviewer thinking here', 'codex', tmp);
|
|
101
|
+
const logDir = join(tmp, '.openairev', 'logs');
|
|
102
|
+
assert.ok(existsSync(logDir));
|
|
103
|
+
const files = readdirSync(logDir);
|
|
104
|
+
assert.equal(files.length, 1);
|
|
105
|
+
assert.ok(files[0].startsWith('review-codex-'));
|
|
106
|
+
assert.equal(readFileSync(join(logDir, files[0]), 'utf-8'), 'raw reviewer thinking here');
|
|
107
|
+
} finally {
|
|
108
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('skips logging when output is empty', () => {
|
|
113
|
+
const tmp = mkdtempSync(join(tmpdir(), 'orev-log-'));
|
|
114
|
+
try {
|
|
115
|
+
logReviewerOutput('', 'codex', tmp);
|
|
116
|
+
assert.ok(!existsSync(join(tmp, '.openairev', 'logs')));
|
|
117
|
+
} finally {
|
|
118
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
});
|
package/src/tools/git-tools.js
CHANGED
|
@@ -18,7 +18,7 @@ const EXCLUDE_PATTERNS = [
|
|
|
18
18
|
* Uses minimal context (1 line) and excludes irrelevant files.
|
|
19
19
|
*/
|
|
20
20
|
export function getDiff(ref, { context = 1, excludes = EXCLUDE_PATTERNS } = {}) {
|
|
21
|
-
const excludeArgs = excludes.
|
|
21
|
+
const excludeArgs = ['--', ...excludes.map(p => `:!${p}`)];
|
|
22
22
|
const contextArgs = [`-U${context}`];
|
|
23
23
|
|
|
24
24
|
if (ref) {
|
package/src/version.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { join, dirname } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
7
|
+
|
|
8
|
+
export const VERSION = pkg.version;
|