Skip to content

Commit b6b8dea

Browse files
wesmclaude
andauthored
Add gemini-cli, copilot CLI support (#3)
* Add Gemini CLI and GitHub Copilot CLI agents - Add gemini agent using `gemini -p` for non-interactive reviews - Add copilot agent using `copilot --prompt` for non-interactive reviews - Update fallback order to include new agents - Update tests to verify all 5 agents are registered Install with: npm install -g @google/gemini-cli npm install -g @github/copilot 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Update README with expanded agent support - Add table showing all 4 agents with install commands - Document fallback order: codex → claude-code → gemini → copilot - Improve "Setting the Default Agent" section with all config options - Update config example to list all agent options 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Update error message to list all supported agents 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Update CLI help text to list all supported agents 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 7bea6f6 commit b6b8dea

File tree

6 files changed

+175
-48
lines changed

6 files changed

+175
-48
lines changed

README.md

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ The range is inclusive of both endpoints. This is useful for reviewing a feature
5959
Per-repository `.roborev.toml`:
6060

6161
```toml
62-
agent = "claude-code" # or "codex"
62+
agent = "claude-code" # codex, claude-code, gemini, or copilot
6363
review_context_count = 5
6464

6565
# Project-specific review guidelines (multi-line string)
@@ -109,37 +109,49 @@ The daemon starts automatically when needed and handles port conflicts by findin
109109

110110
roborev supports multiple AI review agents:
111111

112-
- `codex` - OpenAI Codex CLI
113-
- `claude-code` - Anthropic Claude Code CLI
112+
| Agent | CLI | Install |
113+
|-------|-----|---------|
114+
| `codex` | OpenAI Codex | `npm install -g @openai/codex` |
115+
| `claude-code` | Anthropic Claude Code | `npm install -g @anthropic-ai/claude-code` |
116+
| `gemini` | Google Gemini | `npm install -g @google/gemini-cli` |
117+
| `copilot` | GitHub Copilot | `npm install -g @github/copilot` |
114118

115119
### Automatic Fallback
116120

117-
roborev automatically detects which agents are installed and falls back gracefully:
121+
roborev automatically detects which agents are installed. If your preferred agent isn't available, it falls back in this order:
118122

119-
- If `codex` is requested but not installed, roborev uses `claude` instead
120-
- If `claude-code` is requested but not installed, roborev uses `codex` instead
121-
- If neither is installed, the job fails with a helpful error message
123+
```
124+
codex → claude-code → gemini → copilot
125+
```
122126

123-
### Explicit Agent Selection
127+
If none are installed, the job fails with a helpful error message.
124128

125-
To use a specific agent for a repository, create `.roborev.toml` in the repo root:
129+
### Setting the Default Agent
130+
131+
**Per-repository** - create `.roborev.toml` in the repo root:
126132

127133
```toml
128-
agent = "claude-code"
134+
agent = "gemini"
129135
```
130136

131-
Or set a global default in `~/.roborev/config.toml`:
137+
**Global default** - create or edit `~/.roborev/config.toml`:
132138

133139
```toml
134140
default_agent = "claude-code"
135141
```
136142

143+
**Per-command** - use the `--agent` flag:
144+
145+
```bash
146+
roborev enqueue HEAD --agent copilot
147+
```
148+
137149
### Selection Priority
138150

139151
1. `--agent` flag on enqueue command
140152
2. Per-repo `.roborev.toml`
141153
3. Global `~/.roborev/config.toml`
142-
4. Automatic detection (uses first available: codex, claude-code)
154+
4. Automatic detection (first available: codexclaude-code → gemini → copilot)
143155

144156
## Commands
145157

cmd/roborev/main.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ func main() {
3232
rootCmd := &cobra.Command{
3333
Use: "roborev",
3434
Short: "Automatic code review for git commits",
35-
Long: "RoboRev automatically reviews git commits using AI agents (Codex, Claude Code)",
35+
Long: "RoboRev automatically reviews git commits using AI agents (Codex, Claude Code, Gemini, Copilot)",
3636
}
3737

3838
rootCmd.PersistentFlags().StringVar(&serverAddr, "server", "http://127.0.0.1:7373", "daemon server address")
@@ -295,7 +295,7 @@ roborev enqueue --sha HEAD 2>/dev/null &
295295
},
296296
}
297297

298-
cmd.Flags().StringVar(&agent, "agent", "", "default agent (codex, claude-code)")
298+
cmd.Flags().StringVar(&agent, "agent", "", "default agent (codex, claude-code, gemini, copilot)")
299299

300300
return cmd
301301
}
@@ -418,7 +418,7 @@ Examples:
418418

419419
cmd.Flags().StringVar(&repoPath, "repo", "", "path to git repository (default: current directory)")
420420
cmd.Flags().StringVar(&sha, "sha", "HEAD", "commit SHA to review (used when no positional args)")
421-
cmd.Flags().StringVar(&agent, "agent", "", "agent to use (codex, claude-code)")
421+
cmd.Flags().StringVar(&agent, "agent", "", "agent to use (codex, claude-code, gemini, copilot)")
422422

423423
return cmd
424424
}

internal/agent/agent.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@ func GetAvailable(preferred string) (Agent, error) {
7373
return Get(preferred)
7474
}
7575

76-
// Fallback order: codex, claude-code
77-
fallbacks := []string{"codex", "claude-code"}
76+
// Fallback order: codex, claude-code, gemini, copilot
77+
fallbacks := []string{"codex", "claude-code", "gemini", "copilot"}
7878
for _, name := range fallbacks {
7979
if name != preferred && IsAvailable(name) {
8080
return Get(name)
@@ -90,7 +90,7 @@ func GetAvailable(preferred string) (Agent, error) {
9090
}
9191

9292
if len(available) == 0 {
93-
return nil, fmt.Errorf("no agents available (install codex or claude)")
93+
return nil, fmt.Errorf("no agents available (install one of: codex, claude-code, gemini, copilot)")
9494
}
9595

9696
return Get(available[0])

internal/agent/agent_test.go

Lines changed: 28 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,51 +5,49 @@ import (
55
)
66

77
func TestAgentRegistry(t *testing.T) {
8-
// Check that default agents are registered
9-
codex, err := Get("codex")
10-
if err != nil {
11-
t.Fatalf("Failed to get codex agent: %v", err)
12-
}
13-
if codex.Name() != "codex" {
14-
t.Errorf("Expected name 'codex', got '%s'", codex.Name())
15-
}
16-
17-
claude, err := Get("claude-code")
18-
if err != nil {
19-
t.Fatalf("Failed to get claude-code agent: %v", err)
20-
}
21-
if claude.Name() != "claude-code" {
22-
t.Errorf("Expected name 'claude-code', got '%s'", claude.Name())
8+
// Check that all agents are registered
9+
agents := []string{"codex", "claude-code", "gemini", "copilot", "test"}
10+
for _, name := range agents {
11+
a, err := Get(name)
12+
if err != nil {
13+
t.Fatalf("Failed to get %s agent: %v", name, err)
14+
}
15+
if a.Name() != name {
16+
t.Errorf("Expected name '%s', got '%s'", name, a.Name())
17+
}
2318
}
2419

2520
// Check unknown agent
26-
_, err = Get("unknown-agent")
21+
_, err := Get("unknown-agent")
2722
if err == nil {
2823
t.Error("Expected error for unknown agent")
2924
}
3025
}
3126

3227
func TestAvailableAgents(t *testing.T) {
3328
agents := Available()
34-
if len(agents) < 2 {
35-
t.Errorf("Expected at least 2 agents, got %d", len(agents))
29+
// We have 5 agents: codex, claude-code, gemini, copilot, test
30+
if len(agents) < 5 {
31+
t.Errorf("Expected at least 5 agents, got %d: %v", len(agents), agents)
32+
}
33+
34+
expected := map[string]bool{
35+
"codex": false,
36+
"claude-code": false,
37+
"gemini": false,
38+
"copilot": false,
39+
"test": false,
3640
}
3741

38-
hasCodex := false
39-
hasClaude := false
4042
for _, a := range agents {
41-
if a == "codex" {
42-
hasCodex = true
43-
}
44-
if a == "claude-code" {
45-
hasClaude = true
43+
if _, ok := expected[a]; ok {
44+
expected[a] = true
4645
}
4746
}
4847

49-
if !hasCodex {
50-
t.Error("Expected codex in available agents")
51-
}
52-
if !hasClaude {
53-
t.Error("Expected claude-code in available agents")
48+
for name, found := range expected {
49+
if !found {
50+
t.Errorf("Expected %s in available agents", name)
51+
}
5452
}
5553
}

internal/agent/copilot.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package agent
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"fmt"
7+
"os/exec"
8+
)
9+
10+
// CopilotAgent runs code reviews using the GitHub Copilot CLI
11+
type CopilotAgent struct {
12+
Command string // The copilot command to run (default: "copilot")
13+
}
14+
15+
// NewCopilotAgent creates a new Copilot agent
16+
func NewCopilotAgent(command string) *CopilotAgent {
17+
if command == "" {
18+
command = "copilot"
19+
}
20+
return &CopilotAgent{Command: command}
21+
}
22+
23+
func (a *CopilotAgent) Name() string {
24+
return "copilot"
25+
}
26+
27+
func (a *CopilotAgent) CommandName() string {
28+
return a.Command
29+
}
30+
31+
func (a *CopilotAgent) Review(ctx context.Context, repoPath, commitSHA, prompt string) (string, error) {
32+
// Use copilot with --prompt for non-interactive mode
33+
args := []string{
34+
"--prompt", prompt,
35+
}
36+
37+
cmd := exec.CommandContext(ctx, a.Command, args...)
38+
cmd.Dir = repoPath
39+
40+
var stdout, stderr bytes.Buffer
41+
cmd.Stdout = &stdout
42+
cmd.Stderr = &stderr
43+
44+
if err := cmd.Run(); err != nil {
45+
return "", fmt.Errorf("copilot failed: %w\nstderr: %s", err, stderr.String())
46+
}
47+
48+
output := stdout.String()
49+
if len(output) == 0 {
50+
return "No review output generated", nil
51+
}
52+
53+
return output, nil
54+
}
55+
56+
func init() {
57+
Register(NewCopilotAgent(""))
58+
}

internal/agent/gemini.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package agent
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"fmt"
7+
"os/exec"
8+
)
9+
10+
// GeminiAgent runs code reviews using the Gemini CLI
11+
type GeminiAgent struct {
12+
Command string // The gemini command to run (default: "gemini")
13+
}
14+
15+
// NewGeminiAgent creates a new Gemini agent
16+
func NewGeminiAgent(command string) *GeminiAgent {
17+
if command == "" {
18+
command = "gemini"
19+
}
20+
return &GeminiAgent{Command: command}
21+
}
22+
23+
func (a *GeminiAgent) Name() string {
24+
return "gemini"
25+
}
26+
27+
func (a *GeminiAgent) CommandName() string {
28+
return a.Command
29+
}
30+
31+
func (a *GeminiAgent) Review(ctx context.Context, repoPath, commitSHA, prompt string) (string, error) {
32+
// Use gemini with -p for non-interactive mode
33+
// Output goes to stdout
34+
args := []string{
35+
"-p", prompt,
36+
}
37+
38+
cmd := exec.CommandContext(ctx, a.Command, args...)
39+
cmd.Dir = repoPath
40+
41+
var stdout, stderr bytes.Buffer
42+
cmd.Stdout = &stdout
43+
cmd.Stderr = &stderr
44+
45+
if err := cmd.Run(); err != nil {
46+
return "", fmt.Errorf("gemini failed: %w\nstderr: %s", err, stderr.String())
47+
}
48+
49+
output := stdout.String()
50+
if len(output) == 0 {
51+
return "No review output generated", nil
52+
}
53+
54+
return output, nil
55+
}
56+
57+
func init() {
58+
Register(NewGeminiAgent(""))
59+
}

0 commit comments

Comments
 (0)