From 851b55f6610ecfb8e07813ffc7a3d54ee1258937 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Thu, 22 Jan 2026 16:26:28 +0800 Subject: [PATCH] feat: Add review tool for code review functionality This PR adds a new `review` tool to the Codex MCP server that enables automated code review using the Codex CLI's `codex review` command. ## Changes ### New Files - `src/tools/review-codex.tool.ts`: New review tool implementation ### Modified Files - `src/tools/index.ts`: Register the review tool in the tool registry - `src/utils/commandExecutor.ts`: Add `cwd` parameter support for executing commands in specific working directories ## Features The review tool supports: - `--uncommitted`: Review staged, unstaged, and untracked changes - `--commit `: Review changes from a specific commit - `--base `: Review changes against a base branch - `--title`: Optional commit title for review summary - `--prompt`: Custom review instructions - `-c/--config`: Configuration overrides - `--enable/--disable`: Feature flag controls - `workingDir`: Specify the git repository directory - `timeout`: Custom execution timeout - `debug`: Return raw output for troubleshooting ## Usage Examples ```json { "uncommitted": true, "workingDir": "/path/to/repo" } { "commit": "abc1234", "workingDir": "/path/to/repo" } { "base": "main", "workingDir": "/path/to/repo" } ``` via [HAPI](https://hapi.run) Co-Authored-By: HAPI --- src/tools/index.ts | 4 +- src/tools/review-codex.tool.ts | 272 +++++++++++++++++++++++++++++++++ src/utils/commandExecutor.ts | 6 +- 3 files changed, 279 insertions(+), 3 deletions(-) create mode 100644 src/tools/review-codex.tool.ts diff --git a/src/tools/index.ts b/src/tools/index.ts index 264a994..cb73e8f 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -2,7 +2,7 @@ import { toolRegistry } from './registry.js'; import { askCodexTool } from './ask-codex.tool.js'; import { batchCodexTool } from './batch-codex.tool.js'; -// import { reviewCodexTool } from './review-codex.tool.js'; +import { reviewCodexTool } from './review-codex.tool.js'; import { pingTool, helpTool, versionTool } from './simple-tools.js'; import { brainstormTool } from './brainstorm.tool.js'; import { fetchChunkTool } from './fetch-chunk.tool.js'; @@ -11,7 +11,7 @@ import { timeoutTestTool } from './timeout-test.tool.js'; toolRegistry.push( askCodexTool, batchCodexTool, - // reviewCodexTool, + reviewCodexTool, pingTool, helpTool, versionTool, diff --git a/src/tools/review-codex.tool.ts b/src/tools/review-codex.tool.ts new file mode 100644 index 0000000..d8a4e61 --- /dev/null +++ b/src/tools/review-codex.tool.ts @@ -0,0 +1,272 @@ +import { z } from 'zod'; +import { UnifiedTool } from './registry.js'; +import { executeCommandDetailed } from '../utils/commandExecutor.js'; +import { CLI } from '../constants.js'; +import { Logger } from '../utils/logger.js'; + +const reviewCodexArgsSchema = z.object({ + uncommitted: z + .boolean() + .optional() + .describe('Review staged, unstaged, and untracked changes'), + commit: z + .string() + .optional() + .describe('Review the changes introduced by a specific commit SHA'), + base: z + .string() + .optional() + .describe('Review changes against the given base branch'), + title: z + .string() + .optional() + .describe('Optional commit title to display in the review summary'), + prompt: z + .string() + .optional() + .describe('Custom review instructions'), + config: z + .string() + .optional() + .describe("Configuration override as 'key=value' string"), + enableFeatures: z + .array(z.string()) + .optional() + .describe('Enable feature flags'), + disableFeatures: z + .array(z.string()) + .optional() + .describe('Disable feature flags'), + workingDir: z + .string() + .optional() + .describe('Working directory (must be a git repository)'), + timeout: z + .number() + .optional() + .describe('Maximum execution time in milliseconds'), + debug: z + .boolean() + .optional() + .describe('Return raw output for debugging'), +}); + +export const reviewCodexTool: UnifiedTool = { + name: 'review', + description: + 'Run Codex code review on git changes. Supports reviewing uncommitted changes, specific commits, or branch comparisons.', + zodSchema: reviewCodexArgsSchema, + prompt: { + description: 'Run code review with Codex CLI', + }, + category: 'utility', + execute: async (args, onProgress) => { + const { + uncommitted, + commit, + base, + title, + prompt, + config, + enableFeatures, + disableFeatures, + workingDir, + timeout, + debug, + } = args; + + // Validate: at least one review target must be specified + if (!uncommitted && !commit && !base) { + return `❌ **Review Target Required** + +Please specify what to review: +- \`uncommitted: true\` - Review staged, unstaged, and untracked changes +- \`commit: ""\` - Review a specific commit +- \`base: ""\` - Review changes against a base branch + +**Examples:** +\`\`\`json +{ "uncommitted": true } +{ "commit": "abc1234" } +{ "base": "main" } +\`\`\``; + } + + const cmdArgs: string[] = ['review']; + const cwd = (workingDir as string) || process.cwd(); + + // Review target options + if (uncommitted) { + cmdArgs.push('--uncommitted'); + } + if (commit) { + cmdArgs.push('--commit', commit as string); + } + if (base) { + cmdArgs.push('--base', base as string); + } + + // Optional title + if (title) { + cmdArgs.push('--title', title as string); + } + + // Config override + if (config && typeof config === 'string') { + cmdArgs.push('-c', config); + } + + // Feature flags + if (enableFeatures && Array.isArray(enableFeatures)) { + for (const feature of enableFeatures) { + cmdArgs.push('--enable', feature); + } + } + if (disableFeatures && Array.isArray(disableFeatures)) { + for (const feature of disableFeatures) { + cmdArgs.push('--disable', feature); + } + } + + // Custom prompt (must be last) + if (prompt) { + cmdArgs.push(prompt); + } + + try { + const timeoutMs = timeout || 600000; // 10 minutes default + + Logger.debug(`Executing codex review in ${cwd}`); + Logger.debug(`Args: ${cmdArgs.join(' ')}`); + + const result = await executeCommandDetailed(CLI.COMMANDS.CODEX, cmdArgs, { + onProgress, + timeoutMs, + cwd, + }); + + // Debug mode: return raw output + if (debug) { + return `**Debug Output:** + +ok: ${result.ok} +code: ${result.code} +stdout length: ${result.stdout?.length || 0} +stderr: ${result.stderr || '(empty)'} + +Raw stdout: +${result.stdout || '(empty)'}`; + } + + // Codex outputs to stderr, check both stdout and stderr + const output = result.stdout || result.stderr || ''; + + if (output) { + return formatReviewOutput(output); + } + + // Handle errors + if (!result.ok) { + const errorMessage = result.stderr || `Exit code: ${result.code}`; + return `❌ **Review Failed** + +${errorMessage} + +**Debug Info:** +- Working directory: ${cwd} +- Command: codex ${cmdArgs.join(' ')}`; + } + + return `ℹ️ **Review Complete** + +No issues found or no output generated. + +**Debug Info:** +- Working directory: ${cwd} +- Command: codex ${cmdArgs.join(' ')}`; + } catch (error) { + Logger.error('Codex review failed:', error); + const errorMessage = error instanceof Error ? error.message : String(error); + return `❌ **Review Error**: ${errorMessage}`; + } + }, +}; + +function formatReviewOutput(output: string): string { + if (!output || output.trim() === '') { + return 'ℹ️ **Review Complete** - No issues found.'; + } + + // Extract the main review content from codex output + const lines = output.split('\n'); + const reviewContent: string[] = []; + let inThinking = false; + let thinkingContent: string[] = []; + + for (const line of lines) { + // Skip metadata lines + if ( + line.startsWith('OpenAI Codex') || + line.startsWith('--------') || + line.startsWith('workdir:') || + line.startsWith('model:') || + line.startsWith('provider:') || + line.startsWith('approval:') || + line.startsWith('sandbox:') || + line.startsWith('reasoning') || + line.startsWith('session id:') || + line.startsWith('user') || + line.startsWith('mcp startup:') + ) { + continue; + } + + // Track thinking blocks + if (line.startsWith('thinking')) { + inThinking = true; + const thinkingText = line.replace(/^thinking\s*/, '').trim(); + if (thinkingText) { + thinkingContent.push(thinkingText); + } + continue; + } + + // Track exec blocks (command execution) + if (line.startsWith('exec')) { + inThinking = false; + continue; + } + + // Track codex output (final review) + if (line.startsWith('codex')) { + inThinking = false; + const codexText = line.replace(/^codex\s*/, '').trim(); + if (codexText) { + reviewContent.push(codexText); + } + continue; + } + + // Collect content + if (inThinking) { + thinkingContent.push(line); + } else if (line.trim()) { + reviewContent.push(line); + } + } + + // Build formatted output + let formatted = '## 📋 Code Review Results\n\n'; + + if (reviewContent.length > 0) { + formatted += reviewContent.join('\n'); + } else if (thinkingContent.length > 0) { + // Fallback to thinking content if no explicit review output + formatted += '### Analysis\n\n'; + formatted += thinkingContent.join('\n'); + } else { + formatted += 'No specific issues identified.'; + } + + return formatted; +} diff --git a/src/utils/commandExecutor.ts b/src/utils/commandExecutor.ts index 8151879..858fb7c 100644 --- a/src/utils/commandExecutor.ts +++ b/src/utils/commandExecutor.ts @@ -22,6 +22,7 @@ export interface ExecuteOptions { timeoutMs?: number; maxOutputBytes?: number; retry?: RetryOptions; + cwd?: string; } /** @@ -37,6 +38,7 @@ export async function executeCommandDetailed( timeoutMs = 600000, maxOutputBytes = 50 * 1024 * 1024, // 50MB default retry, + cwd, } = options; let attempt = 0; @@ -48,6 +50,7 @@ export async function executeCommandDetailed( onProgress, timeoutMs, maxOutputBytes, + cwd, }); if (result.ok) { @@ -77,7 +80,7 @@ export async function executeCommandDetailed( async function executeOnce( command: string, args: string[], - { onProgress, timeoutMs, maxOutputBytes }: Omit + { onProgress, timeoutMs, maxOutputBytes, cwd }: Omit ): Promise { return new Promise(resolve => { const startTime = Date.now(); @@ -85,6 +88,7 @@ async function executeOnce( const childProcess = spawn(command, args, { env: process.env, + cwd: cwd || process.cwd(), // cross-spawn automatically handles shell mode and .cmd extensions on Windows stdio: ['ignore', 'pipe', 'pipe'], });