Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2163ea45d2 | |||
| e4790ac77e | |||
| f642e58070 | |||
| d666f7e08b | |||
| 9e7989671a | |||
| a8578beacd | |||
| 823333a4f5 |
@@ -34,13 +34,6 @@ export const AVAILABLE_REVIEWERS: ReviewerOption[] = [
|
||||
description: 'Uses your Gemini CLI (Google account, no API key needed)',
|
||||
needsApiKey: false
|
||||
},
|
||||
{
|
||||
id: 'antigravity',
|
||||
name: 'Antigravity CLI',
|
||||
model: 'antigravity',
|
||||
description: 'Uses your Antigravity CLI (agy) — Google account, no API key needed',
|
||||
needsApiKey: false
|
||||
},
|
||||
{
|
||||
id: 'opencode-cli',
|
||||
name: 'OpenCode (via OpenRouter)',
|
||||
|
||||
@@ -1,194 +0,0 @@
|
||||
import { spawn } from 'child_process'
|
||||
import type { AIProvider, Message, CliProviderOptions } from './types.js'
|
||||
import { CliSessionHelper } from './session-helper.js'
|
||||
import { preparePromptForCli } from '../utils/prompt-file.js'
|
||||
import { withRetry } from '../utils/retry.js'
|
||||
|
||||
// Antigravity (`agy`) is Google's rebrand of the Gemini CLI. Compared to `gemini`:
|
||||
// - auto-approve is `--dangerously-skip-permissions` (was `-y`)
|
||||
// - print mode emits plain text on stdout (no `-o json` / `-o stream-json`)
|
||||
// - there is no `--model` flag; agy pins its own model, so cliModel is ignored
|
||||
//
|
||||
// Unlike gemini-cli, this provider is stateless: agy's `--conversation` resume
|
||||
// re-prints the *entire* assistant transcript on every turn, which would duplicate
|
||||
// prior responses. Instead each call runs a fresh conversation with the full
|
||||
// message history in the prompt (the same approach the API providers use), so each
|
||||
// run returns exactly one response. We therefore expose no session id.
|
||||
export class AntigravityCliProvider implements AIProvider {
|
||||
name = 'antigravity'
|
||||
private cwd: string
|
||||
private timeout: number // ms, 0 = no timeout
|
||||
private formatter = new CliSessionHelper()
|
||||
|
||||
constructor(_options?: CliProviderOptions) {
|
||||
// No API key needed (uses Google account). agy has no --model flag, so cliModel is ignored.
|
||||
this.cwd = process.cwd()
|
||||
this.timeout = 15 * 60 * 1000 // 15 minutes default
|
||||
}
|
||||
|
||||
setCwd(cwd: string) {
|
||||
this.cwd = cwd
|
||||
}
|
||||
|
||||
async chat(messages: Message[], systemPrompt?: string): Promise<string> {
|
||||
const prompt = this.formatter.buildPrompt(messages, systemPrompt)
|
||||
return withRetry(() => this.runAgy(prompt))
|
||||
}
|
||||
|
||||
async *chatStream(messages: Message[], systemPrompt?: string): AsyncGenerator<string, void, unknown> {
|
||||
const prompt = this.formatter.buildPrompt(messages, systemPrompt)
|
||||
yield* this.runAgyStream(prompt)
|
||||
}
|
||||
|
||||
private buildArgs(): string[] {
|
||||
const args = ['--dangerously-skip-permissions']
|
||||
if (this.timeout > 0) {
|
||||
args.push('--print-timeout', `${Math.max(1, Math.ceil(this.timeout / 60000))}m`)
|
||||
}
|
||||
// Prompt is read from stdin via the `-` sentinel
|
||||
args.push('--print', '-')
|
||||
return args
|
||||
}
|
||||
|
||||
private runAgy(prompt: string): Promise<string> {
|
||||
const { prompt: stdinPrompt, cleanup } = preparePromptForCli(prompt)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn('agy', this.buildArgs(), {
|
||||
cwd: this.cwd,
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
})
|
||||
|
||||
// Suppress EPIPE: if child exits early, close handler reports the real error
|
||||
child.stdin.on('error', () => {})
|
||||
child.stdin.write(stdinPrompt)
|
||||
child.stdin.end()
|
||||
|
||||
let output = ''
|
||||
let error = ''
|
||||
|
||||
child.stdout.on('data', (data) => {
|
||||
output += data.toString()
|
||||
})
|
||||
|
||||
child.stderr.on('data', (data) => {
|
||||
error += data.toString()
|
||||
})
|
||||
|
||||
child.on('close', (code) => {
|
||||
cleanup()
|
||||
if (code !== 0) {
|
||||
reject(new Error(`Antigravity CLI exited with code ${code}: ${error}`))
|
||||
} else {
|
||||
resolve(output.trim())
|
||||
}
|
||||
})
|
||||
|
||||
child.on('error', (err) => {
|
||||
cleanup()
|
||||
reject(new Error(`Failed to run agy CLI: ${err.message}`))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
private async *runAgyStream(prompt: string): AsyncGenerator<string, void, unknown> {
|
||||
const { prompt: stdinPrompt, cleanup } = preparePromptForCli(prompt)
|
||||
|
||||
const child = spawn('agy', this.buildArgs(), {
|
||||
cwd: this.cwd,
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
})
|
||||
|
||||
const chunks: string[] = []
|
||||
let resolveNext: ((value: { chunk: string | null }) => void) | null = null
|
||||
let done = false
|
||||
let error: Error | null = null
|
||||
let lastActivity = Date.now()
|
||||
let stderrBuf = ''
|
||||
|
||||
// Timeout checker - kill if no activity for too long
|
||||
const timeoutChecker = this.timeout > 0 ? setInterval(() => {
|
||||
if (Date.now() - lastActivity > this.timeout) {
|
||||
child.kill('SIGTERM')
|
||||
// Force kill if SIGTERM is ignored
|
||||
const forceKill = setTimeout(() => {
|
||||
try { child.kill('SIGKILL') } catch {}
|
||||
}, 5000)
|
||||
forceKill.unref()
|
||||
done = true
|
||||
const stderr = stderrBuf.trim()
|
||||
error = new Error(`Antigravity CLI timed out after ${this.timeout / 1000}s of inactivity${stderr ? ': ' + stderr.slice(-500) : ''}`)
|
||||
if (resolveNext) {
|
||||
resolveNext({ chunk: null })
|
||||
}
|
||||
}
|
||||
}, 10000) : null // Check every 10s
|
||||
|
||||
const pushChunk = (chunk: string) => {
|
||||
if (resolveNext) {
|
||||
resolveNext({ chunk })
|
||||
resolveNext = null
|
||||
} else {
|
||||
chunks.push(chunk)
|
||||
}
|
||||
}
|
||||
|
||||
// Print mode streams the plain-text response on stdout — yield chunks as-is
|
||||
child.stdout.on('data', (data) => {
|
||||
lastActivity = Date.now()
|
||||
pushChunk(data.toString())
|
||||
})
|
||||
|
||||
child.stderr.on('data', (data) => {
|
||||
lastActivity = Date.now() // Activity on stderr also counts
|
||||
stderrBuf += data.toString()
|
||||
if (stderrBuf.length > 10000) stderrBuf = stderrBuf.slice(-10000)
|
||||
})
|
||||
|
||||
child.on('close', (code) => {
|
||||
cleanup()
|
||||
if (timeoutChecker) clearInterval(timeoutChecker)
|
||||
done = true
|
||||
if (code !== 0 && !error) {
|
||||
const stderr = stderrBuf.trim()
|
||||
error = new Error(`Antigravity CLI exited with code ${code}${stderr ? ': ' + stderr.slice(-500) : ''}`)
|
||||
}
|
||||
if (resolveNext) {
|
||||
resolveNext({ chunk: null })
|
||||
}
|
||||
})
|
||||
|
||||
child.on('error', (err) => {
|
||||
cleanup()
|
||||
if (timeoutChecker) clearInterval(timeoutChecker)
|
||||
done = true
|
||||
error = new Error(`Failed to run agy CLI: ${err.message}`)
|
||||
if (resolveNext) {
|
||||
resolveNext({ chunk: null })
|
||||
}
|
||||
})
|
||||
|
||||
// Write prompt to stdin and close
|
||||
// Suppress EPIPE: if child exits early, close handler reports the real error
|
||||
child.stdin.on('error', () => {})
|
||||
child.stdin.write(stdinPrompt)
|
||||
child.stdin.end()
|
||||
|
||||
while (!done || chunks.length > 0) {
|
||||
if (chunks.length > 0) {
|
||||
yield chunks.shift()!
|
||||
} else if (!done) {
|
||||
const result = await new Promise<{ chunk: string | null }>((resolve) => {
|
||||
resolveNext = resolve
|
||||
})
|
||||
if (result.chunk !== null) {
|
||||
yield result.chunk
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import { OpenAIProvider } from './openai.js'
|
||||
import { ClaudeCodeProvider } from './claude-code.js'
|
||||
import { CodexCliProvider } from './codex-cli.js'
|
||||
import { GeminiCliProvider } from './gemini-cli.js'
|
||||
import { AntigravityCliProvider } from './antigravity.js'
|
||||
import { GeminiProvider } from './gemini.js'
|
||||
import { OpencodeCliProvider } from './opencode-cli.js'
|
||||
import { QwenCodeProvider } from './qwen-code.js'
|
||||
@@ -16,7 +15,7 @@ import { checkCliBinary } from './cli-check.js'
|
||||
|
||||
// Parse CLI model string: 'gemini-cli:gemini-2.5-pro' → { provider: 'gemini-cli', cliModel: 'gemini-2.5-pro' }
|
||||
// Plain 'gemini-cli' → { provider: 'gemini-cli', cliModel: undefined }
|
||||
const CLI_PROVIDERS = ['claude-code', 'codex-cli', 'gemini-cli', 'antigravity', 'opencode-cli', 'qwen-code'] as const
|
||||
const CLI_PROVIDERS = ['claude-code', 'codex-cli', 'gemini-cli', 'opencode-cli', 'qwen-code'] as const
|
||||
type CliProviderName = typeof CLI_PROVIDERS[number]
|
||||
|
||||
const OPENROUTER_PREFIX = 'openrouter/'
|
||||
@@ -48,7 +47,7 @@ export function isCliModel(model: string): boolean {
|
||||
return (CLI_PROVIDERS as readonly string[]).includes(provider)
|
||||
}
|
||||
|
||||
export function getProviderForModel(model: string): 'anthropic' | 'openai' | 'google' | 'claude-code' | 'codex-cli' | 'gemini-cli' | 'antigravity' | 'opencode-cli' | 'qwen-code' | 'minimax' | 'mock' | 'openrouter' {
|
||||
export function getProviderForModel(model: string): 'anthropic' | 'openai' | 'google' | 'claude-code' | 'codex-cli' | 'gemini-cli' | 'opencode-cli' | 'qwen-code' | 'minimax' | 'mock' | 'openrouter' {
|
||||
if (model.startsWith(OPENROUTER_PREFIX)) {
|
||||
return 'openrouter'
|
||||
}
|
||||
@@ -102,12 +101,6 @@ export function createProvider(model: string, config: MagpieConfig): AIProvider
|
||||
return new GeminiCliProvider({ cliModel })
|
||||
}
|
||||
|
||||
// Antigravity CLI (`agy`) doesn't need API key config (uses Google account)
|
||||
if (providerName === 'antigravity') {
|
||||
checkCliBinary('agy', 'Antigravity')
|
||||
return new AntigravityCliProvider({ cliModel })
|
||||
}
|
||||
|
||||
// Qwen Code CLI doesn't need API key config (uses OAuth)
|
||||
if (providerName === 'qwen-code') {
|
||||
checkCliBinary('qwen', 'Qwen Code')
|
||||
|
||||
@@ -6,7 +6,6 @@ import { homedir } from 'os'
|
||||
const PROVIDER_CONTEXT_MAP: Record<string, string[]> = {
|
||||
'claude-code': ['CLAUDE.md', 'AGENTS.md', 'GEMINI.md'],
|
||||
'gemini-cli': ['GEMINI.md', 'AGENTS.md', 'CLAUDE.md'],
|
||||
'antigravity': ['GEMINI.md', 'AGENTS.md', 'CLAUDE.md'],
|
||||
'codex-cli': ['AGENTS.md', 'CLAUDE.md', 'GEMINI.md'],
|
||||
}
|
||||
const DEFAULT_CONTEXT_FILES = ['CLAUDE.md', 'AGENTS.md', 'GEMINI.md']
|
||||
|
||||
@@ -44,10 +44,6 @@ describe('Provider Factory', () => {
|
||||
expect(getProviderForModel('codex-cli')).toBe('codex-cli')
|
||||
})
|
||||
|
||||
it('should return antigravity for antigravity model', () => {
|
||||
expect(getProviderForModel('antigravity')).toBe('antigravity')
|
||||
})
|
||||
|
||||
it('should return opencode-cli for opencode-cli model (with and without :model suffix)', () => {
|
||||
expect(getProviderForModel('opencode-cli')).toBe('opencode-cli')
|
||||
expect(getProviderForModel('opencode-cli:openrouter/anthropic/claude-sonnet-4')).toBe('opencode-cli')
|
||||
@@ -95,11 +91,6 @@ describe('Provider Factory', () => {
|
||||
expect(provider.name).toBe('codex-cli')
|
||||
})
|
||||
|
||||
it('should create antigravity provider', () => {
|
||||
const provider = createProvider('antigravity', mockConfig)
|
||||
expect(provider.name).toBe('antigravity')
|
||||
})
|
||||
|
||||
it('should create opencode-cli provider with no extra config', () => {
|
||||
const provider = createProvider('opencode-cli', mockConfig)
|
||||
expect(provider.name).toBe('opencode-cli')
|
||||
|
||||
Reference in New Issue
Block a user