openairev 0.2.0 → 0.2.2

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 CHANGED
@@ -4,6 +4,10 @@ Cross-model AI code reviewer and workflow orchestrator for AI-assisted coding. T
4
4
 
5
5
  OpenAIRev orchestrates AI coding agents (Claude Code, Codex CLI, and more) so that one model reviews another's output. You choose which models pair up. The defaults are opinionated but fully configurable — including self-review if that's what you want.
6
6
 
7
+ <p align="center">
8
+ <img src="docs/architecture.svg" alt="OpenAIRev architecture — cross-model review cycle" width="680" />
9
+ </p>
10
+
7
11
  ## Install
8
12
 
9
13
  ```bash
@@ -183,9 +187,13 @@ The executor keeps full autonomy over what to fix.
183
187
 
184
188
  OpenAIRev includes an MCP server so both CLIs can trigger reviews as tool calls.
185
189
 
186
- ### Add to Claude Code
190
+ ### Automatic Setup
191
+
192
+ `openairev init` automatically adds the MCP server to your project's `.mcp.json`. Both Claude Code and Codex CLI read this file.
193
+
194
+ ### Manual Setup
187
195
 
188
- In your project's `.claude/settings.json` or `~/.claude/settings.json`:
196
+ Add to `.mcp.json` in your project root:
189
197
 
190
198
  ```json
191
199
  {
@@ -198,15 +206,7 @@ In your project's `.claude/settings.json` or `~/.claude/settings.json`:
198
206
  }
199
207
  ```
200
208
 
201
- ### Add to Codex CLI
202
-
203
- In `~/.codex/config.toml`:
204
-
205
- ```toml
206
- [mcp_servers.openairev]
207
- command = "node"
208
- args = ["/path/to/openairev/src/mcp/mcp-server.js"]
209
- ```
209
+ Restart your agent CLI after adding.
210
210
 
211
211
  ### MCP Tools
212
212
 
package/bin/openairev.js CHANGED
@@ -12,7 +12,7 @@ const program = new Command();
12
12
  program
13
13
  .name('openairev')
14
14
  .description('OpenAIRev — cross-model AI code reviewer')
15
- .version('0.2.0');
15
+ .version('0.2.2');
16
16
 
17
17
  program
18
18
  .command('init')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openairev",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Cross-model AI code reviewer — independent review for AI-assisted coding workflows",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,12 +16,21 @@
16
16
  "test": "node --test src/**/*.test.js"
17
17
  },
18
18
  "dependencies": {
19
+ "@modelcontextprotocol/sdk": "^1.27.1",
20
+ "chalk": "^5.4.1",
19
21
  "commander": "^13.1.0",
20
22
  "inquirer": "^12.6.0",
21
- "yaml": "^2.7.1",
22
- "chalk": "^5.4.1"
23
+ "yaml": "^2.7.1"
23
24
  },
24
- "keywords": ["ai", "code-review", "claude", "codex", "mcp", "cross-model", "reviewer"],
25
+ "keywords": [
26
+ "ai",
27
+ "code-review",
28
+ "claude",
29
+ "codex",
30
+ "mcp",
31
+ "cross-model",
32
+ "reviewer"
33
+ ],
25
34
  "license": "MIT",
26
35
  "repository": {
27
36
  "type": "git",
package/src/cli/init.js CHANGED
@@ -1,5 +1,5 @@
1
- import { writeFileSync, mkdirSync, copyFileSync, existsSync } from 'fs';
2
- import { join, dirname } from 'path';
1
+ import { writeFileSync, readFileSync, mkdirSync, copyFileSync, existsSync } from 'fs';
2
+ import { join, dirname, resolve } from 'path';
3
3
  import { fileURLToPath } from 'url';
4
4
  import inquirer from 'inquirer';
5
5
  import YAML from 'yaml';
@@ -8,7 +8,9 @@ import { getConfigDir, getConfigPath, configExists } from '../config/config-load
8
8
  import { detectAgent } from '../agents/detect.js';
9
9
 
10
10
  const __dirname = dirname(fileURLToPath(import.meta.url));
11
- const PROMPTS_SRC = join(__dirname, '../../prompts');
11
+ const PACKAGE_ROOT = resolve(__dirname, '../..');
12
+ const PROMPTS_SRC = join(PACKAGE_ROOT, 'prompts');
13
+ const MCP_SERVER_PATH = join(PACKAGE_ROOT, 'src/mcp/mcp-server.js');
12
14
 
13
15
  export async function initCommand() {
14
16
  const cwd = process.cwd();
@@ -187,6 +189,15 @@ export async function initCommand() {
187
189
 
188
190
  console.log(`\n${chalk.green('✓')} Config written to .openairev/config.yaml`);
189
191
  console.log(`${chalk.green('✓')} Prompt templates written to .openairev/prompts/`);
192
+
193
+ // Configure agent integrations
194
+ if (answers.agents.includes('claude_code')) {
195
+ setupClaudeCode(cwd);
196
+ }
197
+ if (answers.agents.includes('codex')) {
198
+ setupCodex(cwd);
199
+ }
200
+
190
201
  console.log(`\nRun ${chalk.cyan('openairev review')} to trigger a review.\n`);
191
202
  }
192
203
 
@@ -209,3 +220,99 @@ function copyIfMissing(src, dest) {
209
220
  copyFileSync(src, dest);
210
221
  }
211
222
  }
223
+
224
+ /**
225
+ * Set up Claude Code integration:
226
+ * - Add MCP server to .mcp.json (project-level MCP config)
227
+ * - Append instructions to CLAUDE.md
228
+ */
229
+ function setupClaudeCode(cwd) {
230
+ // MCP server config — .mcp.json in project root
231
+ setupMcpJson(cwd);
232
+
233
+ // CLAUDE.md instructions
234
+ const claudeMdPath = join(cwd, 'CLAUDE.md');
235
+ const marker = '<!-- openairev -->';
236
+ const instructions = `
237
+ ${marker}
238
+ ## OpenAIRev — Cross-Model Code Review
239
+
240
+ This project uses OpenAIRev for independent AI code review. When the user asks to "review", "get a review", or "check my changes", use the \`openairev_review\` MCP tool.
241
+
242
+ - You are the **executor**. Set \`executor\` to \`"claude_code"\` when calling the tool.
243
+ - A different AI model will review your code independently.
244
+ - When you receive review feedback, treat it as **peer review** — use your judgment, don't blindly apply every suggestion.
245
+ - The review verdict includes \`critical_issues\`, \`repair_instructions\`, and a \`confidence\` score. Focus on high-confidence critical issues.
246
+ ${marker}
247
+ `;
248
+
249
+ if (existsSync(claudeMdPath)) {
250
+ const existing = readFileSync(claudeMdPath, 'utf-8');
251
+ if (!existing.includes(marker)) {
252
+ writeFileSync(claudeMdPath, existing + '\n' + instructions.trim() + '\n');
253
+ console.log(`${chalk.green('✓')} OpenAIRev instructions appended to CLAUDE.md`);
254
+ }
255
+ } else {
256
+ writeFileSync(claudeMdPath, instructions.trim() + '\n');
257
+ console.log(`${chalk.green('✓')} CLAUDE.md created with OpenAIRev instructions`);
258
+ }
259
+ }
260
+
261
+ /**
262
+ * Add openairev MCP server to .mcp.json in project root.
263
+ * This is the standard project-level MCP config used by Claude Code and Codex.
264
+ */
265
+ function setupMcpJson(cwd) {
266
+ const mcpPath = join(cwd, '.mcp.json');
267
+
268
+ let mcpConfig = {};
269
+ if (existsSync(mcpPath)) {
270
+ try { mcpConfig = JSON.parse(readFileSync(mcpPath, 'utf-8')); } catch { /* start fresh */ }
271
+ }
272
+
273
+ if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
274
+
275
+ if (mcpConfig.mcpServers.openairev) return; // already configured
276
+
277
+ mcpConfig.mcpServers.openairev = {
278
+ command: 'node',
279
+ args: [MCP_SERVER_PATH],
280
+ };
281
+
282
+ writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2) + '\n');
283
+ console.log(`${chalk.green('✓')} MCP server added to .mcp.json`);
284
+ }
285
+
286
+ /**
287
+ * Set up Codex CLI integration:
288
+ * - Add MCP server to .mcp.json
289
+ * - Add instructions to AGENTS.md
290
+ */
291
+ function setupCodex(cwd) {
292
+ setupMcpJson(cwd);
293
+ const agentsMdPath = join(cwd, 'AGENTS.md');
294
+ const marker = '<!-- openairev -->';
295
+ const instructions = `
296
+ ${marker}
297
+ ## OpenAIRev — Cross-Model Code Review
298
+
299
+ This project uses OpenAIRev for independent AI code review. When the user asks to "review", "get a review", or "check my changes", use the \`openairev_review\` MCP tool.
300
+
301
+ - You are the **executor**. Set \`executor\` to \`"codex"\` when calling the tool.
302
+ - A different AI model will review your code independently.
303
+ - When you receive review feedback, treat it as **peer review** — use your judgment, don't blindly apply every suggestion.
304
+ - The review verdict includes \`critical_issues\`, \`repair_instructions\`, and a \`confidence\` score. Focus on high-confidence critical issues.
305
+ ${marker}
306
+ `;
307
+
308
+ if (existsSync(agentsMdPath)) {
309
+ const existing = readFileSync(agentsMdPath, 'utf-8');
310
+ if (!existing.includes(marker)) {
311
+ writeFileSync(agentsMdPath, existing + '\n' + instructions.trim() + '\n');
312
+ console.log(`${chalk.green('✓')} OpenAIRev instructions appended to AGENTS.md`);
313
+ }
314
+ } else {
315
+ writeFileSync(agentsMdPath, instructions.trim() + '\n');
316
+ console.log(`${chalk.green('✓')} AGENTS.md created with OpenAIRev instructions`);
317
+ }
318
+ }
@@ -1,261 +1,110 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
+ import { z } from 'zod';
1
6
  import { loadConfig, getReviewer } from '../config/config-loader.js';
2
7
  import { getDiff } from '../tools/git-tools.js';
3
8
  import { runToolGates } from '../tools/tool-runner.js';
4
9
  import { runReview } from '../review/review-runner.js';
5
10
  import { createSession, saveSession } from '../session/session-manager.js';
6
11
 
7
- /**
8
- * MCP Server using stdio with Content-Length framing per MCP spec.
9
- * Both Claude Code and Codex can call this as an MCP server.
10
- */
11
- export function startMcpServer() {
12
- const cwd = process.cwd();
13
- const config = loadConfig(cwd);
14
-
15
- let buffer = Buffer.alloc(0);
16
-
17
- process.stdin.on('data', (chunk) => {
18
- buffer = Buffer.concat([buffer, chunk]);
19
- buffer = processBuffer(buffer, config, cwd);
20
- });
21
-
22
- process.stdin.on('end', () => process.exit(0));
23
- }
24
-
25
- /**
26
- * Parse Content-Length framed messages from buffer.
27
- * Format: "Content-Length: <N>\r\n\r\n<JSON body of N bytes>"
28
- */
29
- function processBuffer(buf, config, cwd) {
30
- while (true) {
31
- const headerEnd = buf.indexOf('\r\n\r\n');
32
- if (headerEnd === -1) break;
33
-
34
- const header = buf.slice(0, headerEnd).toString('utf-8');
35
- const match = header.match(/Content-Length:\s*(\d+)/i);
36
- if (!match) {
37
- // Fallback: try newline-delimited JSON for compatibility
38
- const nlIndex = buf.indexOf('\n');
39
- if (nlIndex === -1) break;
40
- const line = buf.slice(0, nlIndex).toString('utf-8').trim();
41
- buf = buf.slice(nlIndex + 1);
42
- if (line) {
43
- try {
44
- const request = JSON.parse(line);
45
- handleRequest(request, config, cwd).then(response => {
46
- if (response !== null) sendResponse(response);
47
- }).catch(() => {});
48
- } catch {
49
- // skip malformed
50
- }
51
- }
52
- continue;
12
+ const cwd = process.cwd();
13
+ const config = loadConfig(cwd);
14
+
15
+ const server = new McpServer({
16
+ name: 'openairev',
17
+ version: '0.2.2',
18
+ });
19
+
20
+ server.tool(
21
+ 'openairev_review',
22
+ 'TRIGGER: Use this tool when the user says "review", "review my code", "get a review", "check my changes", "openairev", or asks for independent/cross-model code review. Sends current code changes to a DIFFERENT AI model for independent review. Returns a structured verdict with critical issues, test gaps, risk level, confidence score, and repair instructions.',
23
+ {
24
+ executor: z.string().optional().describe('Which agent wrote the code (claude_code or codex). If you are Claude Code, set this to "claude_code". If you are Codex, set this to "codex".'),
25
+ diff: z.string().optional().describe('The diff or code to review. If omitted, auto-detects from git.'),
26
+ task_description: z.string().optional().describe('What the code is supposed to do. Used for requirement checking.'),
27
+ },
28
+ async ({ executor, diff, task_description }) => {
29
+ const execAgent = executor || Object.keys(config.agents || {}).find(a => config.agents[a].available);
30
+ const reviewerName = getReviewer(config, execAgent);
31
+ if (!reviewerName) {
32
+ return { content: [{ type: 'text', text: `No reviewer configured for executor "${execAgent}"` }] };
53
33
  }
54
34
 
55
- const contentLength = parseInt(match[1], 10);
56
- const bodyStart = headerEnd + 4;
57
- if (buf.length < bodyStart + contentLength) break; // incomplete body
58
-
59
- const body = buf.slice(bodyStart, bodyStart + contentLength).toString('utf-8');
60
- buf = buf.slice(bodyStart + contentLength);
61
-
62
- try {
63
- const request = JSON.parse(body);
64
- handleRequest(request, config, cwd).then(response => {
65
- if (response !== null) sendResponse(response);
66
- });
67
- } catch {
68
- sendResponse({
69
- jsonrpc: '2.0',
70
- error: { code: -32700, message: 'Parse error' },
71
- id: null,
72
- });
35
+ const diffContent = diff || getDiff();
36
+ if (!diffContent?.trim()) {
37
+ return { content: [{ type: 'text', text: 'No changes found to review.' }] };
73
38
  }
74
- }
75
- return buf; // Return unconsumed remainder
76
- }
77
-
78
- /**
79
- * Send a JSON-RPC response with Content-Length framing.
80
- */
81
- function sendResponse(response) {
82
- const body = JSON.stringify(response);
83
- const header = `Content-Length: ${Buffer.byteLength(body)}\r\n\r\n`;
84
- process.stdout.write(header + body);
85
- }
86
-
87
- async function handleRequest(request, config, cwd) {
88
- const { method, params, id } = request;
89
-
90
- switch (method) {
91
- case 'initialize':
92
- return {
93
- jsonrpc: '2.0',
94
- result: {
95
- protocolVersion: '2024-11-05',
96
- capabilities: { tools: {} },
97
- serverInfo: { name: 'openairev', version: '0.2.0' },
98
- },
99
- id,
100
- };
101
-
102
- case 'notifications/initialized':
103
- return null; // Notifications get no response
104
-
105
- case 'tools/list':
106
- return {
107
- jsonrpc: '2.0',
108
- result: {
109
- tools: getToolDefinitions(),
110
- },
111
- id,
112
- };
113
-
114
- case 'tools/call':
115
- return {
116
- jsonrpc: '2.0',
117
- result: await callTool(params.name, params.arguments || {}, config, cwd),
118
- id,
119
- };
120
39
 
121
- default:
122
- return {
123
- jsonrpc: '2.0',
124
- error: { code: -32601, message: `Method not found: ${method}` },
125
- id,
126
- };
40
+ const review = await runReview(diffContent, {
41
+ config,
42
+ reviewerName,
43
+ taskDescription: task_description,
44
+ cwd,
45
+ });
46
+
47
+ const session = createSession({ executor: execAgent, reviewer: reviewerName });
48
+ session.iterations.push({ round: 1, review, timestamp: new Date().toISOString() });
49
+ session.final_verdict = review.verdict;
50
+ session.status = 'completed';
51
+ saveSession(session, cwd);
52
+
53
+ const text = review.executor_feedback || JSON.stringify(review.verdict || review, null, 2);
54
+ return { content: [{ type: 'text', text }] };
127
55
  }
128
- }
129
-
130
- function getToolDefinitions() {
131
- return [
132
- {
133
- name: 'openairev_review',
134
- description: 'TRIGGER: Use this tool when the user says "review", "review my code", "get a review", "check my changes", "openairev", or asks for independent/cross-model code review. Sends current code changes to a DIFFERENT AI model for independent review. Returns a structured verdict with critical issues, test gaps, risk level, confidence score, and repair instructions. The reviewer is never the same model as the executor — this ensures unbiased, independent judgment.',
135
- inputSchema: {
136
- type: 'object',
137
- properties: {
138
- executor: {
139
- type: 'string',
140
- description: 'Which agent wrote the code (claude_code or codex). If you are Claude Code, set this to "claude_code". If you are Codex, set this to "codex". This determines which other model will review.',
141
- },
142
- diff: {
143
- type: 'string',
144
- description: 'The diff or code to review. If omitted, auto-detects from git (staged → unstaged → last commit).',
145
- },
146
- task_description: {
147
- type: 'string',
148
- description: 'What the code is supposed to do. Used for requirement checking. Include acceptance criteria if available.',
149
- },
150
- },
151
- },
152
- },
153
- {
154
- name: 'openairev_status',
155
- description: 'Get the status and verdict of the most recent OpenAIRev review session. Use when the user asks "what did the review say", "review status", or "last review results".',
156
- inputSchema: { type: 'object', properties: {} },
157
- },
158
- {
159
- name: 'openairev_run_tests',
160
- description: 'Run the project test suite and return pass/fail results. Use when user asks to "run tests" or you need to verify code before review.',
161
- inputSchema: { type: 'object', properties: {} },
162
- },
163
- {
164
- name: 'openairev_run_lint',
165
- description: 'Run the project linter and return results.',
166
- inputSchema: { type: 'object', properties: {} },
167
- },
168
- {
169
- name: 'openairev_get_diff',
170
- description: 'Get the current git diff (staged, unstaged, or last commit).',
171
- inputSchema: {
172
- type: 'object',
173
- properties: {
174
- ref: { type: 'string', description: 'Git ref to diff against' },
175
- },
176
- },
177
- },
178
- ];
179
- }
180
-
181
- async function callTool(name, args, config, cwd) {
182
- try {
183
- switch (name) {
184
- case 'openairev_review': {
185
- const executor = args.executor || Object.keys(config.agents).find(a => config.agents[a].available);
186
- const reviewerName = getReviewer(config, executor);
187
- if (!reviewerName) {
188
- return formatResult(`No reviewer configured for executor "${executor}"`);
189
- }
190
- const diff = args.diff || getDiff();
191
-
192
- if (!diff.trim()) {
193
- return formatResult('No changes found to review.');
194
- }
195
-
196
- const review = await runReview(diff, {
197
- config,
198
- reviewerName,
199
- taskDescription: args.task_description,
200
- cwd,
201
- });
202
-
203
- // Save session
204
- const session = createSession({ executor, reviewer: reviewerName });
205
- session.iterations.push({ round: 1, review, timestamp: new Date().toISOString() });
206
- session.final_verdict = review.verdict;
207
- session.status = 'completed';
208
- saveSession(session, cwd);
209
-
210
- // Return executor-facing feedback (framed as peer review, not user command)
211
- return formatResult(review.executor_feedback || JSON.stringify(review.verdict || review, null, 2));
212
- }
213
-
214
- case 'openairev_status': {
215
- const { listSessions } = await import('../session/session-manager.js');
216
- const sessions = listSessions(cwd, 1);
217
- if (sessions.length === 0) {
218
- return formatResult('No review sessions found.');
219
- }
220
- const last = sessions[0];
221
- return formatResult(JSON.stringify({
222
- id: last.id,
223
- status: last.status,
224
- verdict: last.final_verdict,
225
- created: last.created,
226
- }, null, 2));
227
- }
228
-
229
- case 'openairev_run_tests': {
230
- const testCmd = config.tools?.run_tests || 'npm test';
231
- const results = runToolGates(['run_tests'], cwd, { run_tests: testCmd });
232
- return formatResult(JSON.stringify(results.tests, null, 2));
233
- }
234
-
235
- case 'openairev_run_lint': {
236
- const lintCmd = config.tools?.run_lint || 'npm run lint';
237
- const results = runToolGates(['run_lint'], cwd, { run_lint: lintCmd });
238
- return formatResult(JSON.stringify(results.lint, null, 2));
239
- }
240
-
241
- case 'openairev_get_diff': {
242
- const diff = getDiff(args.ref);
243
- return formatResult(diff || 'No changes found.');
244
- }
245
-
246
- default:
247
- return formatResult(`Unknown tool: ${name}`);
56
+ );
57
+
58
+ server.tool(
59
+ 'openairev_status',
60
+ 'Get the status and verdict of the most recent OpenAIRev review session.',
61
+ {},
62
+ async () => {
63
+ const { listSessions } = await import('../session/session-manager.js');
64
+ const sessions = listSessions(cwd, 1);
65
+ if (sessions.length === 0) {
66
+ return { content: [{ type: 'text', text: 'No review sessions found.' }] };
248
67
  }
249
- } catch (e) {
250
- return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
68
+ const last = sessions[0];
69
+ const text = JSON.stringify({ id: last.id, status: last.status, verdict: last.final_verdict, created: last.created }, null, 2);
70
+ return { content: [{ type: 'text', text }] };
251
71
  }
252
- }
253
-
254
- function formatResult(text) {
255
- return { content: [{ type: 'text', text }] };
256
- }
72
+ );
73
+
74
+ server.tool(
75
+ 'openairev_run_tests',
76
+ 'Run the project test suite and return pass/fail results.',
77
+ {},
78
+ async () => {
79
+ const testCmd = config.tools?.run_tests || 'npm test';
80
+ const results = runToolGates(['run_tests'], cwd, { run_tests: testCmd });
81
+ return { content: [{ type: 'text', text: JSON.stringify(results.tests, null, 2) }] };
82
+ }
83
+ );
84
+
85
+ server.tool(
86
+ 'openairev_run_lint',
87
+ 'Run the project linter and return results.',
88
+ {},
89
+ async () => {
90
+ const lintCmd = config.tools?.run_lint || 'npm run lint';
91
+ const results = runToolGates(['run_lint'], cwd, { run_lint: lintCmd });
92
+ return { content: [{ type: 'text', text: JSON.stringify(results.lint, null, 2) }] };
93
+ }
94
+ );
95
+
96
+ server.tool(
97
+ 'openairev_get_diff',
98
+ 'Get the current git diff (staged, unstaged, or last commit).',
99
+ {
100
+ ref: z.string().optional().describe('Git ref to diff against'),
101
+ },
102
+ async ({ ref }) => {
103
+ const diffContent = getDiff(ref);
104
+ return { content: [{ type: 'text', text: diffContent || 'No changes found.' }] };
105
+ }
106
+ );
257
107
 
258
- // If run directly, start the server
259
- if (process.argv[1] && process.argv[1].includes('mcp-server')) {
260
- startMcpServer();
261
- }
108
+ // Start server
109
+ const transport = new StdioServerTransport();
110
+ await server.connect(transport);