cc-reviewer 2.0.0 → 3.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/README.md +15 -35
- package/commands/{codex-xhigh.md → codex-xhigh-review.md} +3 -3
- package/commands/{multi.md → multi-review.md} +1 -1
- package/dist/adapters/base.d.ts +3 -41
- package/dist/adapters/codex.d.ts +4 -6
- package/dist/adapters/codex.js +68 -322
- package/dist/adapters/gemini.d.ts +4 -6
- package/dist/adapters/gemini.js +46 -276
- package/dist/commands.d.ts +2 -3
- package/dist/commands.js +33 -11
- package/dist/decoders/codex.d.ts +11 -0
- package/dist/decoders/codex.js +29 -0
- package/dist/handoff.d.ts +2 -17
- package/dist/handoff.js +3 -87
- package/dist/index.d.ts +1 -1
- package/dist/index.js +17 -22
- package/dist/schema.d.ts +6 -195
- package/dist/schema.js +1 -190
- package/dist/tools/feedback.d.ts +5 -6
- package/dist/tools/feedback.js +63 -338
- package/dist/types.d.ts +0 -1
- package/package.json +2 -3
- package/commands/ask-codex.md +0 -36
- package/commands/ask-gemini.md +0 -35
- package/commands/ask-multi.md +0 -39
- package/dist/setup.d.ts +0 -7
- package/dist/setup.js +0 -27
- package/dist/tools/peer.d.ts +0 -194
- package/dist/tools/peer.js +0 -347
- /package/commands/{codex.md → codex-review.md} +0 -0
- /package/commands/{gemini.md → gemini-review.md} +0 -0
package/dist/adapters/gemini.js
CHANGED
|
@@ -2,22 +2,20 @@
|
|
|
2
2
|
* Gemini CLI Adapter
|
|
3
3
|
*
|
|
4
4
|
* Implements the ReviewerAdapter interface for Google's Gemini CLI.
|
|
5
|
-
*
|
|
5
|
+
* Returns raw text — no JSON parsing or schema enforcement.
|
|
6
|
+
* CC handles interpretation of the reviewer's response.
|
|
6
7
|
*/
|
|
7
8
|
import { spawn } from 'child_process';
|
|
8
9
|
import { existsSync } from 'fs';
|
|
9
10
|
import { registerAdapter, } from './base.js';
|
|
10
|
-
import { parseReviewOutput, parseLegacyMarkdownOutput, parsePeerOutput, isSubstantiveReview } from '../schema.js';
|
|
11
11
|
import { CliExecutor } from '../executor.js';
|
|
12
12
|
import { GeminiEventDecoder } from '../decoders/index.js';
|
|
13
|
-
import { buildSimpleHandoff, buildHandoffPrompt,
|
|
13
|
+
import { buildSimpleHandoff, buildHandoffPrompt, selectRole, } from '../handoff.js';
|
|
14
14
|
// =============================================================================
|
|
15
15
|
// CONFIGURATION
|
|
16
16
|
// =============================================================================
|
|
17
|
-
const
|
|
18
|
-
const STREAMING_TIMEOUT_MS = 90_000; // 90s — if events stop mid-stream
|
|
17
|
+
const INACTIVITY_TIMEOUT_MS = 300_000; // 5 min — covers reasoning gaps between tool use
|
|
19
18
|
const MAX_TIMEOUT_MS = 3_600_000; // 60 min absolute max
|
|
20
|
-
const MAX_RETRIES = 2;
|
|
21
19
|
const MAX_BUFFER_SIZE = 1024 * 1024; // 1MB max buffer
|
|
22
20
|
// =============================================================================
|
|
23
21
|
// GEMINI ADAPTER
|
|
@@ -31,9 +29,9 @@ export class GeminiAdapter {
|
|
|
31
29
|
strengths: ['architecture', 'maintainability', 'scalability', 'documentation'],
|
|
32
30
|
weaknesses: ['security'],
|
|
33
31
|
hasFilesystemAccess: true,
|
|
34
|
-
supportsStructuredOutput:
|
|
35
|
-
maxContextTokens: 2000000,
|
|
36
|
-
reasoningLevels: undefined,
|
|
32
|
+
supportsStructuredOutput: false,
|
|
33
|
+
maxContextTokens: 2000000,
|
|
34
|
+
reasoningLevels: undefined,
|
|
37
35
|
};
|
|
38
36
|
}
|
|
39
37
|
async isAvailable() {
|
|
@@ -41,273 +39,54 @@ export class GeminiAdapter {
|
|
|
41
39
|
const proc = spawn('gemini', ['--version'], {
|
|
42
40
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
43
41
|
});
|
|
44
|
-
proc.on('close', (code) =>
|
|
45
|
-
|
|
46
|
-
});
|
|
47
|
-
proc.on('error', () => {
|
|
48
|
-
resolve(false);
|
|
49
|
-
});
|
|
50
|
-
// Timeout after 5s
|
|
51
|
-
setTimeout(() => {
|
|
52
|
-
proc.kill();
|
|
53
|
-
resolve(false);
|
|
54
|
-
}, 5000);
|
|
42
|
+
proc.on('close', (code) => resolve(code === 0));
|
|
43
|
+
proc.on('error', () => resolve(false));
|
|
44
|
+
setTimeout(() => { proc.kill(); resolve(false); }, 5000);
|
|
55
45
|
});
|
|
56
46
|
}
|
|
57
47
|
async runReview(request) {
|
|
58
48
|
const startTime = Date.now();
|
|
59
|
-
// Validate working directory
|
|
60
49
|
if (!existsSync(request.workingDir)) {
|
|
61
50
|
return {
|
|
62
51
|
success: false,
|
|
63
|
-
error: {
|
|
64
|
-
type: 'cli_error',
|
|
65
|
-
message: `Working directory does not exist: ${request.workingDir}`,
|
|
66
|
-
},
|
|
52
|
+
error: { type: 'cli_error', message: `Working directory does not exist: ${request.workingDir}` },
|
|
67
53
|
suggestion: 'Check that the working directory path is correct',
|
|
68
54
|
executionTimeMs: Date.now() - startTime,
|
|
69
55
|
};
|
|
70
56
|
}
|
|
71
|
-
return this.runWithRetry(request, 0, startTime);
|
|
72
|
-
}
|
|
73
|
-
async runWithRetry(request, attempt, startTime, previousError, previousOutput) {
|
|
74
57
|
try {
|
|
75
|
-
// Build the prompt using handoff protocol
|
|
76
58
|
const handoff = buildSimpleHandoff(request.workingDir, request.ccOutput, request.analyzedFiles, request.focusAreas, request.customPrompt);
|
|
77
|
-
// Select role based on focus areas (Gemini defaults to architect)
|
|
78
59
|
const role = selectRole(request.focusAreas);
|
|
79
|
-
|
|
80
|
-
let prompt = buildHandoffPrompt({
|
|
81
|
-
handoff,
|
|
82
|
-
role,
|
|
83
|
-
outputFormat: 'json',
|
|
84
|
-
});
|
|
85
|
-
// Add retry context if this is a retry attempt
|
|
86
|
-
if (attempt > 0) {
|
|
87
|
-
prompt += `\n\n---\n\n# RETRY ATTEMPT ${attempt + 1}\n\n` +
|
|
88
|
-
`Previous output had issues: ${previousError}\n` +
|
|
89
|
-
`Please fix these issues and provide valid JSON output.\n` +
|
|
90
|
-
(previousOutput ? `\nPrevious output (for reference):\n${previousOutput.slice(0, 500)}...` : '');
|
|
91
|
-
}
|
|
92
|
-
// Run the CLI
|
|
93
|
-
const result = await this.runCli(prompt, request.workingDir);
|
|
94
|
-
// Handle CLI errors
|
|
95
|
-
if (result.exitCode !== 0) {
|
|
96
|
-
const error = this.categorizeError(result.stderr);
|
|
97
|
-
return {
|
|
98
|
-
success: false,
|
|
99
|
-
error,
|
|
100
|
-
suggestion: this.getSuggestion(error),
|
|
101
|
-
rawOutput: result.stderr,
|
|
102
|
-
executionTimeMs: Date.now() - startTime,
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
// Handle buffer truncation
|
|
106
|
-
if (result.truncated) {
|
|
107
|
-
return {
|
|
108
|
-
success: false,
|
|
109
|
-
error: {
|
|
110
|
-
type: 'cli_error',
|
|
111
|
-
message: 'Output exceeded maximum buffer size (1MB) and was truncated',
|
|
112
|
-
},
|
|
113
|
-
suggestion: 'Try reviewing a smaller scope with --focus',
|
|
114
|
-
executionTimeMs: Date.now() - startTime,
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
// Parse the output
|
|
118
|
-
let output = parseReviewOutput(result.stdout);
|
|
119
|
-
let usedFallback = false;
|
|
120
|
-
// If JSON parsing fails, try legacy markdown
|
|
121
|
-
if (!output) {
|
|
122
|
-
output = parseLegacyMarkdownOutput(result.stdout, 'gemini');
|
|
123
|
-
usedFallback = true;
|
|
124
|
-
}
|
|
125
|
-
// If no valid output, retry or fail
|
|
126
|
-
if (!output) {
|
|
127
|
-
if (attempt < MAX_RETRIES) {
|
|
128
|
-
return this.runWithRetry(request, attempt + 1, startTime, 'Output did not match expected JSON schema', result.stdout);
|
|
129
|
-
}
|
|
130
|
-
return {
|
|
131
|
-
success: false,
|
|
132
|
-
error: {
|
|
133
|
-
type: 'parse_error',
|
|
134
|
-
message: 'Failed to parse reviewer output after retries',
|
|
135
|
-
details: { rawOutput: result.stdout.slice(0, 1000) },
|
|
136
|
-
},
|
|
137
|
-
suggestion: 'The model may not be following the output format. Try a different focus area.',
|
|
138
|
-
rawOutput: result.stdout,
|
|
139
|
-
executionTimeMs: Date.now() - startTime,
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
// Check for empty/minimal output — centralized substance check
|
|
143
|
-
if (!isSubstantiveReview(output)) {
|
|
144
|
-
if (attempt < MAX_RETRIES) {
|
|
145
|
-
console.error(`[gemini] Received empty output, retrying...`);
|
|
146
|
-
return this.runWithRetry(request, attempt + 1, startTime, usedFallback
|
|
147
|
-
? 'Received markdown output instead of JSON. Please provide valid JSON output.'
|
|
148
|
-
: 'Output contained no substantive review content. Please provide findings or analysis.', result.stdout);
|
|
149
|
-
}
|
|
150
|
-
return {
|
|
151
|
-
success: false,
|
|
152
|
-
error: {
|
|
153
|
-
type: 'parse_error',
|
|
154
|
-
message: 'Reviewer returned empty output after retries',
|
|
155
|
-
details: { rawOutput: result.stdout.slice(0, 1000) },
|
|
156
|
-
},
|
|
157
|
-
suggestion: 'The model returned no substantive review. Try a different focus area.',
|
|
158
|
-
rawOutput: result.stdout,
|
|
159
|
-
executionTimeMs: Date.now() - startTime,
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
return {
|
|
163
|
-
success: true,
|
|
164
|
-
output,
|
|
165
|
-
rawOutput: result.stdout,
|
|
166
|
-
executionTimeMs: Date.now() - startTime,
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
catch (error) {
|
|
170
|
-
const err = error;
|
|
171
|
-
if (err.code === 'ENOENT') {
|
|
172
|
-
return {
|
|
173
|
-
success: false,
|
|
174
|
-
error: {
|
|
175
|
-
type: 'cli_not_found',
|
|
176
|
-
message: 'Gemini CLI not found',
|
|
177
|
-
},
|
|
178
|
-
suggestion: 'Install with: npm install -g @google/gemini-cli',
|
|
179
|
-
executionTimeMs: Date.now() - startTime,
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
if (err.message === 'TIMEOUT') {
|
|
183
|
-
return {
|
|
184
|
-
success: false,
|
|
185
|
-
error: {
|
|
186
|
-
type: 'timeout',
|
|
187
|
-
message: 'No output for 10 minutes - process may be hung',
|
|
188
|
-
},
|
|
189
|
-
suggestion: 'Try a smaller scope or use --focus',
|
|
190
|
-
executionTimeMs: Date.now() - startTime,
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
if (err.message === 'MAX_TIMEOUT') {
|
|
194
|
-
return {
|
|
195
|
-
success: false,
|
|
196
|
-
error: {
|
|
197
|
-
type: 'timeout',
|
|
198
|
-
message: 'Task exceeded 60 minute maximum',
|
|
199
|
-
},
|
|
200
|
-
suggestion: 'Try a smaller scope',
|
|
201
|
-
executionTimeMs: Date.now() - startTime,
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
return {
|
|
205
|
-
success: false,
|
|
206
|
-
error: {
|
|
207
|
-
type: 'cli_error',
|
|
208
|
-
message: err.message,
|
|
209
|
-
},
|
|
210
|
-
executionTimeMs: Date.now() - startTime,
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
async runPeerRequest(request) {
|
|
215
|
-
const startTime = Date.now();
|
|
216
|
-
if (!existsSync(request.workingDir)) {
|
|
217
|
-
return {
|
|
218
|
-
success: false,
|
|
219
|
-
error: { type: 'cli_error', message: `Working directory does not exist: ${request.workingDir}` },
|
|
220
|
-
suggestion: 'Check that the working directory path is correct',
|
|
221
|
-
executionTimeMs: Date.now() - startTime,
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
return this.runPeerWithRetry(request, 0, startTime);
|
|
225
|
-
}
|
|
226
|
-
async runPeerWithRetry(request, attempt, startTime, previousError, previousOutput) {
|
|
227
|
-
try {
|
|
228
|
-
let prompt = buildPeerPrompt({
|
|
229
|
-
workingDir: request.workingDir,
|
|
230
|
-
prompt: request.prompt,
|
|
231
|
-
taskType: request.taskType,
|
|
232
|
-
relevantFiles: request.relevantFiles,
|
|
233
|
-
context: request.context,
|
|
234
|
-
focusAreas: request.focusAreas,
|
|
235
|
-
customInstructions: request.customPrompt,
|
|
236
|
-
outputFormat: 'json',
|
|
237
|
-
});
|
|
238
|
-
if (attempt > 0) {
|
|
239
|
-
prompt += `\n\n---\n\n# RETRY ATTEMPT ${attempt + 1}\n\n` +
|
|
240
|
-
`Previous output had issues: ${previousError}\n` +
|
|
241
|
-
`Please fix these issues and provide valid JSON output.\n` +
|
|
242
|
-
(previousOutput ? `\nPrevious output (for reference):\n${previousOutput.slice(0, 500)}...` : '');
|
|
243
|
-
}
|
|
60
|
+
const prompt = buildHandoffPrompt({ handoff, role });
|
|
244
61
|
const result = await this.runCli(prompt, request.workingDir);
|
|
245
62
|
if (result.exitCode !== 0) {
|
|
246
63
|
const error = this.categorizeError(result.stderr);
|
|
247
|
-
return {
|
|
248
|
-
success: false, error,
|
|
249
|
-
suggestion: this.getSuggestion(error),
|
|
250
|
-
rawOutput: result.stderr,
|
|
251
|
-
executionTimeMs: Date.now() - startTime,
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
if (result.truncated) {
|
|
255
|
-
return {
|
|
256
|
-
success: false,
|
|
257
|
-
error: { type: 'cli_error', message: 'Output exceeded maximum buffer size (1MB)' },
|
|
258
|
-
suggestion: 'Try a more focused request',
|
|
259
|
-
executionTimeMs: Date.now() - startTime,
|
|
260
|
-
};
|
|
64
|
+
return { success: false, error, suggestion: this.getSuggestion(error), executionTimeMs: Date.now() - startTime };
|
|
261
65
|
}
|
|
262
|
-
|
|
263
|
-
if (!output) {
|
|
264
|
-
if (attempt < MAX_RETRIES) {
|
|
265
|
-
return this.runPeerWithRetry(request, attempt + 1, startTime, 'Output did not match expected JSON schema', result.stdout);
|
|
266
|
-
}
|
|
66
|
+
if (!result.stdout.trim()) {
|
|
267
67
|
return {
|
|
268
68
|
success: false,
|
|
269
|
-
error: { type: '
|
|
270
|
-
|
|
271
|
-
suggestion: 'The model may not be following the output format.',
|
|
272
|
-
rawOutput: result.stdout,
|
|
69
|
+
error: { type: 'cli_error', message: 'Gemini returned empty response' },
|
|
70
|
+
suggestion: 'Try again or use /codex-review instead',
|
|
273
71
|
executionTimeMs: Date.now() - startTime,
|
|
274
72
|
};
|
|
275
73
|
}
|
|
276
|
-
return {
|
|
277
|
-
success: true, output,
|
|
278
|
-
rawOutput: result.stdout,
|
|
279
|
-
executionTimeMs: Date.now() - startTime,
|
|
280
|
-
};
|
|
74
|
+
return { success: true, output: result.stdout, executionTimeMs: Date.now() - startTime };
|
|
281
75
|
}
|
|
282
76
|
catch (error) {
|
|
283
|
-
|
|
284
|
-
if (err.code === 'ENOENT') {
|
|
285
|
-
return { success: false, error: { type: 'cli_not_found', message: 'Gemini CLI not found' },
|
|
286
|
-
suggestion: 'Install with: npm install -g @google/gemini-cli', executionTimeMs: Date.now() - startTime };
|
|
287
|
-
}
|
|
288
|
-
if (err.message === 'TIMEOUT') {
|
|
289
|
-
return { success: false, error: { type: 'timeout', message: 'No output for 10 minutes' },
|
|
290
|
-
suggestion: 'Try a simpler request', executionTimeMs: Date.now() - startTime };
|
|
291
|
-
}
|
|
292
|
-
if (err.message === 'MAX_TIMEOUT') {
|
|
293
|
-
return { success: false, error: { type: 'timeout', message: 'Task exceeded 60 minute maximum' },
|
|
294
|
-
suggestion: 'Try a smaller scope', executionTimeMs: Date.now() - startTime };
|
|
295
|
-
}
|
|
296
|
-
return { success: false, error: { type: 'cli_error', message: err.message },
|
|
297
|
-
executionTimeMs: Date.now() - startTime };
|
|
77
|
+
return this.handleException(error, startTime);
|
|
298
78
|
}
|
|
299
79
|
}
|
|
300
80
|
async runCli(prompt, workingDir) {
|
|
301
81
|
const args = [
|
|
302
82
|
'--yolo',
|
|
303
|
-
'--output-format', 'stream-json',
|
|
83
|
+
'--output-format', 'stream-json',
|
|
304
84
|
'--include-directories', workingDir,
|
|
305
|
-
'-p', '',
|
|
85
|
+
'-p', '',
|
|
306
86
|
];
|
|
307
87
|
const decoder = new GeminiEventDecoder();
|
|
308
88
|
const cliStartTime = Date.now();
|
|
309
|
-
|
|
310
|
-
console.error('[gemini] Running review...');
|
|
89
|
+
console.error('[gemini] Running...');
|
|
311
90
|
decoder.onProgress = (eventType, detail) => {
|
|
312
91
|
const elapsed = Math.round((Date.now() - cliStartTime) / 1000);
|
|
313
92
|
const detailStr = detail ? ` — ${detail}` : '';
|
|
@@ -318,15 +97,11 @@ export class GeminiAdapter {
|
|
|
318
97
|
args,
|
|
319
98
|
cwd: workingDir,
|
|
320
99
|
stdin: prompt,
|
|
321
|
-
inactivityTimeoutMs:
|
|
100
|
+
inactivityTimeoutMs: INACTIVITY_TIMEOUT_MS,
|
|
322
101
|
maxTimeoutMs: MAX_TIMEOUT_MS,
|
|
323
102
|
maxBufferSize: MAX_BUFFER_SIZE,
|
|
324
103
|
onLine: (line) => {
|
|
325
104
|
decoder.processLine(line);
|
|
326
|
-
if (!firstEventReceived) {
|
|
327
|
-
firstEventReceived = true;
|
|
328
|
-
executor.setInactivityTimeout(STREAMING_TIMEOUT_MS);
|
|
329
|
-
}
|
|
330
105
|
},
|
|
331
106
|
});
|
|
332
107
|
const result = await executor.run();
|
|
@@ -340,46 +115,41 @@ export class GeminiAdapter {
|
|
|
340
115
|
truncated: result.truncated,
|
|
341
116
|
};
|
|
342
117
|
}
|
|
118
|
+
handleException(error, startTime) {
|
|
119
|
+
const err = error;
|
|
120
|
+
if (err.code === 'ENOENT') {
|
|
121
|
+
return { success: false, error: { type: 'cli_not_found', message: 'Gemini CLI not found' },
|
|
122
|
+
suggestion: 'Install with: npm install -g @google/gemini-cli', executionTimeMs: Date.now() - startTime };
|
|
123
|
+
}
|
|
124
|
+
if (err.message === 'TIMEOUT') {
|
|
125
|
+
return { success: false, error: { type: 'timeout', message: 'Gemini timed out — no events received' },
|
|
126
|
+
suggestion: 'Try a smaller scope or use /codex-review', executionTimeMs: Date.now() - startTime };
|
|
127
|
+
}
|
|
128
|
+
if (err.message === 'MAX_TIMEOUT') {
|
|
129
|
+
return { success: false, error: { type: 'timeout', message: 'Task exceeded 60 minute maximum' },
|
|
130
|
+
suggestion: 'Try a smaller scope', executionTimeMs: Date.now() - startTime };
|
|
131
|
+
}
|
|
132
|
+
return { success: false, error: { type: 'cli_error', message: err.message }, executionTimeMs: Date.now() - startTime };
|
|
133
|
+
}
|
|
343
134
|
categorizeError(stderr) {
|
|
344
135
|
const lower = stderr.toLowerCase();
|
|
345
136
|
if (lower.includes('rate limit') || lower.includes('quota')) {
|
|
346
|
-
return {
|
|
347
|
-
type: 'rate_limit',
|
|
348
|
-
message: 'Rate limit or quota exceeded',
|
|
349
|
-
details: { retryAfterMs: this.parseRetryAfter(stderr) },
|
|
350
|
-
};
|
|
137
|
+
return { type: 'rate_limit', message: 'Rate limit or quota exceeded' };
|
|
351
138
|
}
|
|
352
|
-
if (lower.includes('unauthorized') || lower.includes('authentication') ||
|
|
353
|
-
|
|
354
|
-
return {
|
|
355
|
-
type: 'auth_error',
|
|
356
|
-
message: 'Authentication failed',
|
|
357
|
-
details: { stderr },
|
|
358
|
-
};
|
|
139
|
+
if (lower.includes('unauthorized') || lower.includes('authentication') || lower.includes('api key') || stderr.includes('401') || stderr.includes('403')) {
|
|
140
|
+
return { type: 'auth_error', message: 'Authentication failed', details: { stderr } };
|
|
359
141
|
}
|
|
360
|
-
return {
|
|
361
|
-
type: 'cli_error',
|
|
362
|
-
message: stderr || 'Unknown error',
|
|
363
|
-
};
|
|
142
|
+
return { type: 'cli_error', message: stderr || 'Unknown error' };
|
|
364
143
|
}
|
|
365
144
|
getSuggestion(error) {
|
|
366
145
|
switch (error.type) {
|
|
367
|
-
case 'rate_limit':
|
|
368
|
-
|
|
369
|
-
case '
|
|
370
|
-
|
|
371
|
-
case 'cli_not_found':
|
|
372
|
-
return 'Install with: npm install -g @google/gemini-cli';
|
|
373
|
-
default:
|
|
374
|
-
return 'Check the error message and try again';
|
|
146
|
+
case 'rate_limit': return 'Wait and retry, or use /codex-review instead';
|
|
147
|
+
case 'auth_error': return 'Run `gemini` and follow auth prompts, or set GEMINI_API_KEY';
|
|
148
|
+
case 'cli_not_found': return 'Install with: npm install -g @google/gemini-cli';
|
|
149
|
+
default: return 'Check the error message and try again';
|
|
375
150
|
}
|
|
376
151
|
}
|
|
377
|
-
parseRetryAfter(errorMessage) {
|
|
378
|
-
const match = errorMessage.match(/retry[- ]?after[:\s]+(\d+)/i);
|
|
379
|
-
return match ? parseInt(match[1]) * 1000 : undefined;
|
|
380
|
-
}
|
|
381
152
|
}
|
|
382
153
|
// Register the adapter
|
|
383
154
|
registerAdapter(new GeminiAdapter());
|
|
384
|
-
// Export for direct use
|
|
385
155
|
export const geminiAdapter = new GeminiAdapter();
|
package/dist/commands.d.ts
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared module for slash command installation
|
|
3
3
|
*
|
|
4
|
-
* Used by
|
|
5
|
-
* - setup.ts (manual CLI tool: npx cc-reviewer-setup)
|
|
6
|
-
* - index.ts (auto-install on MCP server startup)
|
|
4
|
+
* Used by index.ts (auto-install on MCP server startup and `update` subcommand)
|
|
7
5
|
*/
|
|
8
6
|
export interface InstallResult {
|
|
9
7
|
success: boolean;
|
|
10
8
|
installed: string[];
|
|
9
|
+
removed: string[];
|
|
11
10
|
error?: string;
|
|
12
11
|
}
|
|
13
12
|
/**
|
package/dist/commands.js
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared module for slash command installation
|
|
3
3
|
*
|
|
4
|
-
* Used by
|
|
5
|
-
* - setup.ts (manual CLI tool: npx cc-reviewer-setup)
|
|
6
|
-
* - index.ts (auto-install on MCP server startup)
|
|
4
|
+
* Used by index.ts (auto-install on MCP server startup and `update` subcommand)
|
|
7
5
|
*/
|
|
8
|
-
import { existsSync, mkdirSync, copyFileSync, readdirSync, statSync } from 'fs';
|
|
6
|
+
import { existsSync, mkdirSync, copyFileSync, readdirSync, statSync, unlinkSync } from 'fs';
|
|
9
7
|
import { join, dirname } from 'path';
|
|
10
8
|
import { homedir } from 'os';
|
|
11
9
|
import { fileURLToPath } from 'url';
|
|
12
10
|
const __filename = fileURLToPath(import.meta.url);
|
|
13
11
|
const __dirname = dirname(__filename);
|
|
12
|
+
/** Old command filenames that should be pruned on upgrade */
|
|
13
|
+
const DEPRECATED_COMMANDS = [
|
|
14
|
+
'codex.md',
|
|
15
|
+
'gemini.md',
|
|
16
|
+
'multi.md',
|
|
17
|
+
'codex-xhigh.md',
|
|
18
|
+
'ask-codex.md',
|
|
19
|
+
'ask-gemini.md',
|
|
20
|
+
'ask-multi.md',
|
|
21
|
+
];
|
|
14
22
|
/**
|
|
15
23
|
* Get source and target paths for command files
|
|
16
24
|
*/
|
|
@@ -29,13 +37,13 @@ export function installCommands() {
|
|
|
29
37
|
const { source, target } = getCommandPaths();
|
|
30
38
|
// Check source exists
|
|
31
39
|
if (!existsSync(source)) {
|
|
32
|
-
return { success: false, installed: [], error: 'Commands directory not found' };
|
|
40
|
+
return { success: false, installed: [], removed: [], error: 'Commands directory not found' };
|
|
33
41
|
}
|
|
34
42
|
// Create target directory, handle errors (not a dir, permission denied)
|
|
35
43
|
try {
|
|
36
44
|
if (existsSync(target)) {
|
|
37
45
|
if (!statSync(target).isDirectory()) {
|
|
38
|
-
return { success: false, installed: [], error: `${target} exists but is not a directory` };
|
|
46
|
+
return { success: false, installed: [], removed: [], error: `${target} exists but is not a directory` };
|
|
39
47
|
}
|
|
40
48
|
}
|
|
41
49
|
else {
|
|
@@ -44,13 +52,27 @@ export function installCommands() {
|
|
|
44
52
|
}
|
|
45
53
|
catch (err) {
|
|
46
54
|
const msg = err instanceof Error ? err.message : String(err);
|
|
47
|
-
return { success: false, installed: [], error: `Cannot create target directory: ${msg}` };
|
|
55
|
+
return { success: false, installed: [], removed: [], error: `Cannot create target directory: ${msg}` };
|
|
48
56
|
}
|
|
49
57
|
const files = readdirSync(source).filter(f => f.endsWith('.md'));
|
|
50
58
|
if (files.length === 0) {
|
|
51
|
-
return { success: false, installed: [], error: 'No command files found' };
|
|
59
|
+
return { success: false, installed: [], removed: [], error: 'No command files found' };
|
|
52
60
|
}
|
|
53
|
-
//
|
|
61
|
+
// Prune deprecated commands from target
|
|
62
|
+
const removed = [];
|
|
63
|
+
for (const oldFile of DEPRECATED_COMMANDS) {
|
|
64
|
+
const oldPath = join(target, oldFile);
|
|
65
|
+
if (existsSync(oldPath)) {
|
|
66
|
+
try {
|
|
67
|
+
unlinkSync(oldPath);
|
|
68
|
+
removed.push(oldFile.replace('.md', ''));
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// Best-effort removal — don't fail the install
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Copy current files
|
|
54
76
|
const installed = [];
|
|
55
77
|
try {
|
|
56
78
|
for (const file of files) {
|
|
@@ -60,7 +82,7 @@ export function installCommands() {
|
|
|
60
82
|
}
|
|
61
83
|
catch (err) {
|
|
62
84
|
const msg = err instanceof Error ? err.message : String(err);
|
|
63
|
-
return { success: false, installed, error: `Copy failed: ${msg}` };
|
|
85
|
+
return { success: false, installed, removed, error: `Copy failed: ${msg}` };
|
|
64
86
|
}
|
|
65
|
-
return { success: true, installed };
|
|
87
|
+
return { success: true, installed, removed };
|
|
66
88
|
}
|
package/dist/decoders/codex.d.ts
CHANGED
|
@@ -42,6 +42,8 @@ export declare class CodexEventDecoder {
|
|
|
42
42
|
onProgress?: (eventType: string, detail?: string) => void;
|
|
43
43
|
private _finalResponse;
|
|
44
44
|
private _usage;
|
|
45
|
+
private _error;
|
|
46
|
+
private _eventCount;
|
|
45
47
|
/**
|
|
46
48
|
* Parse a single JSONL line. Silently skips malformed or empty input.
|
|
47
49
|
*/
|
|
@@ -56,5 +58,14 @@ export declare class CodexEventDecoder {
|
|
|
56
58
|
* `null` if no such event has been seen.
|
|
57
59
|
*/
|
|
58
60
|
getUsage(): CodexEvent['usage'] | null;
|
|
61
|
+
/**
|
|
62
|
+
* Returns the error message from `error` or `turn.failed` events, or `null`.
|
|
63
|
+
*/
|
|
64
|
+
getError(): string | null;
|
|
65
|
+
/**
|
|
66
|
+
* Returns true if events were received but no agent_message was produced.
|
|
67
|
+
* Combined with a fast exit, this indicates rate limiting or instant rejection.
|
|
68
|
+
*/
|
|
69
|
+
hasNoOutput(): boolean;
|
|
59
70
|
private _handleEvent;
|
|
60
71
|
}
|
package/dist/decoders/codex.js
CHANGED
|
@@ -25,6 +25,10 @@ export class CodexEventDecoder {
|
|
|
25
25
|
_finalResponse = null;
|
|
26
26
|
// Token usage from the most recently seen turn.completed
|
|
27
27
|
_usage = null;
|
|
28
|
+
// Error message from error/turn.failed events
|
|
29
|
+
_error = null;
|
|
30
|
+
// Count of events received (0 = possible rate limit / instant rejection)
|
|
31
|
+
_eventCount = 0;
|
|
28
32
|
// =============================================================================
|
|
29
33
|
// PUBLIC API
|
|
30
34
|
// =============================================================================
|
|
@@ -63,10 +67,24 @@ export class CodexEventDecoder {
|
|
|
63
67
|
getUsage() {
|
|
64
68
|
return this._usage;
|
|
65
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Returns the error message from `error` or `turn.failed` events, or `null`.
|
|
72
|
+
*/
|
|
73
|
+
getError() {
|
|
74
|
+
return this._error;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Returns true if events were received but no agent_message was produced.
|
|
78
|
+
* Combined with a fast exit, this indicates rate limiting or instant rejection.
|
|
79
|
+
*/
|
|
80
|
+
hasNoOutput() {
|
|
81
|
+
return this._eventCount > 0 && this._finalResponse === null;
|
|
82
|
+
}
|
|
66
83
|
// =============================================================================
|
|
67
84
|
// PRIVATE HELPERS
|
|
68
85
|
// =============================================================================
|
|
69
86
|
_handleEvent(event) {
|
|
87
|
+
this._eventCount++;
|
|
70
88
|
// Track the last agent_message text
|
|
71
89
|
if (event.type === 'item.completed' &&
|
|
72
90
|
event.item?.type === 'agent_message' &&
|
|
@@ -77,6 +95,17 @@ export class CodexEventDecoder {
|
|
|
77
95
|
if (event.type === 'turn.completed' && event.usage != null) {
|
|
78
96
|
this._usage = event.usage;
|
|
79
97
|
}
|
|
98
|
+
// Capture errors from error/turn.failed events
|
|
99
|
+
if (event.type === 'error') {
|
|
100
|
+
this._error = event.message || 'Unknown error from Codex';
|
|
101
|
+
}
|
|
102
|
+
if (event.type === 'turn.failed') {
|
|
103
|
+
this._error = event.error?.message || 'Turn failed';
|
|
104
|
+
}
|
|
105
|
+
// Capture error items (e.g. model errors reported as item.completed with type=error)
|
|
106
|
+
if (event.type === 'item.completed' && event.item?.type === 'error') {
|
|
107
|
+
this._error = event.item.message || event.item.text || 'Model error';
|
|
108
|
+
}
|
|
80
109
|
// Notify caller
|
|
81
110
|
this.onProgress?.(event.type, describeEvent(event));
|
|
82
111
|
}
|
package/dist/handoff.d.ts
CHANGED
|
@@ -214,10 +214,10 @@ export declare function selectRole(focusAreas?: FocusArea[]): ReviewerRole;
|
|
|
214
214
|
export interface PromptOptions {
|
|
215
215
|
handoff: Handoff;
|
|
216
216
|
role?: ReviewerRole;
|
|
217
|
-
outputFormat: 'json' | 'markdown' | 'schema-enforced';
|
|
218
217
|
}
|
|
219
218
|
/**
|
|
220
|
-
* Build the review prompt using minimal, targeted context
|
|
219
|
+
* Build the review prompt using minimal, targeted context.
|
|
220
|
+
* No output format constraints — reviewer responds naturally, CC interprets.
|
|
221
221
|
*/
|
|
222
222
|
export declare function buildHandoffPrompt(options: PromptOptions): string;
|
|
223
223
|
/**
|
|
@@ -229,18 +229,3 @@ export declare function buildSimpleHandoff(workingDir: string, ccOutput: string,
|
|
|
229
229
|
* CC should call this to add its specific concerns
|
|
230
230
|
*/
|
|
231
231
|
export declare function enhanceHandoff(handoff: Handoff, uncertainties?: Uncertainty[], questions?: Question[], decisions?: Decision[]): Handoff;
|
|
232
|
-
export interface PeerPromptOptions {
|
|
233
|
-
workingDir: string;
|
|
234
|
-
prompt: string;
|
|
235
|
-
taskType?: string;
|
|
236
|
-
relevantFiles?: string[];
|
|
237
|
-
context?: string;
|
|
238
|
-
focusAreas?: FocusArea[];
|
|
239
|
-
customInstructions?: string;
|
|
240
|
-
outputFormat: 'json' | 'schema-enforced';
|
|
241
|
-
}
|
|
242
|
-
/**
|
|
243
|
-
* Build a prompt for general-purpose peer assistance (not review).
|
|
244
|
-
* The peer acts as a collaborative coworker, not a critic.
|
|
245
|
-
*/
|
|
246
|
-
export declare function buildPeerPrompt(options: PeerPromptOptions): string;
|