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.
- package/dist/cli/check.d.ts +20 -0
- package/dist/cli/check.js +75 -0
- package/dist/cli/codex.d.ts +11 -0
- package/dist/cli/codex.js +236 -0
- package/dist/cli/gemini.d.ts +12 -0
- package/dist/cli/gemini.js +229 -0
- package/dist/errors.d.ts +46 -0
- package/dist/errors.js +188 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +77 -0
- package/dist/prompt.d.ts +24 -0
- package/dist/prompt.js +134 -0
- package/dist/tools/feedback.d.ts +185 -0
- package/dist/tools/feedback.js +360 -0
- package/dist/types.d.ts +100 -0
- package/dist/types.js +26 -0
- package/package.json +43 -0
|
@@ -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
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -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;
|