openairev 0.2.1 → 0.2.3
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 +11 -11
- package/bin/openairev.js +1 -1
- package/package.json +13 -4
- package/src/cli/init.js +32 -20
- package/src/mcp/mcp-server.js +105 -246
- package/src/tools/git-tools.js +21 -5
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
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openairev",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
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": [
|
|
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
|
@@ -223,29 +223,12 @@ function copyIfMissing(src, dest) {
|
|
|
223
223
|
|
|
224
224
|
/**
|
|
225
225
|
* Set up Claude Code integration:
|
|
226
|
-
* - Add MCP server to .
|
|
226
|
+
* - Add MCP server to .mcp.json (project-level MCP config)
|
|
227
227
|
* - Append instructions to CLAUDE.md
|
|
228
228
|
*/
|
|
229
229
|
function setupClaudeCode(cwd) {
|
|
230
|
-
// MCP server config
|
|
231
|
-
|
|
232
|
-
const settingsPath = join(claudeDir, 'settings.json');
|
|
233
|
-
|
|
234
|
-
mkdirSync(claudeDir, { recursive: true });
|
|
235
|
-
|
|
236
|
-
let settings = {};
|
|
237
|
-
if (existsSync(settingsPath)) {
|
|
238
|
-
try { settings = JSON.parse(readFileSync(settingsPath, 'utf-8')); } catch { /* start fresh */ }
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
if (!settings.mcpServers) settings.mcpServers = {};
|
|
242
|
-
settings.mcpServers.openairev = {
|
|
243
|
-
command: 'node',
|
|
244
|
-
args: [MCP_SERVER_PATH],
|
|
245
|
-
};
|
|
246
|
-
|
|
247
|
-
writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
248
|
-
console.log(`${chalk.green('✓')} MCP server added to .claude/settings.json`);
|
|
230
|
+
// MCP server config — .mcp.json in project root
|
|
231
|
+
setupMcpJson(cwd);
|
|
249
232
|
|
|
250
233
|
// CLAUDE.md instructions
|
|
251
234
|
const claudeMdPath = join(cwd, 'CLAUDE.md');
|
|
@@ -258,6 +241,7 @@ This project uses OpenAIRev for independent AI code review. When the user asks t
|
|
|
258
241
|
|
|
259
242
|
- You are the **executor**. Set \`executor\` to \`"claude_code"\` when calling the tool.
|
|
260
243
|
- A different AI model will review your code independently.
|
|
244
|
+
- **IMPORTANT**: Pass only the diff for files you changed using \`diff_cmd\`, e.g. \`"git diff HEAD -- src/auth.ts src/routes.ts"\`. Do NOT let it auto-detect — the full repo diff may be too large.
|
|
261
245
|
- When you receive review feedback, treat it as **peer review** — use your judgment, don't blindly apply every suggestion.
|
|
262
246
|
- The review verdict includes \`critical_issues\`, \`repair_instructions\`, and a \`confidence\` score. Focus on high-confidence critical issues.
|
|
263
247
|
${marker}
|
|
@@ -275,11 +259,38 @@ ${marker}
|
|
|
275
259
|
}
|
|
276
260
|
}
|
|
277
261
|
|
|
262
|
+
/**
|
|
263
|
+
* Add openairev MCP server to .mcp.json in project root.
|
|
264
|
+
* This is the standard project-level MCP config used by Claude Code and Codex.
|
|
265
|
+
*/
|
|
266
|
+
function setupMcpJson(cwd) {
|
|
267
|
+
const mcpPath = join(cwd, '.mcp.json');
|
|
268
|
+
|
|
269
|
+
let mcpConfig = {};
|
|
270
|
+
if (existsSync(mcpPath)) {
|
|
271
|
+
try { mcpConfig = JSON.parse(readFileSync(mcpPath, 'utf-8')); } catch { /* start fresh */ }
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
|
|
275
|
+
|
|
276
|
+
if (mcpConfig.mcpServers.openairev) return; // already configured
|
|
277
|
+
|
|
278
|
+
mcpConfig.mcpServers.openairev = {
|
|
279
|
+
command: 'node',
|
|
280
|
+
args: [MCP_SERVER_PATH],
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2) + '\n');
|
|
284
|
+
console.log(`${chalk.green('✓')} MCP server added to .mcp.json`);
|
|
285
|
+
}
|
|
286
|
+
|
|
278
287
|
/**
|
|
279
288
|
* Set up Codex CLI integration:
|
|
289
|
+
* - Add MCP server to .mcp.json
|
|
280
290
|
* - Add instructions to AGENTS.md
|
|
281
291
|
*/
|
|
282
292
|
function setupCodex(cwd) {
|
|
293
|
+
setupMcpJson(cwd);
|
|
283
294
|
const agentsMdPath = join(cwd, 'AGENTS.md');
|
|
284
295
|
const marker = '<!-- openairev -->';
|
|
285
296
|
const instructions = `
|
|
@@ -290,6 +301,7 @@ This project uses OpenAIRev for independent AI code review. When the user asks t
|
|
|
290
301
|
|
|
291
302
|
- You are the **executor**. Set \`executor\` to \`"codex"\` when calling the tool.
|
|
292
303
|
- A different AI model will review your code independently.
|
|
304
|
+
- **IMPORTANT**: Pass only the diff for files you changed using \`diff_cmd\`, e.g. \`"git diff HEAD -- src/auth.ts src/routes.ts"\`. Do NOT let it auto-detect — the full repo diff may be too large.
|
|
293
305
|
- When you receive review feedback, treat it as **peer review** — use your judgment, don't blindly apply every suggestion.
|
|
294
306
|
- The review verdict includes \`critical_issues\`, \`repair_instructions\`, and a \`confidence\` score. Focus on high-confidence critical issues.
|
|
295
307
|
${marker}
|
package/src/mcp/mcp-server.js
CHANGED
|
@@ -1,261 +1,120 @@
|
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
while (true) {
|
|
31
|
-
const headerEnd = buf.indexOf('\r\n\r\n');
|
|
32
|
-
if (headerEnd === -1) break;
|
|
12
|
+
const cwd = process.cwd();
|
|
13
|
+
const config = loadConfig(cwd);
|
|
14
|
+
|
|
15
|
+
const server = new McpServer({
|
|
16
|
+
name: 'openairev',
|
|
17
|
+
version: '0.2.3',
|
|
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 to review. IMPORTANT: Pass only the diff for files YOU changed, not the entire repo. Use `git diff HEAD -- file1 file2` to scope it. If omitted, auto-detects from git which may be too large.'),
|
|
26
|
+
diff_cmd: z.string().optional().describe('The git command used to get the diff, e.g. "git diff HEAD -- src/auth.ts src/routes.ts". If provided instead of diff, the server will run this command to get the diff.'),
|
|
27
|
+
task_description: z.string().optional().describe('What the code is supposed to do. Used for requirement checking.'),
|
|
28
|
+
},
|
|
29
|
+
async ({ executor, diff, diff_cmd, task_description }) => {
|
|
30
|
+
const execAgent = executor || Object.keys(config.agents || {}).find(a => config.agents[a].available);
|
|
31
|
+
const reviewerName = getReviewer(config, execAgent);
|
|
32
|
+
if (!reviewerName) {
|
|
33
|
+
return { content: [{ type: 'text', text: `No reviewer configured for executor "${execAgent}"` }] };
|
|
34
|
+
}
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
}
|
|
36
|
+
let diffContent = diff;
|
|
37
|
+
if (!diffContent && diff_cmd) {
|
|
38
|
+
try {
|
|
39
|
+
const { execSync } = await import('child_process');
|
|
40
|
+
diffContent = execSync(diff_cmd, { encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024, cwd });
|
|
41
|
+
} catch (e) {
|
|
42
|
+
return { content: [{ type: 'text', text: `diff_cmd failed: ${e.message}` }] };
|
|
51
43
|
}
|
|
52
|
-
continue;
|
|
53
44
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
});
|
|
45
|
+
if (!diffContent) diffContent = getDiff();
|
|
46
|
+
if (!diffContent?.trim()) {
|
|
47
|
+
return { content: [{ type: 'text', text: 'No changes found to review.' }] };
|
|
73
48
|
}
|
|
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.1' },
|
|
98
|
-
},
|
|
99
|
-
id,
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
case 'notifications/initialized':
|
|
103
|
-
return null; // Notifications get no response
|
|
104
49
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
default:
|
|
122
|
-
return {
|
|
123
|
-
jsonrpc: '2.0',
|
|
124
|
-
error: { code: -32601, message: `Method not found: ${method}` },
|
|
125
|
-
id,
|
|
126
|
-
};
|
|
50
|
+
const review = await runReview(diffContent, {
|
|
51
|
+
config,
|
|
52
|
+
reviewerName,
|
|
53
|
+
taskDescription: task_description,
|
|
54
|
+
cwd,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const session = createSession({ executor: execAgent, reviewer: reviewerName });
|
|
58
|
+
session.iterations.push({ round: 1, review, timestamp: new Date().toISOString() });
|
|
59
|
+
session.final_verdict = review.verdict;
|
|
60
|
+
session.status = 'completed';
|
|
61
|
+
saveSession(session, cwd);
|
|
62
|
+
|
|
63
|
+
const text = review.executor_feedback || JSON.stringify(review.verdict || review, null, 2);
|
|
64
|
+
return { content: [{ type: 'text', text }] };
|
|
127
65
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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}`);
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
server.tool(
|
|
69
|
+
'openairev_status',
|
|
70
|
+
'Get the status and verdict of the most recent OpenAIRev review session.',
|
|
71
|
+
{},
|
|
72
|
+
async () => {
|
|
73
|
+
const { listSessions } = await import('../session/session-manager.js');
|
|
74
|
+
const sessions = listSessions(cwd, 1);
|
|
75
|
+
if (sessions.length === 0) {
|
|
76
|
+
return { content: [{ type: 'text', text: 'No review sessions found.' }] };
|
|
248
77
|
}
|
|
249
|
-
|
|
250
|
-
|
|
78
|
+
const last = sessions[0];
|
|
79
|
+
const text = JSON.stringify({ id: last.id, status: last.status, verdict: last.final_verdict, created: last.created }, null, 2);
|
|
80
|
+
return { content: [{ type: 'text', text }] };
|
|
251
81
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
server.tool(
|
|
85
|
+
'openairev_run_tests',
|
|
86
|
+
'Run the project test suite and return pass/fail results.',
|
|
87
|
+
{},
|
|
88
|
+
async () => {
|
|
89
|
+
const testCmd = config.tools?.run_tests || 'npm test';
|
|
90
|
+
const results = runToolGates(['run_tests'], cwd, { run_tests: testCmd });
|
|
91
|
+
return { content: [{ type: 'text', text: JSON.stringify(results.tests, null, 2) }] };
|
|
92
|
+
}
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
server.tool(
|
|
96
|
+
'openairev_run_lint',
|
|
97
|
+
'Run the project linter and return results.',
|
|
98
|
+
{},
|
|
99
|
+
async () => {
|
|
100
|
+
const lintCmd = config.tools?.run_lint || 'npm run lint';
|
|
101
|
+
const results = runToolGates(['run_lint'], cwd, { run_lint: lintCmd });
|
|
102
|
+
return { content: [{ type: 'text', text: JSON.stringify(results.lint, null, 2) }] };
|
|
103
|
+
}
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
server.tool(
|
|
107
|
+
'openairev_get_diff',
|
|
108
|
+
'Get the current git diff (staged, unstaged, or last commit).',
|
|
109
|
+
{
|
|
110
|
+
ref: z.string().optional().describe('Git ref to diff against'),
|
|
111
|
+
},
|
|
112
|
+
async ({ ref }) => {
|
|
113
|
+
const diffContent = getDiff(ref);
|
|
114
|
+
return { content: [{ type: 'text', text: diffContent || 'No changes found.' }] };
|
|
115
|
+
}
|
|
116
|
+
);
|
|
257
117
|
|
|
258
|
-
//
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
}
|
|
118
|
+
// Start server
|
|
119
|
+
const transport = new StdioServerTransport();
|
|
120
|
+
await server.connect(transport);
|
package/src/tools/git-tools.js
CHANGED
|
@@ -1,18 +1,34 @@
|
|
|
1
1
|
import { execFileSync } from 'child_process';
|
|
2
2
|
|
|
3
|
+
// Files to exclude from diffs — lock files, generated code, etc.
|
|
4
|
+
const EXCLUDE_PATTERNS = [
|
|
5
|
+
'*.lock',
|
|
6
|
+
'package-lock.json',
|
|
7
|
+
'yarn.lock',
|
|
8
|
+
'pnpm-lock.yaml',
|
|
9
|
+
'*.generated.*',
|
|
10
|
+
'dist/*',
|
|
11
|
+
'build/*',
|
|
12
|
+
'.next/*',
|
|
13
|
+
];
|
|
14
|
+
|
|
3
15
|
/**
|
|
4
16
|
* Get the current git diff. Tries staged first, then unstaged.
|
|
5
17
|
* Returns empty string if no changes found.
|
|
18
|
+
* Uses minimal context (1 line) and excludes irrelevant files.
|
|
6
19
|
*/
|
|
7
|
-
export function getDiff(ref) {
|
|
20
|
+
export function getDiff(ref, { context = 1, excludes = EXCLUDE_PATTERNS } = {}) {
|
|
21
|
+
const excludeArgs = excludes.flatMap(p => ['--', `:!${p}`]);
|
|
22
|
+
const contextArgs = [`-U${context}`];
|
|
23
|
+
|
|
8
24
|
if (ref) {
|
|
9
|
-
return gitExec(['diff', ref]);
|
|
25
|
+
return gitExec(['diff', ...contextArgs, ref, ...excludeArgs]);
|
|
10
26
|
}
|
|
11
27
|
|
|
12
|
-
const staged = gitExec(['diff', '--cached']);
|
|
28
|
+
const staged = gitExec(['diff', '--cached', ...contextArgs, ...excludeArgs]);
|
|
13
29
|
if (staged.trim()) return staged;
|
|
14
30
|
|
|
15
|
-
const unstaged = gitExec(['diff']);
|
|
31
|
+
const unstaged = gitExec(['diff', ...contextArgs, ...excludeArgs]);
|
|
16
32
|
if (unstaged.trim()) return unstaged;
|
|
17
33
|
|
|
18
34
|
return '';
|
|
@@ -20,7 +36,7 @@ export function getDiff(ref) {
|
|
|
20
36
|
|
|
21
37
|
function gitExec(args) {
|
|
22
38
|
try {
|
|
23
|
-
return execFileSync('git', args, { encoding: 'utf-8', maxBuffer:
|
|
39
|
+
return execFileSync('git', args, { encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024 });
|
|
24
40
|
} catch (e) {
|
|
25
41
|
throw new Error(`git ${args[0]} failed: ${e.message}`);
|
|
26
42
|
}
|