Skip to content

Commit 8227078

Browse files
Merge pull request #52 from MiniCodeMonkey/add-cursor-support-rebased
feat: add cursor agent support
2 parents 7380992 + 0593ca5 commit 8227078

File tree

14 files changed

+1000
-16
lines changed

14 files changed

+1000
-16
lines changed

cmd/chief/main.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ type TUIOptions struct {
2828
Merge bool
2929
Force bool
3030
NoRetry bool
31-
Agent string // --agent claude|codex
31+
Agent string // --agent claude|codex|opencode|cursor
3232
AgentPath string // --agent-path
3333
}
3434

@@ -134,7 +134,7 @@ func parseAgentFlags(args []string, startIdx int) (agentName, agentPath string,
134134
i++
135135
agentName = args[i]
136136
} else {
137-
fmt.Fprintf(os.Stderr, "Error: --agent requires a value (claude, codex, or opencode)\n")
137+
fmt.Fprintf(os.Stderr, "Error: --agent requires a value (claude, codex, opencode, or cursor)\n")
138138
os.Exit(1)
139139
}
140140
case strings.HasPrefix(arg, "--agent="):
@@ -514,7 +514,7 @@ Commands:
514514
help Show this help message
515515
516516
Global Options:
517-
--agent <provider> Agent CLI to use: claude (default), codex, or opencode
517+
--agent <provider> Agent CLI to use: claude (default), codex, opencode, or cursor
518518
--agent-path <path> Custom path to agent CLI binary
519519
--max-iterations N, -n N Set maximum iterations (default: dynamic)
520520
--no-retry Disable auto-retry on agent crashes
@@ -541,6 +541,7 @@ Examples:
541541
Launch auth PRD with 5 max iterations
542542
chief --verbose Launch with raw agent output visible
543543
chief --agent codex Use Codex CLI instead of Claude
544+
chief --agent cursor Use Cursor CLI as agent
544545
chief new Create PRD in .chief/prds/main/
545546
chief new auth Create PRD in .chief/prds/auth/
546547
chief new auth "JWT authentication for REST API"

docs/.vitepress/theme/components/AgentSupport.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const agents = [
33
{ name: 'Claude Code', description: 'By Anthropic' },
44
{ name: 'Codex CLI', description: 'By OpenAI' },
55
{ name: 'OpenCode', description: 'Open source' },
6+
{ name: 'Cursor CLI', description: 'By Cursor' },
67
]
78
</script>
89

docs/concepts/prd-format.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ Each PRD lives in its own subdirectory inside `.chief/prds/`:
2323

2424
- **`prd.md`** — Written by you, read and updated by Chief. Contains project context and structured user stories.
2525
- **`progress.md`** — Written by the agent. Tracks what was done, what changed, and what was learned.
26-
- **`claude.log`** (or `codex.log` / `opencode.log`) — Written by Chief. Raw output from the agent for debugging.
26+
- **`claude.log`** (or `codex.log` / `opencode.log` / `cursor.log`) — Written by Chief. Raw output from the agent for debugging.
2727

2828
## prd.md — The PRD File
2929

docs/guide/installation.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,15 @@ To use [OpenCode CLI](https://opencode.ai) as an alternative:
4545
2. Ensure `opencode` is on your PATH, or set `agent.cliPath` in `.chief/config.yaml` (see [Configuration](/reference/configuration#agent)).
4646
3. Run Chief with `chief --agent opencode` or set `CHIEF_AGENT=opencode`, or set `agent.provider: opencode` in `.chief/config.yaml`.
4747

48+
### Option D: Cursor CLI
49+
50+
To use [Cursor CLI](https://cursor.com/docs/cli/overview) as the agent:
51+
52+
1. Install Cursor CLI per the [official docs](https://cursor.com/docs/cli/overview)
53+
2. Ensure `agent` is on your PATH, or set `agent.cliPath` in `.chief/config.yaml`.
54+
3. Run `agent login` for authentication.
55+
4. Run Chief with `chief --agent cursor` or set `CHIEF_AGENT=cursor`, or set `agent.provider: cursor` in `.chief/config.yaml`.
56+
4857
### Optional: GitHub CLI (`gh`)
4958

5059
If you want Chief to automatically create pull requests when a PRD completes, install the [GitHub CLI](https://cli.github.com/):

docs/guide/quick-start.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@ Before you begin, make sure you have:
1818
- [Claude Code](https://github.com/anthropics/claude-code) (default)
1919
- [Codex CLI](https://developers.openai.com/codex/cli/reference)
2020
- [OpenCode CLI](https://opencode.ai/docs/)
21+
- [Cursor CLI](https://cursor.com/docs/cli/overview)
2122
- A project you want to work on (or create a new one)
2223

2324
::: tip Verify your agent CLI is working
2425
Run the version command for your agent to confirm it's installed:
2526
- `claude --version` (Claude Code)
2627
- `codex --version` (Codex)
2728
- `opencode --version` (OpenCode)
29+
- `agent --version` (Cursor CLI)
2830
:::
2931

3032
## Step 1: Install Chief

docs/reference/configuration.md

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ Chief stores project-level settings in `.chief/config.yaml`. This file is create
1414

1515
```yaml
1616
agent:
17-
provider: claude # or "codex" or "opencode"
17+
provider: claude # or "codex", "opencode", or "cursor"
1818
cliPath: "" # optional path to CLI binary
1919
worktree:
2020
setup: "npm install"
@@ -27,7 +27,7 @@ onComplete:
2727
2828
| Key | Type | Default | Description |
2929
|-----|------|---------|-------------|
30-
| `agent.provider` | string | `"claude"` | Agent CLI to use: `claude`, `codex`, or `opencode` |
30+
| `agent.provider` | string | `"claude"` | Agent CLI to use: `claude`, `codex`, `opencode`, or `cursor` |
3131
| `agent.cliPath` | string | `""` | Optional path to the agent binary (e.g. `/usr/local/bin/opencode`). If empty, Chief uses the provider name from PATH. |
3232
| `worktree.setup` | string | `""` | Shell command to run in new worktrees (e.g., `npm install`, `go mod download`) |
3333
| `onComplete.push` | bool | `false` | Automatically push the branch to remote when a PRD completes |
@@ -88,7 +88,7 @@ These settings are saved to `.chief/config.yaml` and can be changed at any time
8888

8989
| Flag | Description | Default |
9090
|------|-------------|---------|
91-
| `--agent <provider>` | Agent CLI to use: `claude`, `codex`, or `opencode` | From config / env / `claude` |
91+
| `--agent <provider>` | Agent CLI to use: `claude`, `codex`, `opencode`, or `cursor` | From config / env / `claude` |
9292
| `--agent-path <path>` | Custom path to the agent CLI binary | From config / env |
9393
| `--max-iterations <n>`, `-n` | Loop iteration limit | Dynamic |
9494
| `--no-retry` | Disable auto-retry on agent crashes | `false` |
@@ -100,7 +100,7 @@ When `--max-iterations` is not specified, Chief calculates a dynamic limit based
100100

101101
## Agent
102102

103-
Chief can use **Claude Code** (default), **Codex CLI**, or **OpenCode CLI** as the agent. Choose via:
103+
Chief can use **Claude Code** (default), **Codex CLI**, **OpenCode CLI**, or **Cursor CLI** as the agent. Choose via:
104104

105105
- **Config:** `agent.provider: opencode` and optionally `agent.cliPath: /path/to/opencode` in `.chief/config.yaml`
106106
- **Environment:** `CHIEF_AGENT=opencode`, `CHIEF_AGENT_PATH=/path/to/opencode`
@@ -120,6 +120,15 @@ claude config set model claude-3-opus-20240229
120120

121121
See [Claude Code documentation](https://github.com/anthropics/claude-code) for details.
122122

123+
When using Cursor CLI:
124+
125+
```bash
126+
# Authentication (or set CURSOR_API_KEY for headless)
127+
agent login
128+
```
129+
130+
Chief runs Cursor in headless mode with `--trust` and `--force` so it can modify files without prompts. See [Cursor CLI documentation](https://cursor.com/docs/cli/overview) for details.
131+
123132
## Permission Handling
124133

125134
Some agents (like Claude Code) ask for permission before executing bash commands, writing files, and making network requests. Chief automatically configures the agent for autonomous operation by disabling these prompts.

docs/troubleshooting/common-issues.md

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,13 @@ Error: OpenCode CLI not found in PATH. Install it or set agent.cliPath in .chief
4444
cliPath: /usr/local/bin/opencode
4545
```
4646
Verify with `opencode --version` (or your `cliPath`).
47+
- **Cursor:** Install [Cursor CLI](https://cursor.com/docs/cli/overview) and ensure `agent` is in PATH, or set the path in config:
48+
```yaml
49+
agent:
50+
provider: cursor
51+
cliPath: /path/to/agent
52+
```
53+
Run `agent login`. Verify with `agent --version` (or your `cliPath`).
4754

4855
## Permission Denied
4956

@@ -63,9 +70,9 @@ Chief automatically configures the agent for autonomous operation by disabling p
6370

6471
**Solution:**
6572

66-
1. Check the agent log for errors (the log file matches your agent: `claude.log`, `codex.log`, or `opencode.log`):
73+
1. Check the agent log for errors (the log file matches your agent: `claude.log`, `codex.log`, `opencode.log`, or `cursor.log`):
6774
```bash
68-
tail -100 .chief/prds/your-prd/claude.log # or codex.log / opencode.log
75+
tail -100 .chief/prds/your-prd/claude.log # or codex.log / opencode.log / cursor.log
6976
```
7077

7178
2. Manually mark story complete if appropriate by editing `prd.md`:
@@ -85,7 +92,7 @@ Chief automatically configures the agent for autonomous operation by disabling p
8592

8693
1. Check the agent log for what the agent is doing:
8794
```bash
88-
tail -f .chief/prds/your-prd/claude.log # or codex.log / opencode.log
95+
tail -f .chief/prds/your-prd/claude.log # or codex.log / opencode.log / cursor.log
8996
```
9097

9198
2. Simplify the current story's acceptance criteria
@@ -114,7 +121,7 @@ Chief automatically configures the agent for autonomous operation by disabling p
114121

115122
2. Or investigate why it's taking so many iterations:
116123
- Story too complex? Split it
117-
- Stuck in a loop? Check the agent log (`claude.log`, `codex.log`, or `opencode.log`)
124+
- Stuck in a loop? Check the agent log (`claude.log`, `codex.log`, `opencode.log`, or `cursor.log`)
118125
- Unclear acceptance criteria? Clarify them
119126

120127
## "No PRD Found"
@@ -255,4 +262,4 @@ If none of these solutions help:
255262
3. Open a new issue with:
256263
- Chief version (`chief --version`)
257264
- Your `prd.md` (sanitized)
258-
- Relevant agent log excerpts (e.g. `claude.log`, `codex.log`, or `opencode.log`)
265+
- Relevant agent log excerpts (e.g. `claude.log`, `codex.log`, `opencode.log`, or `cursor.log`)

internal/agent/cursor.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package agent
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"os/exec"
7+
"strings"
8+
9+
"github.com/minicodemonkey/chief/internal/loop"
10+
)
11+
12+
// CursorProvider implements loop.Provider for the Cursor CLI (agent).
13+
type CursorProvider struct {
14+
cliPath string
15+
}
16+
17+
// NewCursorProvider returns a Provider for the Cursor CLI.
18+
// If cliPath is empty, "agent" is used.
19+
func NewCursorProvider(cliPath string) *CursorProvider {
20+
if cliPath == "" {
21+
cliPath = "agent"
22+
}
23+
return &CursorProvider{cliPath: cliPath}
24+
}
25+
26+
// Name implements loop.Provider.
27+
func (p *CursorProvider) Name() string { return "Cursor" }
28+
29+
// CLIPath implements loop.Provider.
30+
func (p *CursorProvider) CLIPath() string { return p.cliPath }
31+
32+
// LoopCommand implements loop.Provider.
33+
// Prompt is supplied via stdin; Cursor CLI reads it when -p has no argument.
34+
func (p *CursorProvider) LoopCommand(ctx context.Context, prompt, workDir string) *exec.Cmd {
35+
cmd := exec.CommandContext(ctx, p.cliPath,
36+
"-p",
37+
"--output-format", "stream-json",
38+
"--force",
39+
"--workspace", workDir,
40+
"--trust",
41+
)
42+
cmd.Dir = workDir
43+
cmd.Stdin = strings.NewReader(prompt)
44+
return cmd
45+
}
46+
47+
// InteractiveCommand implements loop.Provider.
48+
func (p *CursorProvider) InteractiveCommand(workDir, prompt string) *exec.Cmd {
49+
cmd := exec.Command(p.cliPath, prompt)
50+
cmd.Dir = workDir
51+
return cmd
52+
}
53+
54+
// ParseLine implements loop.Provider.
55+
func (p *CursorProvider) ParseLine(line string) *loop.Event {
56+
return loop.ParseLineCursor(line)
57+
}
58+
59+
// LogFileName implements loop.Provider.
60+
func (p *CursorProvider) LogFileName() string { return "cursor.log" }
61+
62+
// cursorResultLine is the structure for Cursor's result/success JSON lines.
63+
type cursorResultLine struct {
64+
Type string `json:"type"`
65+
Subtype string `json:"subtype,omitempty"`
66+
Result string `json:"result,omitempty"`
67+
}
68+
69+
// CleanOutput extracts the result from Cursor's json or stream-json output.
70+
// For stream-json, finds the last type "result", subtype "success" and returns its result field.
71+
// For single-line json, parses and returns result.
72+
func (p *CursorProvider) CleanOutput(output string) string {
73+
output = strings.TrimSpace(output)
74+
if output == "" {
75+
return output
76+
}
77+
// Try single JSON object (json output format)
78+
var single cursorResultLine
79+
if json.Unmarshal([]byte(output), &single) == nil && single.Type == "result" && single.Subtype == "success" && single.Result != "" {
80+
return single.Result
81+
}
82+
// NDJSON: find last result/success line
83+
lines := strings.Split(output, "\n")
84+
for i := len(lines) - 1; i >= 0; i-- {
85+
line := strings.TrimSpace(lines[i])
86+
if line == "" {
87+
continue
88+
}
89+
var ev cursorResultLine
90+
if json.Unmarshal([]byte(line), &ev) == nil && ev.Type == "result" && ev.Subtype == "success" && ev.Result != "" {
91+
return ev.Result
92+
}
93+
}
94+
return output
95+
}

internal/agent/cursor_test.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package agent
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/minicodemonkey/chief/internal/loop"
8+
)
9+
10+
func TestCursorProvider_Name(t *testing.T) {
11+
p := NewCursorProvider("")
12+
if p.Name() != "Cursor" {
13+
t.Errorf("Name() = %q, want Cursor", p.Name())
14+
}
15+
}
16+
17+
func TestCursorProvider_CLIPath(t *testing.T) {
18+
p := NewCursorProvider("")
19+
if p.CLIPath() != "agent" {
20+
t.Errorf("CLIPath() empty arg = %q, want agent", p.CLIPath())
21+
}
22+
p2 := NewCursorProvider("/usr/local/bin/agent")
23+
if p2.CLIPath() != "/usr/local/bin/agent" {
24+
t.Errorf("CLIPath() custom = %q, want /usr/local/bin/agent", p2.CLIPath())
25+
}
26+
}
27+
28+
func TestCursorProvider_LogFileName(t *testing.T) {
29+
p := NewCursorProvider("")
30+
if p.LogFileName() != "cursor.log" {
31+
t.Errorf("LogFileName() = %q, want cursor.log", p.LogFileName())
32+
}
33+
}
34+
35+
func TestCursorProvider_LoopCommand(t *testing.T) {
36+
ctx := context.Background()
37+
p := NewCursorProvider("/bin/agent")
38+
cmd := p.LoopCommand(ctx, "hello world", "/work/dir")
39+
40+
if cmd.Path != "/bin/agent" {
41+
t.Errorf("LoopCommand Path = %q, want /bin/agent", cmd.Path)
42+
}
43+
wantArgs := []string{"/bin/agent", "-p", "--output-format", "stream-json", "--force", "--workspace", "/work/dir", "--trust"}
44+
if len(cmd.Args) != len(wantArgs) {
45+
t.Fatalf("LoopCommand Args len = %d, want %d: %v", len(cmd.Args), len(wantArgs), cmd.Args)
46+
}
47+
for i, w := range wantArgs {
48+
if cmd.Args[i] != w {
49+
t.Errorf("LoopCommand Args[%d] = %q, want %q", i, cmd.Args[i], w)
50+
}
51+
}
52+
if cmd.Dir != "/work/dir" {
53+
t.Errorf("LoopCommand Dir = %q, want /work/dir", cmd.Dir)
54+
}
55+
if cmd.Stdin == nil {
56+
t.Error("LoopCommand Stdin must be set (prompt via stdin)")
57+
}
58+
}
59+
60+
func TestCursorProvider_InteractiveCommand(t *testing.T) {
61+
p := NewCursorProvider("/bin/agent")
62+
cmd := p.InteractiveCommand("/work", "my prompt")
63+
if cmd.Dir != "/work" {
64+
t.Errorf("InteractiveCommand Dir = %q, want /work", cmd.Dir)
65+
}
66+
if len(cmd.Args) != 2 || cmd.Args[0] != "/bin/agent" || cmd.Args[1] != "my prompt" {
67+
t.Errorf("InteractiveCommand Args = %v, want [/bin/agent my prompt]", cmd.Args)
68+
}
69+
}
70+
71+
func TestCursorProvider_ParseLine(t *testing.T) {
72+
p := NewCursorProvider("")
73+
line := `{"type":"system","subtype":"init","session_id":"x"}`
74+
e := p.ParseLine(line)
75+
if e == nil {
76+
t.Fatal("ParseLine(system init) returned nil")
77+
}
78+
if e.Type != loop.EventIterationStart {
79+
t.Errorf("ParseLine(system init) Type = %v, want EventIterationStart", e.Type)
80+
}
81+
}
82+
83+
func TestCursorProvider_CleanOutput(t *testing.T) {
84+
p := NewCursorProvider("")
85+
// NDJSON: last result/success
86+
ndjson := `{"type":"system","subtype":"init"}
87+
{"type":"result","subtype":"success","result":"final answer","session_id":"x"}`
88+
if got := p.CleanOutput(ndjson); got != "final answer" {
89+
t.Errorf("CleanOutput(NDJSON) = %q, want final answer", got)
90+
}
91+
// Single JSON result
92+
single := `{"type":"result","subtype":"success","result":"single result","session_id":"x"}`
93+
if got := p.CleanOutput(single); got != "single result" {
94+
t.Errorf("CleanOutput(single JSON) = %q, want single result", got)
95+
}
96+
// No result: return as-is
97+
plain := "plain text"
98+
if got := p.CleanOutput(plain); got != plain {
99+
t.Errorf("CleanOutput(plain) = %q, want %q", got, plain)
100+
}
101+
}

internal/agent/resolve.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,10 @@ func Resolve(flagAgent, flagPath string, cfg *config.Config) (loop.Provider, err
3939
return NewCodexProvider(cliPath), nil
4040
case "opencode":
4141
return NewOpenCodeProvider(cliPath), nil
42+
case "cursor":
43+
return NewCursorProvider(cliPath), nil
4244
default:
43-
return nil, fmt.Errorf("unknown agent provider %q: expected \"claude\", \"codex\", or \"opencode\"", providerName)
45+
return nil, fmt.Errorf("unknown agent provider %q: expected \"claude\", \"codex\", \"opencode\", or \"cursor\"", providerName)
4446
}
4547
}
4648

0 commit comments

Comments
 (0)