cc-reviewer 1.0.0

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.
@@ -0,0 +1,20 @@
1
+ /**
2
+ * CLI Availability Checker
3
+ */
4
+ import { CliStatus, CliType } from '../types.js';
5
+ /**
6
+ * Check availability of all supported CLIs
7
+ */
8
+ export declare function checkCliAvailability(): Promise<CliStatus>;
9
+ /**
10
+ * Check if a specific CLI is available
11
+ */
12
+ export declare function isCliAvailable(cli: CliType): Promise<boolean>;
13
+ /**
14
+ * Get CLI version (for debugging)
15
+ */
16
+ export declare function getCliVersion(cli: CliType): Promise<string | null>;
17
+ /**
18
+ * Log CLI availability status (for startup debugging)
19
+ */
20
+ export declare function logCliStatus(): Promise<void>;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * CLI Availability Checker
3
+ */
4
+ import { spawn } from 'child_process';
5
+ /**
6
+ * Check if a command exists on the system
7
+ */
8
+ async function commandExists(command) {
9
+ return new Promise((resolve) => {
10
+ const proc = spawn('which', [command], {
11
+ stdio: ['ignore', 'pipe', 'ignore']
12
+ });
13
+ proc.on('close', (code) => {
14
+ resolve(code === 0);
15
+ });
16
+ proc.on('error', () => {
17
+ resolve(false);
18
+ });
19
+ });
20
+ }
21
+ /**
22
+ * Check availability of all supported CLIs
23
+ */
24
+ export async function checkCliAvailability() {
25
+ const [codex, gemini] = await Promise.all([
26
+ commandExists('codex'),
27
+ commandExists('gemini')
28
+ ]);
29
+ return { codex, gemini };
30
+ }
31
+ /**
32
+ * Check if a specific CLI is available
33
+ */
34
+ export async function isCliAvailable(cli) {
35
+ return commandExists(cli);
36
+ }
37
+ /**
38
+ * Get CLI version (for debugging)
39
+ */
40
+ export async function getCliVersion(cli) {
41
+ return new Promise((resolve) => {
42
+ const proc = spawn(cli, ['--version'], {
43
+ stdio: ['ignore', 'pipe', 'pipe']
44
+ });
45
+ let stdout = '';
46
+ proc.stdout.on('data', (data) => {
47
+ stdout += data.toString();
48
+ });
49
+ proc.on('close', (code) => {
50
+ if (code === 0 && stdout) {
51
+ resolve(stdout.trim().split('\n')[0]);
52
+ }
53
+ else {
54
+ resolve(null);
55
+ }
56
+ });
57
+ proc.on('error', () => {
58
+ resolve(null);
59
+ });
60
+ });
61
+ }
62
+ /**
63
+ * Log CLI availability status (for startup debugging)
64
+ */
65
+ export async function logCliStatus() {
66
+ const status = await checkCliAvailability();
67
+ console.error('AI Reviewer CLI Status:');
68
+ console.error(` - Codex: ${status.codex ? '✓ Available' : '✗ Not found'}`);
69
+ console.error(` - Gemini: ${status.gemini ? '✓ Available' : '✗ Not found'}`);
70
+ if (!status.codex && !status.gemini) {
71
+ console.error('\nWarning: No AI CLIs found. Install with:');
72
+ console.error(' npm install -g @openai/codex-cli');
73
+ console.error(' npm install -g @google/gemini-cli');
74
+ }
75
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Codex CLI Wrapper
3
+ *
4
+ * Uses OpenAI's Codex CLI in non-interactive mode (codex exec)
5
+ * Reference: https://developers.openai.com/codex/cli/reference/
6
+ */
7
+ import { FeedbackRequest, FeedbackResult } from '../types.js';
8
+ /**
9
+ * Run Codex CLI with the given request
10
+ */
11
+ export declare function runCodexReview(request: FeedbackRequest): Promise<FeedbackResult>;
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Codex CLI Wrapper
3
+ *
4
+ * Uses OpenAI's Codex CLI in non-interactive mode (codex exec)
5
+ * Reference: https://developers.openai.com/codex/cli/reference/
6
+ */
7
+ import { spawn } from 'child_process';
8
+ import { existsSync } from 'fs';
9
+ import { build7SectionPrompt, buildDeveloperInstructions, buildRetryPrompt, isValidFeedbackOutput } from '../prompt.js';
10
+ import { createTimeoutError, createCliNotFoundError, getSuggestion } from '../errors.js';
11
+ const TIMEOUT_MS = 180000; // 3 minutes (Codex can be slow)
12
+ const MAX_RETRIES = 2;
13
+ const MAX_BUFFER_SIZE = 1024 * 1024; // 1MB max buffer to prevent memory issues
14
+ /**
15
+ * Run Codex CLI with the given request
16
+ */
17
+ export async function runCodexReview(request) {
18
+ // Validate workingDir exists before running
19
+ if (!existsSync(request.workingDir)) {
20
+ return {
21
+ success: false,
22
+ error: {
23
+ type: 'cli_error',
24
+ cli: 'codex',
25
+ exitCode: -1,
26
+ stderr: `Working directory does not exist: ${request.workingDir}`
27
+ },
28
+ suggestion: 'Check that the working directory path is correct',
29
+ model: 'codex'
30
+ };
31
+ }
32
+ return runWithRetry(request, 0);
33
+ }
34
+ /**
35
+ * Run Codex with retry logic
36
+ */
37
+ async function runWithRetry(request, attempt, previousError, previousOutput) {
38
+ try {
39
+ // Build the prompt (use retry prompt if this is a retry)
40
+ const basePrompt = attempt === 0
41
+ ? build7SectionPrompt(request)
42
+ : buildRetryPrompt(request, attempt + 1, previousError, previousOutput);
43
+ const developerInstructions = buildDeveloperInstructions('codex');
44
+ // Combine developer instructions with the prompt for Codex
45
+ // Codex exec doesn't have a separate system instruction flag
46
+ const fullPrompt = `${developerInstructions}\n\n---\n\n${basePrompt}`;
47
+ // Run the CLI
48
+ const result = await runCodexCli(fullPrompt, request.workingDir);
49
+ // Check for CLI errors
50
+ if (result.exitCode !== 0) {
51
+ // Check for specific error patterns in stderr
52
+ if (result.stderr.toLowerCase().includes('rate limit')) {
53
+ return {
54
+ success: false,
55
+ error: {
56
+ type: 'rate_limit',
57
+ cli: 'codex',
58
+ retryAfterMs: parseRetryAfter(result.stderr)
59
+ },
60
+ suggestion: 'Wait and retry, or use /gemini-review instead',
61
+ model: 'codex'
62
+ };
63
+ }
64
+ if (result.stderr.toLowerCase().includes('unauthorized') ||
65
+ result.stderr.toLowerCase().includes('authentication') ||
66
+ result.stderr.includes('401') ||
67
+ result.stderr.includes('403')) {
68
+ return {
69
+ success: false,
70
+ error: {
71
+ type: 'auth_error',
72
+ cli: 'codex',
73
+ message: result.stderr
74
+ },
75
+ suggestion: 'Run `codex login` to authenticate',
76
+ model: 'codex'
77
+ };
78
+ }
79
+ return {
80
+ success: false,
81
+ error: {
82
+ type: 'cli_error',
83
+ cli: 'codex',
84
+ exitCode: result.exitCode,
85
+ stderr: result.stderr
86
+ },
87
+ model: 'codex'
88
+ };
89
+ }
90
+ // Check for buffer truncation warning
91
+ if (result.truncated) {
92
+ return {
93
+ success: false,
94
+ error: {
95
+ type: 'cli_error',
96
+ cli: 'codex',
97
+ exitCode: 0,
98
+ stderr: 'Output exceeded maximum buffer size (1MB) and was truncated'
99
+ },
100
+ suggestion: 'Try reviewing a smaller scope with --focus',
101
+ model: 'codex'
102
+ };
103
+ }
104
+ // Validate the response structure
105
+ if (!isValidFeedbackOutput(result.stdout)) {
106
+ if (attempt < MAX_RETRIES) {
107
+ // Retry with history
108
+ return runWithRetry(request, attempt + 1, 'Output missing required sections (Agreements, Disagreements, Additions, Alternatives, Risk Assessment)', result.stdout);
109
+ }
110
+ // Max retries reached, return invalid response error
111
+ return {
112
+ success: false,
113
+ error: {
114
+ type: 'invalid_response',
115
+ cli: 'codex',
116
+ rawOutput: result.stdout
117
+ },
118
+ suggestion: getSuggestion({ type: 'invalid_response', cli: 'codex', rawOutput: result.stdout }),
119
+ model: 'codex'
120
+ };
121
+ }
122
+ return {
123
+ success: true,
124
+ feedback: result.stdout,
125
+ model: 'codex'
126
+ };
127
+ }
128
+ catch (error) {
129
+ const err = error;
130
+ // Handle CLI not found (ENOENT for the codex binary itself)
131
+ if (err.code === 'ENOENT') {
132
+ return {
133
+ success: false,
134
+ error: createCliNotFoundError('codex'),
135
+ suggestion: getSuggestion(createCliNotFoundError('codex')),
136
+ model: 'codex'
137
+ };
138
+ }
139
+ if (err.message === 'TIMEOUT') {
140
+ return {
141
+ success: false,
142
+ error: createTimeoutError('codex', TIMEOUT_MS),
143
+ suggestion: getSuggestion(createTimeoutError('codex', TIMEOUT_MS)),
144
+ model: 'codex'
145
+ };
146
+ }
147
+ // Generic error
148
+ return {
149
+ success: false,
150
+ error: {
151
+ type: 'cli_error',
152
+ cli: 'codex',
153
+ exitCode: -1,
154
+ stderr: err.message
155
+ },
156
+ model: 'codex'
157
+ };
158
+ }
159
+ }
160
+ /**
161
+ * Execute the Codex CLI in non-interactive mode
162
+ *
163
+ * Uses user's preferred flags:
164
+ * codex exec -m gpt-5.2-codex -c model_reasoning_effort="xhigh" \
165
+ * -c model_reasoning_summary_format=experimental --search \
166
+ * --dangerously-bypass-approvals-and-sandbox "<prompt>"
167
+ */
168
+ function runCodexCli(prompt, workingDir) {
169
+ return new Promise((resolve, reject) => {
170
+ // Build CLI arguments for non-interactive execution
171
+ // Uses: codex exec -m gpt-5.2-codex -c model_reasoning_effort="xhigh" ...
172
+ const args = [
173
+ 'exec',
174
+ '-m', 'gpt-5.2-codex',
175
+ '-c', 'model_reasoning_effort=xhigh',
176
+ '-c', 'model_reasoning_summary_format=experimental',
177
+ '--search',
178
+ '--dangerously-bypass-approvals-and-sandbox',
179
+ '-C', workingDir,
180
+ prompt
181
+ ];
182
+ const proc = spawn('codex', args, {
183
+ cwd: workingDir,
184
+ stdio: ['ignore', 'pipe', 'pipe'],
185
+ env: { ...process.env }
186
+ });
187
+ let stdout = '';
188
+ let stderr = '';
189
+ let truncated = false;
190
+ proc.stdout.on('data', (data) => {
191
+ if (stdout.length < MAX_BUFFER_SIZE) {
192
+ stdout += data.toString();
193
+ if (stdout.length > MAX_BUFFER_SIZE) {
194
+ stdout = stdout.slice(0, MAX_BUFFER_SIZE);
195
+ truncated = true;
196
+ }
197
+ }
198
+ });
199
+ proc.stderr.on('data', (data) => {
200
+ if (stderr.length < MAX_BUFFER_SIZE) {
201
+ stderr += data.toString();
202
+ if (stderr.length > MAX_BUFFER_SIZE) {
203
+ stderr = stderr.slice(0, MAX_BUFFER_SIZE);
204
+ }
205
+ }
206
+ });
207
+ // Timeout handling
208
+ const timeout = setTimeout(() => {
209
+ proc.kill('SIGTERM');
210
+ reject(new Error('TIMEOUT'));
211
+ }, TIMEOUT_MS);
212
+ proc.on('close', (code) => {
213
+ clearTimeout(timeout);
214
+ resolve({
215
+ stdout,
216
+ stderr,
217
+ exitCode: code ?? -1,
218
+ truncated
219
+ });
220
+ });
221
+ proc.on('error', (err) => {
222
+ clearTimeout(timeout);
223
+ reject(err);
224
+ });
225
+ });
226
+ }
227
+ /**
228
+ * Parse retry-after duration from error message
229
+ */
230
+ function parseRetryAfter(errorMessage) {
231
+ const match = errorMessage.match(/retry[- ]?after[:\s]+(\d+)/i);
232
+ if (match) {
233
+ return parseInt(match[1]) * 1000; // Convert to ms
234
+ }
235
+ return undefined;
236
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Gemini CLI Wrapper
3
+ *
4
+ * Uses Google's Gemini CLI in non-interactive mode (gemini -p)
5
+ * Reference: https://github.com/google-gemini/gemini-cli
6
+ * Package: @google/gemini-cli
7
+ */
8
+ import { FeedbackRequest, FeedbackResult } from '../types.js';
9
+ /**
10
+ * Run Gemini CLI with the given request
11
+ */
12
+ export declare function runGeminiReview(request: FeedbackRequest): Promise<FeedbackResult>;
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Gemini CLI Wrapper
3
+ *
4
+ * Uses Google's Gemini CLI in non-interactive mode (gemini -p)
5
+ * Reference: https://github.com/google-gemini/gemini-cli
6
+ * Package: @google/gemini-cli
7
+ */
8
+ import { spawn } from 'child_process';
9
+ import { existsSync } from 'fs';
10
+ import { build7SectionPrompt, buildDeveloperInstructions, buildRetryPrompt, isValidFeedbackOutput } from '../prompt.js';
11
+ import { createTimeoutError, createCliNotFoundError, getSuggestion } from '../errors.js';
12
+ const TIMEOUT_MS = 180000; // 3 minutes
13
+ const MAX_RETRIES = 2;
14
+ const MAX_BUFFER_SIZE = 1024 * 1024; // 1MB max buffer to prevent memory issues
15
+ /**
16
+ * Run Gemini CLI with the given request
17
+ */
18
+ export async function runGeminiReview(request) {
19
+ // Validate workingDir exists before running
20
+ if (!existsSync(request.workingDir)) {
21
+ return {
22
+ success: false,
23
+ error: {
24
+ type: 'cli_error',
25
+ cli: 'gemini',
26
+ exitCode: -1,
27
+ stderr: `Working directory does not exist: ${request.workingDir}`
28
+ },
29
+ suggestion: 'Check that the working directory path is correct',
30
+ model: 'gemini'
31
+ };
32
+ }
33
+ return runWithRetry(request, 0);
34
+ }
35
+ /**
36
+ * Run Gemini with retry logic
37
+ */
38
+ async function runWithRetry(request, attempt, previousError, previousOutput) {
39
+ try {
40
+ // Build the prompt (use retry prompt if this is a retry)
41
+ const basePrompt = attempt === 0
42
+ ? build7SectionPrompt(request)
43
+ : buildRetryPrompt(request, attempt + 1, previousError, previousOutput);
44
+ const developerInstructions = buildDeveloperInstructions('gemini');
45
+ // Combine developer instructions with the prompt
46
+ // Gemini CLI doesn't have a separate system instruction flag in non-interactive mode
47
+ const fullPrompt = `${developerInstructions}\n\n---\n\n${basePrompt}`;
48
+ // Run the CLI
49
+ const result = await runGeminiCli(fullPrompt, request.workingDir);
50
+ // Check for CLI errors
51
+ if (result.exitCode !== 0) {
52
+ // Check for specific error patterns in stderr
53
+ if (result.stderr.toLowerCase().includes('rate limit') ||
54
+ result.stderr.toLowerCase().includes('quota')) {
55
+ return {
56
+ success: false,
57
+ error: {
58
+ type: 'rate_limit',
59
+ cli: 'gemini',
60
+ retryAfterMs: parseRetryAfter(result.stderr)
61
+ },
62
+ suggestion: 'Wait and retry, or use /codex-review instead',
63
+ model: 'gemini'
64
+ };
65
+ }
66
+ if (result.stderr.toLowerCase().includes('unauthorized') ||
67
+ result.stderr.toLowerCase().includes('authentication') ||
68
+ result.stderr.toLowerCase().includes('api key') ||
69
+ result.stderr.includes('401') ||
70
+ result.stderr.includes('403')) {
71
+ return {
72
+ success: false,
73
+ error: {
74
+ type: 'auth_error',
75
+ cli: 'gemini',
76
+ message: result.stderr
77
+ },
78
+ suggestion: 'Run `gemini` and follow the authentication prompts, or set GEMINI_API_KEY',
79
+ model: 'gemini'
80
+ };
81
+ }
82
+ return {
83
+ success: false,
84
+ error: {
85
+ type: 'cli_error',
86
+ cli: 'gemini',
87
+ exitCode: result.exitCode,
88
+ stderr: result.stderr
89
+ },
90
+ model: 'gemini'
91
+ };
92
+ }
93
+ // Check for buffer truncation warning
94
+ if (result.truncated) {
95
+ return {
96
+ success: false,
97
+ error: {
98
+ type: 'cli_error',
99
+ cli: 'gemini',
100
+ exitCode: 0,
101
+ stderr: 'Output exceeded maximum buffer size (1MB) and was truncated'
102
+ },
103
+ suggestion: 'Try reviewing a smaller scope with --focus',
104
+ model: 'gemini'
105
+ };
106
+ }
107
+ // Validate the response structure
108
+ if (!isValidFeedbackOutput(result.stdout)) {
109
+ if (attempt < MAX_RETRIES) {
110
+ // Retry with history
111
+ return runWithRetry(request, attempt + 1, 'Output missing required sections (Agreements, Disagreements, Additions, Alternatives, Risk Assessment)', result.stdout);
112
+ }
113
+ // Max retries reached, return invalid response error
114
+ return {
115
+ success: false,
116
+ error: {
117
+ type: 'invalid_response',
118
+ cli: 'gemini',
119
+ rawOutput: result.stdout
120
+ },
121
+ suggestion: getSuggestion({ type: 'invalid_response', cli: 'gemini', rawOutput: result.stdout }),
122
+ model: 'gemini'
123
+ };
124
+ }
125
+ return {
126
+ success: true,
127
+ feedback: result.stdout,
128
+ model: 'gemini'
129
+ };
130
+ }
131
+ catch (error) {
132
+ const err = error;
133
+ // Handle CLI not found (ENOENT for the gemini binary itself)
134
+ if (err.code === 'ENOENT') {
135
+ return {
136
+ success: false,
137
+ error: createCliNotFoundError('gemini'),
138
+ suggestion: getSuggestion(createCliNotFoundError('gemini')),
139
+ model: 'gemini'
140
+ };
141
+ }
142
+ if (err.message === 'TIMEOUT') {
143
+ return {
144
+ success: false,
145
+ error: createTimeoutError('gemini', TIMEOUT_MS),
146
+ suggestion: getSuggestion(createTimeoutError('gemini', TIMEOUT_MS)),
147
+ model: 'gemini'
148
+ };
149
+ }
150
+ // Generic error
151
+ return {
152
+ success: false,
153
+ error: {
154
+ type: 'cli_error',
155
+ cli: 'gemini',
156
+ exitCode: -1,
157
+ stderr: err.message
158
+ },
159
+ model: 'gemini'
160
+ };
161
+ }
162
+ }
163
+ /**
164
+ * Execute the Gemini CLI in non-interactive mode
165
+ *
166
+ * Uses: gemini -p "<prompt>" --include-directories <workingDir>
167
+ */
168
+ function runGeminiCli(prompt, workingDir) {
169
+ return new Promise((resolve, reject) => {
170
+ // Build CLI arguments for non-interactive execution
171
+ const args = [
172
+ '-p', prompt,
173
+ '--include-directories', workingDir
174
+ ];
175
+ const proc = spawn('gemini', args, {
176
+ cwd: workingDir,
177
+ stdio: ['ignore', 'pipe', 'pipe'],
178
+ env: { ...process.env }
179
+ });
180
+ let stdout = '';
181
+ let stderr = '';
182
+ let truncated = false;
183
+ proc.stdout.on('data', (data) => {
184
+ if (stdout.length < MAX_BUFFER_SIZE) {
185
+ stdout += data.toString();
186
+ if (stdout.length > MAX_BUFFER_SIZE) {
187
+ stdout = stdout.slice(0, MAX_BUFFER_SIZE);
188
+ truncated = true;
189
+ }
190
+ }
191
+ });
192
+ proc.stderr.on('data', (data) => {
193
+ if (stderr.length < MAX_BUFFER_SIZE) {
194
+ stderr += data.toString();
195
+ if (stderr.length > MAX_BUFFER_SIZE) {
196
+ stderr = stderr.slice(0, MAX_BUFFER_SIZE);
197
+ }
198
+ }
199
+ });
200
+ // Timeout handling
201
+ const timeout = setTimeout(() => {
202
+ proc.kill('SIGTERM');
203
+ reject(new Error('TIMEOUT'));
204
+ }, TIMEOUT_MS);
205
+ proc.on('close', (code) => {
206
+ clearTimeout(timeout);
207
+ resolve({
208
+ stdout,
209
+ stderr,
210
+ exitCode: code ?? -1,
211
+ truncated
212
+ });
213
+ });
214
+ proc.on('error', (err) => {
215
+ clearTimeout(timeout);
216
+ reject(err);
217
+ });
218
+ });
219
+ }
220
+ /**
221
+ * Parse retry-after duration from error message
222
+ */
223
+ function parseRetryAfter(errorMessage) {
224
+ const match = errorMessage.match(/retry[- ]?after[:\s]+(\d+)/i);
225
+ if (match) {
226
+ return parseInt(match[1]) * 1000; // Convert to ms
227
+ }
228
+ return undefined;
229
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Error Handling for AI Reviewer MCP Server
3
+ */
4
+ import { FeedbackError, CliType } from './types.js';
5
+ /**
6
+ * Create a CLI not found error
7
+ */
8
+ export declare function createCliNotFoundError(cli: CliType): FeedbackError;
9
+ /**
10
+ * Create a timeout error
11
+ */
12
+ export declare function createTimeoutError(cli: CliType, durationMs: number): FeedbackError;
13
+ /**
14
+ * Create a rate limit error
15
+ */
16
+ export declare function createRateLimitError(cli: CliType, retryAfterMs?: number): FeedbackError;
17
+ /**
18
+ * Create an auth error
19
+ */
20
+ export declare function createAuthError(cli: CliType, message: string): FeedbackError;
21
+ /**
22
+ * Create an invalid response error
23
+ */
24
+ export declare function createInvalidResponseError(cli: CliType, rawOutput: string): FeedbackError;
25
+ /**
26
+ * Create a CLI crash error
27
+ */
28
+ export declare function createCliError(cli: CliType, exitCode: number, stderr: string): FeedbackError;
29
+ /**
30
+ * Format an error for user display
31
+ */
32
+ export declare function formatErrorForUser(error: FeedbackError): string;
33
+ /**
34
+ * Detect error type from CLI output and error messages
35
+ */
36
+ export declare function detectErrorType(cli: CliType, error: Error & {
37
+ code?: string;
38
+ }, stdout: string, stderr: string, exitCode: number | null): FeedbackError;
39
+ /**
40
+ * Parse retry-after from error response
41
+ */
42
+ export declare function parseRetryAfter(errorMessage: string): number | undefined;
43
+ /**
44
+ * Generate suggestion based on error type
45
+ */
46
+ export declare function getSuggestion(error: FeedbackError): string | undefined;