@simonren/quorum 0.7.0 → 0.8.1

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 CHANGED
@@ -42,9 +42,9 @@ Install at least one AI CLI:
42
42
  npm install -g @openai/codex-cli
43
43
  codex login
44
44
 
45
- # Google Gemini CLI
46
- npm install -g @google/gemini-cli
47
- gemini # follow auth prompts
45
+ # Google Gemini — via Antigravity CLI (`agy`). `gemini-cli` is sunset on 2026-06-18.
46
+ curl -fsSL https://antigravity.google/cli/install.sh | bash
47
+ agy # follow the Google OAuth flow
48
48
  ```
49
49
 
50
50
  ## Usage
@@ -124,27 +124,30 @@ export class ClaudeAdapter {
124
124
  const result = await executor.run();
125
125
  const elapsed = Math.round((Date.now() - cliStartTime) / 1000);
126
126
  console.error(`[claude] ✓ complete (${elapsed}s)`);
127
- // Check for errors captured from stream events
127
+ // Prefer a completed response over a captured error: a non-fatal error event
128
+ // that precedes a successful agent_message must not discard the result (same
129
+ // class of bug fixed in the codex adapter). Only treat a decoder error as
130
+ // fatal when no final response was produced.
131
+ const finalResponse = decoder.getFinalResponse();
132
+ if (finalResponse) {
133
+ return {
134
+ stdout: finalResponse,
135
+ stderr: result.stderr,
136
+ exitCode: result.exitCode,
137
+ truncated: result.truncated,
138
+ };
139
+ }
128
140
  const decoderError = decoder.getError();
129
141
  if (decoderError) {
130
142
  const combined = result.stderr ? `${decoderError}\n\nCLI stderr: ${result.stderr}` : decoderError;
131
143
  return { stdout: '', stderr: combined, exitCode: 1, truncated: false };
132
144
  }
133
- const finalResponse = decoder.getFinalResponse();
134
- if (!finalResponse && decoder.hasNoOutput()) {
145
+ if (decoder.hasNoOutput()) {
135
146
  const combined = result.stderr ? `No output from Claude\n\nCLI stderr: ${result.stderr}` : 'No output from Claude';
136
147
  return { stdout: '', stderr: combined, exitCode: 1, truncated: false };
137
148
  }
138
- if (!finalResponse) {
139
- const combined = result.stderr ? `No result event from Claude\n\nCLI stderr: ${result.stderr}` : 'No result event from Claude';
140
- return { stdout: '', stderr: combined, exitCode: 1, truncated: false };
141
- }
142
- return {
143
- stdout: finalResponse,
144
- stderr: result.stderr,
145
- exitCode: result.exitCode,
146
- truncated: result.truncated,
147
- };
149
+ const combined = result.stderr ? `No result event from Claude\n\nCLI stderr: ${result.stderr}` : 'No result event from Claude';
150
+ return { stdout: '', stderr: combined, exitCode: 1, truncated: false };
148
151
  }
149
152
  handleException(error, startTime) {
150
153
  const err = error;
@@ -125,27 +125,45 @@ export class CodexAdapter {
125
125
  const result = await executor.run();
126
126
  const elapsed = Math.round((Date.now() - cliStartTime) / 1000);
127
127
  console.error(`[codex] ✓ complete (${elapsed}s)`);
128
- // Check for errors captured from JSONL events
128
+ // A genuine fatal error (a top-level `error` event or `turn.failed`) is
129
+ // TERMINAL and wins even when a — possibly intermediate/stale — agent_message
130
+ // exists. `--full-auto` runs a multi-turn loop, so a turn can emit a message
131
+ // and a later turn can fail; without this check, that failure would be
132
+ // reported as a successful review with stale content.
133
+ const fatalError = decoder.getFatalError();
134
+ if (fatalError) {
135
+ const combined = result.stderr ? `${fatalError}\n\nCLI stderr: ${result.stderr}` : fatalError;
136
+ return { stdout: '', stderr: combined, exitCode: 1, truncated: false };
137
+ }
138
+ // No fatal error: a completed response wins even if a NON-fatal error-item
139
+ // appeared earlier. Codex emits its "Skill descriptions were shortened to fit
140
+ // the 2% skills context budget" notice as an item.completed/type:error event —
141
+ // purely informational ("Codex can still see every skill"); the turn still
142
+ // completes and produces an agent_message. Use `!== null` (not truthiness) so
143
+ // an empty-string response is preserved for the caller's empty-response
144
+ // handling rather than misclassified as "no result event".
145
+ // (Checking getError() first discarded otherwise-successful reviews.)
146
+ const finalResponse = decoder.getFinalResponse();
147
+ if (finalResponse !== null) {
148
+ return {
149
+ stdout: finalResponse,
150
+ stderr: result.stderr,
151
+ exitCode: result.exitCode,
152
+ truncated: result.truncated,
153
+ };
154
+ }
155
+ // No response: surface a non-fatal item-level error if one was captured.
129
156
  const decoderError = decoder.getError();
130
157
  if (decoderError) {
131
158
  const combined = result.stderr ? `${decoderError}\n\nCLI stderr: ${result.stderr}` : decoderError;
132
159
  return { stdout: '', stderr: combined, exitCode: 1, truncated: false };
133
160
  }
134
- const finalResponse = decoder.getFinalResponse();
135
- if (!finalResponse && decoder.hasNoOutput()) {
161
+ if (decoder.hasNoOutput()) {
136
162
  const combined = result.stderr ? `No output from Codex\n\nCLI stderr: ${result.stderr}` : 'No output from Codex';
137
163
  return { stdout: '', stderr: combined, exitCode: 1, truncated: false };
138
164
  }
139
- if (!finalResponse) {
140
- const combined = result.stderr ? `No result event from Codex\n\nCLI stderr: ${result.stderr}` : 'No result event from Codex';
141
- return { stdout: '', stderr: combined, exitCode: 1, truncated: false };
142
- }
143
- return {
144
- stdout: finalResponse,
145
- stderr: result.stderr,
146
- exitCode: result.exitCode,
147
- truncated: result.truncated,
148
- };
165
+ const combined = result.stderr ? `No result event from Codex\n\nCLI stderr: ${result.stderr}` : 'No result event from Codex';
166
+ return { stdout: '', stderr: combined, exitCode: 1, truncated: false };
149
167
  }
150
168
  handleException(error, startTime) {
151
169
  const err = error;
@@ -1,9 +1,17 @@
1
1
  /**
2
- * Gemini CLI Adapter
2
+ * Gemini Adapter (via Antigravity CLI `agy`)
3
3
  *
4
- * Implements the ReviewerAdapter interface for Google's Gemini CLI.
5
- * Returns raw text no JSON parsing or schema enforcement.
6
- * CC handles interpretation of the reviewer's response.
4
+ * Google replaced `gemini-cli` with the Antigravity CLI (`agy`) at I/O 2026.
5
+ * The free-tier `gemini` binary stops serving requests on 2026-06-18, so this
6
+ * adapter now spawns `agy --print` with the prompt on stdin. The model is still
7
+ * Gemini under the hood — only the CLI brand changed — so the adapter id and
8
+ * config key remain `gemini`.
9
+ *
10
+ * Differences from the old gemini-cli adapter:
11
+ * - No `--output-format stream-json` → no live progress events
12
+ * - No `--model` flag → model selection is done in agy's settings file
13
+ * - `--include-directories` → `--add-dir`
14
+ * - `--approval-mode plan` → folded into `--sandbox`
7
15
  */
8
16
  import { ReviewerAdapter, ReviewerCapabilities, ReviewRequest, ReviewResult, ConsultRequest, ConsultResult } from './base.js';
9
17
  export declare class GeminiAdapter implements ReviewerAdapter {
@@ -1,27 +1,32 @@
1
1
  /**
2
- * Gemini CLI Adapter
2
+ * Gemini Adapter (via Antigravity CLI `agy`)
3
3
  *
4
- * Implements the ReviewerAdapter interface for Google's Gemini CLI.
5
- * Returns raw text no JSON parsing or schema enforcement.
6
- * CC handles interpretation of the reviewer's response.
4
+ * Google replaced `gemini-cli` with the Antigravity CLI (`agy`) at I/O 2026.
5
+ * The free-tier `gemini` binary stops serving requests on 2026-06-18, so this
6
+ * adapter now spawns `agy --print` with the prompt on stdin. The model is still
7
+ * Gemini under the hood — only the CLI brand changed — so the adapter id and
8
+ * config key remain `gemini`.
9
+ *
10
+ * Differences from the old gemini-cli adapter:
11
+ * - No `--output-format stream-json` → no live progress events
12
+ * - No `--model` flag → model selection is done in agy's settings file
13
+ * - `--include-directories` → `--add-dir`
14
+ * - `--approval-mode plan` → folded into `--sandbox`
7
15
  */
8
16
  import { spawn } from 'child_process';
9
17
  import { existsSync } from 'fs';
10
18
  import { registerAdapter, } from './base.js';
11
19
  import { CliExecutor } from '../executor.js';
12
- import { GeminiEventDecoder } from '../decoders/index.js';
13
20
  import { buildSimpleHandoff, buildHandoffPrompt, buildAdversarialHandoffPrompt, selectRole, } from '../handoff.js';
14
21
  import { buildConsultPrompt } from '../consult-prompt.js';
15
22
  import { getConfig } from '../config.js';
16
- // =============================================================================
17
- // GEMINI ADAPTER
18
- // =============================================================================
23
+ const AGY_INSTALL_CMD = 'curl -fsSL https://antigravity.google/cli/install.sh | bash';
19
24
  export class GeminiAdapter {
20
25
  id = 'gemini';
21
26
  getCapabilities() {
22
27
  return {
23
28
  name: 'Gemini',
24
- description: 'Google Gemini - excels at architecture analysis, design patterns, and large codebase understanding',
29
+ description: 'Google Gemini (via Antigravity CLI) - excels at architecture analysis, design patterns, and large codebase understanding',
25
30
  strengths: ['architecture', 'maintainability', 'scalability', 'documentation'],
26
31
  weaknesses: ['security'],
27
32
  hasFilesystemAccess: true,
@@ -38,7 +43,7 @@ export class GeminiAdapter {
38
43
  clearTimeout(timer);
39
44
  resolve(result);
40
45
  } };
41
- const proc = spawn('gemini', ['--version'], {
46
+ const proc = spawn('agy', ['--version'], {
42
47
  stdio: ['ignore', 'pipe', 'pipe'],
43
48
  });
44
49
  proc.on('close', (code) => done(code === 0));
@@ -69,7 +74,7 @@ export class GeminiAdapter {
69
74
  if (!result.stdout.trim()) {
70
75
  return {
71
76
  success: false,
72
- error: { type: 'cli_error', message: 'Gemini returned empty response' },
77
+ error: { type: 'cli_error', message: 'agy returned empty response' },
73
78
  suggestion: 'Try again or use /multi-review instead',
74
79
  executionTimeMs: Date.now() - startTime,
75
80
  };
@@ -82,45 +87,29 @@ export class GeminiAdapter {
82
87
  }
83
88
  async runCli(prompt, workingDir) {
84
89
  const cfg = getConfig().gemini;
90
+ // agy requires the prompt as the positional after --print; passing only via
91
+ // stdin makes it print --help and exit 0. Sandbox keeps terminal-restricted
92
+ // execution like the old gemini-cli's `--approval-mode plan`.
85
93
  const args = [
86
94
  '--sandbox',
87
- '--approval-mode', 'plan',
88
- '--output-format', 'stream-json',
89
- '--include-directories', workingDir,
90
- '-p', '',
95
+ '--add-dir', workingDir,
96
+ '--print', prompt,
91
97
  ];
92
- if (cfg.model) {
93
- args.push('--model', cfg.model);
94
- }
95
- const decoder = new GeminiEventDecoder();
96
98
  const cliStartTime = Date.now();
97
- console.error('[gemini] Running...');
98
- decoder.onProgress = (eventType, detail) => {
99
- const elapsed = Math.round((Date.now() - cliStartTime) / 1000);
100
- const detailStr = detail ? ` — ${detail}` : '';
101
- console.error(`[gemini] ${eventType}${detailStr} (${elapsed}s)`);
102
- };
99
+ console.error('[gemini] Running agy...');
103
100
  const executor = new CliExecutor({
104
- command: 'gemini',
101
+ command: 'agy',
105
102
  args,
106
103
  cwd: workingDir,
107
- stdin: prompt,
108
104
  inactivityTimeoutMs: cfg.inactivityTimeoutMs,
109
105
  maxTimeoutMs: cfg.maxTimeoutMs,
110
106
  maxBufferSize: cfg.maxBufferSize,
111
- onLine: (line) => {
112
- decoder.processLine(line);
113
- },
114
107
  });
115
108
  const result = await executor.run();
116
109
  const elapsed = Math.round((Date.now() - cliStartTime) / 1000);
117
110
  console.error(`[gemini] ✓ complete (${elapsed}s)`);
118
- const finalResponse = decoder.getFinalResponse();
119
- if (!finalResponse && result.exitCode === 0) {
120
- return { stdout: '', stderr: 'Gemini produced no output — review may have failed silently', exitCode: 1, truncated: false };
121
- }
122
111
  return {
123
- stdout: finalResponse || '',
112
+ stdout: result.rawStdout,
124
113
  stderr: result.stderr,
125
114
  exitCode: result.exitCode,
126
115
  truncated: result.truncated,
@@ -129,11 +118,11 @@ export class GeminiAdapter {
129
118
  handleException(error, startTime) {
130
119
  const err = error;
131
120
  if (err.code === 'ENOENT') {
132
- return { success: false, error: { type: 'cli_not_found', message: 'Gemini CLI not found' },
133
- suggestion: 'Install with: npm install -g @google/gemini-cli', executionTimeMs: Date.now() - startTime };
121
+ return { success: false, error: { type: 'cli_not_found', message: 'agy CLI not found' },
122
+ suggestion: `Install with: ${AGY_INSTALL_CMD}`, executionTimeMs: Date.now() - startTime };
134
123
  }
135
124
  if (err.message === 'TIMEOUT') {
136
- return { success: false, error: { type: 'timeout', message: 'Gemini timed out — no events received' },
125
+ return { success: false, error: { type: 'timeout', message: 'agy timed out — no output received' },
137
126
  suggestion: 'Try a smaller scope or use /multi-review', executionTimeMs: Date.now() - startTime };
138
127
  }
139
128
  if (err.message === 'MAX_TIMEOUT') {
@@ -147,7 +136,7 @@ export class GeminiAdapter {
147
136
  if (lower.includes('rate limit') || lower.includes('quota')) {
148
137
  return { type: 'rate_limit', message: `Rate limit or quota exceeded: ${stderr.slice(0, 500)}` };
149
138
  }
150
- if (lower.includes('unauthorized') || lower.includes('authentication') || lower.includes('api key') || stderr.includes('401') || stderr.includes('403')) {
139
+ if (lower.includes('unauthorized') || lower.includes('authentication') || lower.includes('oauth') || stderr.includes('401') || stderr.includes('403')) {
151
140
  return { type: 'auth_error', message: `Authentication failed: ${stderr.slice(0, 500)}`, details: { stderr } };
152
141
  }
153
142
  return { type: 'cli_error', message: stderr.slice(0, 500) || 'Unknown error' };
@@ -155,8 +144,8 @@ export class GeminiAdapter {
155
144
  getSuggestion(error) {
156
145
  switch (error.type) {
157
146
  case 'rate_limit': return 'Wait and retry, or use /multi-review instead';
158
- case 'auth_error': return 'Run `gemini` and follow auth prompts, or set GEMINI_API_KEY';
159
- case 'cli_not_found': return 'Install with: npm install -g @google/gemini-cli';
147
+ case 'auth_error': return 'Run `agy` and complete the Google OAuth sign-in';
148
+ case 'cli_not_found': return `Install with: ${AGY_INSTALL_CMD}`;
160
149
  default: return 'Check the error message and try again';
161
150
  }
162
151
  }
@@ -180,7 +169,7 @@ export class GeminiAdapter {
180
169
  if (!result.stdout.trim()) {
181
170
  return {
182
171
  success: false,
183
- error: { type: 'cli_error', message: 'Gemini returned empty response' },
172
+ error: { type: 'cli_error', message: 'agy returned empty response' },
184
173
  suggestion: 'Try again or use /multi-review instead',
185
174
  executionTimeMs: Date.now() - startTime,
186
175
  };
@@ -192,6 +181,5 @@ export class GeminiAdapter {
192
181
  }
193
182
  }
194
183
  }
195
- // Register the adapter
196
184
  registerAdapter(new GeminiAdapter());
197
185
  export const geminiAdapter = new GeminiAdapter();
package/dist/config.d.ts CHANGED
@@ -76,17 +76,14 @@ export declare const ClaudeConfigSchema: z.ZodDefault<z.ZodObject<{
76
76
  maxBufferSize?: number | undefined;
77
77
  }>>;
78
78
  export declare const GeminiConfigSchema: z.ZodDefault<z.ZodObject<{
79
- model: z.ZodDefault<z.ZodNullable<z.ZodString>>;
80
79
  inactivityTimeoutMs: z.ZodDefault<z.ZodNumber>;
81
80
  maxTimeoutMs: z.ZodDefault<z.ZodNumber>;
82
81
  maxBufferSize: z.ZodDefault<z.ZodNumber>;
83
82
  }, "strip", z.ZodTypeAny, {
84
- model: string | null;
85
83
  inactivityTimeoutMs: number;
86
84
  maxTimeoutMs: number;
87
85
  maxBufferSize: number;
88
86
  }, {
89
- model?: string | null | undefined;
90
87
  inactivityTimeoutMs?: number | undefined;
91
88
  maxTimeoutMs?: number | undefined;
92
89
  maxBufferSize?: number | undefined;
@@ -155,17 +152,14 @@ export declare const ConfigSchema: z.ZodDefault<z.ZodObject<{
155
152
  maxBufferSize?: number | undefined;
156
153
  }>>;
157
154
  gemini: z.ZodDefault<z.ZodObject<{
158
- model: z.ZodDefault<z.ZodNullable<z.ZodString>>;
159
155
  inactivityTimeoutMs: z.ZodDefault<z.ZodNumber>;
160
156
  maxTimeoutMs: z.ZodDefault<z.ZodNumber>;
161
157
  maxBufferSize: z.ZodDefault<z.ZodNumber>;
162
158
  }, "strip", z.ZodTypeAny, {
163
- model: string | null;
164
159
  inactivityTimeoutMs: number;
165
160
  maxTimeoutMs: number;
166
161
  maxBufferSize: number;
167
162
  }, {
168
- model?: string | null | undefined;
169
163
  inactivityTimeoutMs?: number | undefined;
170
164
  maxTimeoutMs?: number | undefined;
171
165
  maxBufferSize?: number | undefined;
@@ -191,7 +185,6 @@ export declare const ConfigSchema: z.ZodDefault<z.ZodObject<{
191
185
  maxBufferSize: number;
192
186
  };
193
187
  gemini: {
194
- model: string | null;
195
188
  inactivityTimeoutMs: number;
196
189
  maxTimeoutMs: number;
197
190
  maxBufferSize: number;
@@ -217,7 +210,6 @@ export declare const ConfigSchema: z.ZodDefault<z.ZodObject<{
217
210
  maxBufferSize?: number | undefined;
218
211
  } | undefined;
219
212
  gemini?: {
220
- model?: string | null | undefined;
221
213
  inactivityTimeoutMs?: number | undefined;
222
214
  maxTimeoutMs?: number | undefined;
223
215
  maxBufferSize?: number | undefined;
package/dist/config.js CHANGED
@@ -47,9 +47,11 @@ export const ClaudeConfigSchema = z
47
47
  maxBufferSize: z.number().int().positive().default(1_048_576),
48
48
  })
49
49
  .default({});
50
+ // agy (Antigravity CLI) does not expose `--model`; model selection lives in
51
+ // agy's own settings file. We keep the timeout/buffer knobs because CliExecutor
52
+ // enforces them regardless of the underlying CLI.
50
53
  export const GeminiConfigSchema = z
51
54
  .object({
52
- model: z.string().nullable().default('gemini-3.1-pro-preview'),
53
55
  inactivityTimeoutMs: z.number().int().positive().default(300_000),
54
56
  maxTimeoutMs: z.number().int().positive().default(3_600_000),
55
57
  maxBufferSize: z.number().int().positive().default(1_048_576),
@@ -43,6 +43,7 @@ export declare class CodexEventDecoder {
43
43
  private _finalResponse;
44
44
  private _usage;
45
45
  private _error;
46
+ private _fatalError;
46
47
  private _eventCount;
47
48
  /**
48
49
  * Parse a single JSONL line. Silently skips malformed or empty input.
@@ -59,9 +60,18 @@ export declare class CodexEventDecoder {
59
60
  */
60
61
  getUsage(): CodexEvent['usage'] | null;
61
62
  /**
62
- * Returns the error message from `error` or `turn.failed` events, or `null`.
63
+ * Returns the error message from any source a top-level `error`, a
64
+ * `turn.failed`, or a non-fatal item-level error notice — or `null`.
65
+ * Superset of `getFatalError()`.
63
66
  */
64
67
  getError(): string | null;
68
+ /**
69
+ * Returns the message from a TERMINAL fatal error only — a top-level `error`
70
+ * event or `turn.failed` — or `null`. The benign item-level error notice
71
+ * (e.g. the "skills shortened" budget message) is NOT fatal and is excluded,
72
+ * so a successful turn can win over it without masking a genuine failure.
73
+ */
74
+ getFatalError(): string | null;
65
75
  /**
66
76
  * Returns true if events were received but no agent_message was produced.
67
77
  * Combined with a fast exit, this indicates rate limiting or instant rejection.
@@ -25,8 +25,14 @@ 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
28
+ // Error message from ANY source (top-level error, turn.failed, or a non-fatal
29
+ // item-level error notice). Superset of _fatalError.
29
30
  _error = null;
31
+ // Error message from a TERMINAL fatal source ONLY: a top-level `error` event
32
+ // or `turn.failed`. Kept separate from the benign item-level error notice
33
+ // (e.g. Codex's "skills shortened" budget message) so the adapter can let a
34
+ // completed response override the benign notice WITHOUT masking a real failure.
35
+ _fatalError = null;
30
36
  // Count of events received (0 = possible rate limit / instant rejection)
31
37
  _eventCount = 0;
32
38
  // =============================================================================
@@ -68,11 +74,22 @@ export class CodexEventDecoder {
68
74
  return this._usage;
69
75
  }
70
76
  /**
71
- * Returns the error message from `error` or `turn.failed` events, or `null`.
77
+ * Returns the error message from any source a top-level `error`, a
78
+ * `turn.failed`, or a non-fatal item-level error notice — or `null`.
79
+ * Superset of `getFatalError()`.
72
80
  */
73
81
  getError() {
74
82
  return this._error;
75
83
  }
84
+ /**
85
+ * Returns the message from a TERMINAL fatal error only — a top-level `error`
86
+ * event or `turn.failed` — or `null`. The benign item-level error notice
87
+ * (e.g. the "skills shortened" budget message) is NOT fatal and is excluded,
88
+ * so a successful turn can win over it without masking a genuine failure.
89
+ */
90
+ getFatalError() {
91
+ return this._fatalError;
92
+ }
76
93
  /**
77
94
  * Returns true if events were received but no agent_message was produced.
78
95
  * Combined with a fast exit, this indicates rate limiting or instant rejection.
@@ -95,14 +112,19 @@ export class CodexEventDecoder {
95
112
  if (event.type === 'turn.completed' && event.usage != null) {
96
113
  this._usage = event.usage;
97
114
  }
98
- // Capture errors from error/turn.failed events
115
+ // Capture errors. A top-level `error` event and `turn.failed` are TERMINAL
116
+ // failures — record them as fatal (and in the general _error superset).
99
117
  if (event.type === 'error') {
100
118
  this._error = event.message || 'Unknown error from Codex';
119
+ this._fatalError = this._error;
101
120
  }
102
121
  if (event.type === 'turn.failed') {
103
122
  this._error = event.error?.message || 'Turn failed';
123
+ this._fatalError = this._error;
104
124
  }
105
- // Capture error items (e.g. model errors reported as item.completed with type=error)
125
+ // An item.completed/type:error is a NON-fatal notice (e.g. the "skills
126
+ // shortened" budget message, or a per-item model error). Record it only in
127
+ // the general _error bucket — never as fatal — so it cannot mask a response.
106
128
  if (event.type === 'item.completed' && event.item?.type === 'error') {
107
129
  this._error = event.item.message || event.item.text || 'Model error';
108
130
  }
@@ -1,6 +1,4 @@
1
1
  export { CodexEventDecoder } from './codex.js';
2
2
  export type { CodexEvent } from './codex.js';
3
- export { GeminiEventDecoder } from './gemini.js';
4
- export type { GeminiEvent } from './gemini.js';
5
3
  export { ClaudeEventDecoder } from './claude.js';
6
4
  export type { ClaudeEvent } from './claude.js';
@@ -1,3 +1,2 @@
1
1
  export { CodexEventDecoder } from './codex.js';
2
- export { GeminiEventDecoder } from './gemini.js';
3
2
  export { ClaudeEventDecoder } from './claude.js';
package/dist/errors.js CHANGED
@@ -3,22 +3,23 @@
3
3
  */
4
4
  // CLI installation commands
5
5
  // Codex: https://developers.openai.com/codex/cli/
6
- // Gemini: https://github.com/google-gemini/gemini-cli
6
+ // Gemini: now ships as `agy` (Antigravity CLI) — https://antigravity.google/download
7
7
  const INSTALL_COMMANDS = {
8
8
  codex: 'npm install -g @openai/codex-cli',
9
- gemini: 'npm install -g @google/gemini-cli',
9
+ gemini: 'curl -fsSL https://antigravity.google/cli/install.sh | bash',
10
10
  claude: 'https://docs.anthropic.com/en/docs/claude-code'
11
11
  };
12
- // Environment variables for API keys
12
+ // Environment variables for API keys.
13
+ // agy uses Google OAuth and has no env var — null surfaces "no key needed".
13
14
  const ENV_VARS = {
14
15
  codex: 'OPENAI_API_KEY',
15
- gemini: 'GEMINI_API_KEY',
16
+ gemini: null,
16
17
  claude: 'ANTHROPIC_API_KEY'
17
18
  };
18
19
  // Authentication commands
19
20
  const AUTH_COMMANDS = {
20
21
  codex: 'codex login',
21
- gemini: 'gemini (follow prompts)',
22
+ gemini: 'agy (complete Google OAuth)',
22
23
  claude: 'claude auth'
23
24
  };
24
25
  /**
@@ -110,13 +111,14 @@ This might happen with complex reviews. Try:
110
111
  ${retryMsg}
111
112
 
112
113
  Alternative: Use /${otherCli}-review instead`;
113
- case 'auth_error':
114
+ case 'auth_error': {
115
+ const envVar = ENV_VARS[error.cli];
116
+ const keyLine = envVar ? `\nCheck your API key: ${envVar}` : '';
114
117
  return `🔐 ${error.cli} authentication failed.
115
118
 
116
- ${error.message}
117
-
118
- Check your API key: ${ENV_VARS[error.cli]}
119
+ ${error.message}${keyLine}
119
120
  Run: ${AUTH_COMMANDS[error.cli]}`;
121
+ }
120
122
  case 'invalid_response':
121
123
  return `⚠️ ${error.cli} returned an unusable response.
122
124
 
@@ -180,8 +182,12 @@ export function getSuggestion(error) {
180
182
  return error.retryAfterMs
181
183
  ? `Wait ${Math.ceil(error.retryAfterMs / 1000)}s and retry`
182
184
  : 'Wait a moment and retry';
183
- case 'auth_error':
184
- return `Check your ${ENV_VARS[error.cli]} environment variable`;
185
+ case 'auth_error': {
186
+ const envVar = ENV_VARS[error.cli];
187
+ return envVar
188
+ ? `Check your ${envVar} environment variable`
189
+ : `Re-run \`${AUTH_COMMANDS[error.cli]}\` to refresh credentials`;
190
+ }
185
191
  case 'invalid_response':
186
192
  return 'Retry with a more specific focus area';
187
193
  case 'cli_error':
package/dist/index.js CHANGED
File without changes
@@ -163,7 +163,7 @@ export async function handleMultiConsult(input) {
163
163
  return {
164
164
  content: [{
165
165
  type: 'text',
166
- text: '❌ No AI CLIs found.\n\nInstall at least one:\n - Codex: npm install -g @openai/codex-cli\n - Gemini: npm install -g @google/gemini-cli',
166
+ text: '❌ No AI CLIs found.\n\nInstall at least one:\n - Codex: npm install -g @openai/codex-cli\n - Gemini (agy): curl -fsSL https://antigravity.google/cli/install.sh | bash',
167
167
  }],
168
168
  };
169
169
  }
@@ -57,7 +57,7 @@ export async function handleMultiReview(input) {
57
57
  const request = toReviewRequest(input);
58
58
  const availableAdapters = await getAvailableAdapters();
59
59
  if (availableAdapters.length === 0) {
60
- return { content: [{ type: 'text', text: '❌ No AI CLIs found.\n\nInstall at least one:\n - Codex: npm install -g @openai/codex-cli\n - Gemini: npm install -g @google/gemini-cli' }] };
60
+ return { content: [{ type: 'text', text: '❌ No AI CLIs found.\n\nInstall at least one:\n - Codex: npm install -g @openai/codex-cli\n - Gemini (agy): curl -fsSL https://antigravity.google/cli/install.sh | bash' }] };
61
61
  }
62
62
  // Spawn 2 reviews per adapter: standard + adversarial (all in parallel)
63
63
  // customPrompt steers the adversarial focus only — strip it from standard pass to avoid bias
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simonren/quorum",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
4
4
  "description": "MCP server for Claude Code — a quorum of AI models (Codex, Gemini, Claude) for adversarial review and consultation, synthesized by Claude Code",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",