openairev 0.3.0 → 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/codex.js +10 -5
- package/src/agents/stream-summarizer.js +37 -22
- package/src/mcp/mcp-server.js +13 -3
- package/src/review/review-runner.js +2 -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
package/src/agents/codex.js
CHANGED
|
@@ -31,8 +31,11 @@ export class CodexAdapter {
|
|
|
31
31
|
args.push('--output-schema', schemaPath);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
const
|
|
35
|
-
|
|
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 });
|
|
36
39
|
|
|
37
40
|
try {
|
|
38
41
|
const lines = result.stdout.trim().split('\n');
|
|
@@ -62,15 +65,17 @@ export class CodexAdapter {
|
|
|
62
65
|
this.sessionId = sessionId;
|
|
63
66
|
}
|
|
64
67
|
|
|
68
|
+
const progress = summarizer?.getProgress() || [];
|
|
69
|
+
|
|
65
70
|
if (agentMessage) {
|
|
66
71
|
try {
|
|
67
|
-
return { result: JSON.parse(agentMessage), raw_output: result.stdout, session_id: this.sessionId };
|
|
72
|
+
return { result: JSON.parse(agentMessage), raw_output: result.stdout, progress, session_id: this.sessionId };
|
|
68
73
|
} catch {
|
|
69
|
-
return { result: agentMessage, raw_output: result.stdout, session_id: this.sessionId };
|
|
74
|
+
return { result: agentMessage, raw_output: result.stdout, progress, session_id: this.sessionId };
|
|
70
75
|
}
|
|
71
76
|
}
|
|
72
77
|
|
|
73
|
-
return { raw: result.stdout, raw_output: result.stdout, session_id: this.sessionId };
|
|
78
|
+
return { raw: result.stdout, raw_output: result.stdout, progress, session_id: this.sessionId };
|
|
74
79
|
} catch {
|
|
75
80
|
return { raw: result.stdout, raw_output: result.stdout, error: 'Failed to parse output' };
|
|
76
81
|
}
|
|
@@ -2,51 +2,70 @@ import chalk from 'chalk';
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Creates a summarizer callback for Codex NDJSON event streams.
|
|
5
|
-
*
|
|
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.
|
|
6
8
|
*/
|
|
7
|
-
export function createCodexSummarizer({ reviewerName } = {}) {
|
|
9
|
+
export function createCodexSummarizer({ reviewerName, tty = true } = {}) {
|
|
8
10
|
const seenFiles = new Set();
|
|
11
|
+
const progressLines = [];
|
|
9
12
|
let buffer = '';
|
|
10
13
|
let started = false;
|
|
11
14
|
|
|
12
|
-
|
|
15
|
+
const summarizer = (chunk) => {
|
|
13
16
|
if (!started) {
|
|
14
17
|
started = true;
|
|
15
|
-
if (reviewerName)
|
|
18
|
+
if (reviewerName) emit(`reviewer: ${reviewerName}`, 'cyan');
|
|
16
19
|
}
|
|
17
20
|
buffer += chunk;
|
|
18
21
|
const lines = buffer.split('\n');
|
|
19
|
-
buffer = lines.pop();
|
|
22
|
+
buffer = lines.pop();
|
|
20
23
|
|
|
21
24
|
for (const line of lines) {
|
|
22
25
|
if (!line.trim()) continue;
|
|
23
26
|
try {
|
|
24
27
|
const event = JSON.parse(line);
|
|
25
|
-
summarizeCodexEvent(event, seenFiles);
|
|
28
|
+
summarizeCodexEvent(event, seenFiles, { emit });
|
|
26
29
|
} catch {
|
|
27
30
|
// skip non-JSON
|
|
28
31
|
}
|
|
29
32
|
}
|
|
30
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;
|
|
31
50
|
}
|
|
32
51
|
|
|
33
|
-
function summarizeCodexEvent(event, seenFiles) {
|
|
52
|
+
function summarizeCodexEvent(event, seenFiles, { emit }) {
|
|
34
53
|
const type = event.type;
|
|
35
54
|
|
|
36
55
|
if (type === 'thread.started') {
|
|
37
|
-
|
|
56
|
+
emit(`session: ${event.thread_id}`);
|
|
38
57
|
}
|
|
39
58
|
|
|
40
59
|
if (type === 'item.started' && event.item?.type === 'todo_list') {
|
|
41
60
|
const items = event.item.items?.map(i => i.text) || [];
|
|
42
|
-
|
|
43
|
-
for (const item of items)
|
|
61
|
+
emit('plan:', 'cyan');
|
|
62
|
+
for (const item of items) emit(` • ${item}`);
|
|
44
63
|
}
|
|
45
64
|
|
|
46
65
|
if (type === 'item.completed' && event.item?.type === 'todo_list') {
|
|
47
66
|
const items = event.item.items || [];
|
|
48
67
|
const done = items.filter(i => i.completed).length;
|
|
49
|
-
|
|
68
|
+
emit(`progress: ${done}/${items.length} tasks done`);
|
|
50
69
|
}
|
|
51
70
|
|
|
52
71
|
if (type === 'item.started' && event.item?.type === 'command_execution') {
|
|
@@ -55,10 +74,10 @@ function summarizeCodexEvent(event, seenFiles) {
|
|
|
55
74
|
const file = extractFileFromCmd(cmd);
|
|
56
75
|
if (file && !seenFiles.has(file)) {
|
|
57
76
|
seenFiles.add(file);
|
|
58
|
-
|
|
77
|
+
emit(`reading: ${file}`);
|
|
59
78
|
} else if (!file) {
|
|
60
79
|
const short = summarizeCmd(cmd);
|
|
61
|
-
if (short)
|
|
80
|
+
if (short) emit(`running: ${short}`);
|
|
62
81
|
}
|
|
63
82
|
}
|
|
64
83
|
|
|
@@ -66,27 +85,27 @@ function summarizeCodexEvent(event, seenFiles) {
|
|
|
66
85
|
const exit = event.item.exit_code;
|
|
67
86
|
if (exit !== null && exit !== 0) {
|
|
68
87
|
const cmd = summarizeCmd(event.item.command || '');
|
|
69
|
-
|
|
88
|
+
emit(`command failed (exit ${exit}): ${cmd}`, 'yellow');
|
|
70
89
|
}
|
|
71
90
|
}
|
|
72
91
|
|
|
73
92
|
if (type === 'item.started' && event.item?.type === 'agent_message') {
|
|
74
|
-
|
|
93
|
+
emit('generating verdict...', 'cyan');
|
|
75
94
|
}
|
|
76
95
|
|
|
77
96
|
if (type === 'item.completed' && event.item?.type === 'agent_message') {
|
|
78
|
-
|
|
97
|
+
emit('verdict ready', 'green');
|
|
79
98
|
}
|
|
80
99
|
|
|
81
100
|
if (type === 'turn.completed' && event.usage) {
|
|
82
101
|
const { input_tokens, output_tokens } = event.usage;
|
|
83
102
|
const total = input_tokens + output_tokens;
|
|
84
|
-
|
|
103
|
+
emit(`tokens: ${fmt(total)} total (${fmt(input_tokens)} in / ${fmt(output_tokens)} out)`);
|
|
85
104
|
}
|
|
86
105
|
|
|
87
106
|
if (type === 'error' || type === 'turn.failed') {
|
|
88
107
|
const msg = event.message || event.error?.message || 'unknown error';
|
|
89
|
-
|
|
108
|
+
emit(`error: ${msg}`, 'red');
|
|
90
109
|
}
|
|
91
110
|
}
|
|
92
111
|
|
|
@@ -118,7 +137,3 @@ function fmt(n) {
|
|
|
118
137
|
if (n >= 1_000) return (n / 1_000).toFixed(1) + 'k';
|
|
119
138
|
return String(n);
|
|
120
139
|
}
|
|
121
|
-
|
|
122
|
-
function log(msg) {
|
|
123
|
-
process.stderr.write(msg + '\n');
|
|
124
|
-
}
|
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
|
|
|
@@ -40,7 +40,7 @@ export async function runReview(content, {
|
|
|
40
40
|
schemaFile,
|
|
41
41
|
continueSession: !!sessionId,
|
|
42
42
|
sessionName: sessionId ? undefined : `review-${Date.now()}`,
|
|
43
|
-
stream: stream ? reviewerName : false,
|
|
43
|
+
stream: stream ? { reviewerName, tty: stream === true } : false,
|
|
44
44
|
});
|
|
45
45
|
|
|
46
46
|
const verdict = extractVerdict(result);
|
|
@@ -54,6 +54,7 @@ export async function runReview(content, {
|
|
|
54
54
|
verdict,
|
|
55
55
|
executor_feedback: executorFeedback,
|
|
56
56
|
reviewer_output: rawOutput,
|
|
57
|
+
progress: result?.progress || [],
|
|
57
58
|
session_id: adapter.sessionName || adapter.sessionId,
|
|
58
59
|
};
|
|
59
60
|
}
|
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;
|