Skip to content

Commit 4fd5b10

Browse files
etothexipiJonathan Simsclaude
authored
Add OpenCode agent support (#5)
Adds support for the OpenCode agent (https://opencode.ai), providing users with another powerful AI code review option. Enhances #2 ## Changes - Add `opencode` agent implementation using non-interactive CLI mode - Update fallback chain: codex → claude-code → gemini → copilot → opencode - Update tests, README, and CLI help text - Follows same pattern as gemini/copilot PR (#3) ## Installation ```bash npm install -g opencode-ai ``` ## Testing - [x] `go test ./...` passes - [x] `go build ./...` succeeds - [x] Follows established contribution patterns ## Notes OpenCode is an open-source AI coding agent that supports multiple backends, including local LLMs. --------- Co-authored-by: Jonathan Sims <jonathan@simsventures.ai> Co-authored-by: Claude <noreply@anthropic.com>
1 parent a175460 commit 4fd5b10

File tree

5 files changed

+83
-13
lines changed

5 files changed

+83
-13
lines changed

README.md

Lines changed: 4 additions & 3 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" # codex, claude-code, gemini, or copilot
62+
agent = "claude-code" # codex, claude-code, gemini, copilot, or opencode
6363
review_context_count = 5
6464

6565
# Project-specific review guidelines (multi-line string)
@@ -115,13 +115,14 @@ roborev supports multiple AI review agents:
115115
| `claude-code` | Anthropic Claude Code | `npm install -g @anthropic-ai/claude-code` |
116116
| `gemini` | Google Gemini | `npm install -g @google/gemini-cli` |
117117
| `copilot` | GitHub Copilot | `npm install -g @github/copilot` |
118+
| `opencode` | OpenCode | `npm install -g opencode-ai` |
118119

119120
### Automatic Fallback
120121

121122
roborev automatically detects which agents are installed. If your preferred agent isn't available, it falls back in this order:
122123

123124
```
124-
codex → claude-code → gemini → copilot
125+
codex → claude-code → gemini → copilot → opencode
125126
```
126127

127128
If none are installed, the job fails with a helpful error message.
@@ -151,7 +152,7 @@ roborev enqueue HEAD --agent copilot
151152
1. `--agent` flag on enqueue command
152153
2. Per-repo `.roborev.toml`
153154
3. Global `~/.roborev/config.toml`
154-
4. Automatic detection (first available: codex → claude-code → gemini → copilot)
155+
4. Automatic detection (first available: codex → claude-code → gemini → copilot → opencode)
155156

156157
## Commands
157158

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, Gemini, Copilot)",
35+
Long: "RoboRev automatically reviews git commits using AI agents (Codex, Claude Code, Gemini, Copilot, OpenCode)",
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, gemini, copilot)")
298+
cmd.Flags().StringVar(&agent, "agent", "", "default agent (codex, claude-code, gemini, copilot, opencode)")
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, gemini, copilot)")
421+
cmd.Flags().StringVar(&agent, "agent", "", "agent to use (codex, claude-code, gemini, copilot, opencode)")
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, gemini, copilot
77-
fallbacks := []string{"codex", "claude-code", "gemini", "copilot"}
76+
// Fallback order: codex, claude-code, gemini, copilot, opencode
77+
fallbacks := []string{"codex", "claude-code", "gemini", "copilot", "opencode"}
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 one of: codex, claude-code, gemini, copilot)")
93+
return nil, fmt.Errorf("no agents available (install one of: codex, claude-code, gemini, copilot, opencode)")
9494
}
9595

9696
return Get(available[0])

internal/agent/agent_test.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import (
66

77
func TestAgentRegistry(t *testing.T) {
88
// Check that all agents are registered
9-
agents := []string{"codex", "claude-code", "gemini", "copilot", "test"}
9+
agents := []string{"codex", "claude-code", "gemini", "copilot", "opencode", "test"}
1010
for _, name := range agents {
1111
a, err := Get(name)
1212
if err != nil {
@@ -26,16 +26,17 @@ func TestAgentRegistry(t *testing.T) {
2626

2727
func TestAvailableAgents(t *testing.T) {
2828
agents := Available()
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)
29+
// We have 6 agents: codex, claude-code, gemini, copilot, opencode, test
30+
if len(agents) < 6 {
31+
t.Errorf("Expected at least 6 agents, got %d: %v", len(agents), agents)
3232
}
3333

3434
expected := map[string]bool{
3535
"codex": false,
3636
"claude-code": false,
3737
"gemini": false,
3838
"copilot": false,
39+
"opencode": false,
3940
"test": false,
4041
}
4142

internal/agent/opencode.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package agent
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"fmt"
7+
"os/exec"
8+
)
9+
10+
// OpenCodeAgent runs code reviews using the OpenCode CLI
11+
type OpenCodeAgent struct {
12+
Command string // The opencode command to run (default: "opencode")
13+
}
14+
15+
// NewOpenCodeAgent creates a new OpenCode agent
16+
func NewOpenCodeAgent(command string) *OpenCodeAgent {
17+
if command == "" {
18+
command = "opencode"
19+
}
20+
return &OpenCodeAgent{Command: command}
21+
}
22+
23+
func (a *OpenCodeAgent) Name() string {
24+
return "opencode"
25+
}
26+
27+
func (a *OpenCodeAgent) CommandName() string {
28+
return a.Command
29+
}
30+
31+
func (a *OpenCodeAgent) Review(ctx context.Context, repoPath, commitSHA, prompt string) (string, error) {
32+
// OpenCode CLI supports a headless invocation via `opencode run [message..]`.
33+
// We run it from the repo root so it can use project context, and pass the full
34+
// roborev prompt as the message.
35+
//
36+
// Helpful reference:
37+
// opencode --help
38+
// opencode run --help
39+
args := []string{"run", "--format", "default", prompt}
40+
41+
cmd := exec.CommandContext(ctx, a.Command, args...)
42+
cmd.Dir = repoPath
43+
44+
var stdout, stderr bytes.Buffer
45+
cmd.Stdout = &stdout
46+
cmd.Stderr = &stderr
47+
48+
if err := cmd.Run(); err != nil {
49+
// opencode sometimes prints failures/usage to stdout; include both streams.
50+
return "", fmt.Errorf(
51+
"opencode failed: %w\nstdout: %s\nstderr: %s",
52+
err,
53+
stdout.String(),
54+
stderr.String(),
55+
)
56+
}
57+
58+
output := stdout.String()
59+
if len(output) == 0 {
60+
return "No review output generated", nil
61+
}
62+
63+
return output, nil
64+
}
65+
66+
func init() {
67+
Register(NewOpenCodeAgent(""))
68+
}

0 commit comments

Comments
 (0)