Skip to content

Commit a81ed94

Browse files
35C4n0rjohnstcn
andcommitted
feat: add support for cursor cli (#54)
Co-authored-by: Cian Johnston <[email protected]>
1 parent 6eb6fb4 commit a81ed94

File tree

20 files changed

+266
-16
lines changed

20 files changed

+266
-16
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# AgentAPI
22

3-
Control [Claude Code](https://github.com/anthropics/claude-code), [Goose](https://github.com/block/goose), [Aider](https://github.com/Aider-AI/aider), [Gemini](https://github.com/google-gemini/gemini-cli), [Sourcegraph Amp](https://github.com/sourcegraph/amp-cli) and [Codex](https://github.com/openai/codex) with an HTTP API.
3+
Control [Claude Code](https://github.com/anthropics/claude-code), [Goose](https://github.com/block/goose), [Aider](https://github.com/Aider-AI/aider), [Gemini](https://github.com/google-gemini/gemini-cli), [Sourcegraph Amp](https://github.com/sourcegraph/amp-cli), [Codex](https://github.com/openai/codex), and [Cursor CLI](https://cursor.com/en/cli) with an HTTP API.
44

55
![agentapi-chat](https://github.com/user-attachments/assets/57032c9f-4146-4b66-b219-09e38ab7690d)
66

@@ -65,7 +65,7 @@ agentapi server -- goose
6565
```
6666

6767
> [!NOTE]
68-
> When using Codex, always specify the agent type explicitly (`agentapi server --type=codex -- codex`), or message formatting may break.
68+
> When using Codex, Gemini or CursorCLI, always specify the agent type explicitly (eg: `agentapi server --type=codex -- codex`), or message formatting may break.
6969
7070
An OpenAPI schema is available in [openapi.json](openapi.json).
7171

cmd/server/server.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const (
2929
AgentTypeCodex AgentType = msgfmt.AgentTypeCodex
3030
AgentTypeGemini AgentType = msgfmt.AgentTypeGemini
3131
AgentTypeAmp AgentType = msgfmt.AgentTypeAmp
32+
AgentTypeCursor AgentType = msgfmt.AgentTypeCursor
3233
AgentTypeCustom AgentType = msgfmt.AgentTypeCustom
3334
)
3435

@@ -40,6 +41,7 @@ var agentTypeMap = map[AgentType]bool{
4041
AgentTypeCodex: true,
4142
AgentTypeGemini: true,
4243
AgentTypeAmp: true,
44+
AgentTypeCursor: true,
4345
AgentTypeCustom: true,
4446
}
4547

cmd/server/server_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ func TestParseAgentType(t *testing.T) {
4747
agentTypeVar: "",
4848
want: AgentTypeGemini,
4949
},
50+
{
51+
firstArg: "cursor",
52+
agentTypeVar: "",
53+
want: AgentTypeCursor,
54+
},
5055
{
5156
firstArg: "amp",
5257
agentTypeVar: "",
@@ -82,6 +87,11 @@ func TestParseAgentType(t *testing.T) {
8287
agentTypeVar: "gemini",
8388
want: AgentTypeGemini,
8489
},
90+
{
91+
firstArg: "claude",
92+
agentTypeVar: "cursor",
93+
want: AgentTypeCursor,
94+
},
8595
{
8696
firstArg: "aider",
8797
agentTypeVar: "claude",

lib/msgfmt/msgfmt.go

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,24 @@ func findUserInputEndIdx(userInputStartIdx int, msg []rune, userInput []rune) in
140140
return msgIdx
141141
}
142142

143+
// skipTrailingInputBoxLine checks if the next line contains all the given markers
144+
// and returns the incremented index if found. In case of Gemini and Cursor, the user
145+
// input is echoed back in a box. This function searches for the markers passed by the
146+
// caller and returns (currentIdx+1, true) if the next line contains all of them,
147+
// otherwise returns (currentIdx, false).
148+
func skipTrailingInputBoxLine(lines []string, currentIdx int, markers ...string) (idx int, found bool) {
149+
if currentIdx+1 >= len(lines) {
150+
return currentIdx, false
151+
}
152+
line := lines[currentIdx+1]
153+
for _, m := range markers {
154+
if !strings.Contains(line, m) {
155+
return currentIdx, false
156+
}
157+
}
158+
return currentIdx + 1, true
159+
}
160+
143161
// RemoveUserInput removes the user input from the message.
144162
// Goose, Aider, and Claude Code echo back the user's input to
145163
// make it visible in the terminal. This function makes a best effort
@@ -149,7 +167,7 @@ func findUserInputEndIdx(userInputStartIdx int, msg []rune, userInput []rune) in
149167
// For instance, if there are any leading or trailing lines with only whitespace,
150168
// and each line of the input in msgRaw is preceded by a character like `>`,
151169
// these lines will not be removed.
152-
func RemoveUserInput(msgRaw string, userInputRaw string) string {
170+
func RemoveUserInput(msgRaw string, userInputRaw string, agentType AgentType) string {
153171
if userInputRaw == "" {
154172
return msgRaw
155173
}
@@ -169,9 +187,15 @@ func RemoveUserInput(msgRaw string, userInputRaw string) string {
169187
// that doesn't contain the echoed user input.
170188
lastUserInputLineIdx := msgRuneLineLocations[userInputEndIdx]
171189

172-
// In case of Gemini, the user input echoed back is wrapped in a rounded box, so we remove it.
173-
if lastUserInputLineIdx+1 < len(msgLines) && strings.Contains(msgLines[lastUserInputLineIdx+1], "╯") && strings.Contains(msgLines[lastUserInputLineIdx+1], "╰") {
174-
lastUserInputLineIdx += 1
190+
// Skip Gemini/Cursor trailing input box line
191+
if agentType == AgentTypeGemini {
192+
if idx, found := skipTrailingInputBoxLine(msgLines, lastUserInputLineIdx, "╯", "╰"); found {
193+
lastUserInputLineIdx = idx
194+
}
195+
} else if agentType == AgentTypeCursor {
196+
if idx, found := skipTrailingInputBoxLine(msgLines, lastUserInputLineIdx, "┘", "└"); found {
197+
lastUserInputLineIdx = idx
198+
}
175199
}
176200

177201
return strings.Join(msgLines[lastUserInputLineIdx+1:], "\n")
@@ -207,18 +231,19 @@ const (
207231
AgentTypeCodex AgentType = "codex"
208232
AgentTypeGemini AgentType = "gemini"
209233
AgentTypeAmp AgentType = "amp"
234+
AgentTypeCursor AgentType = "cursor"
210235
AgentTypeCustom AgentType = "custom"
211236
)
212237

213-
func formatGenericMessage(message string, userInput string) string {
214-
message = RemoveUserInput(message, userInput)
238+
func formatGenericMessage(message string, userInput string, agentType AgentType) string {
239+
message = RemoveUserInput(message, userInput, agentType)
215240
message = removeMessageBox(message)
216241
message = trimEmptyLines(message)
217242
return message
218243
}
219244

220245
func formatCodexMessage(message string, userInput string) string {
221-
message = RemoveUserInput(message, userInput)
246+
message = RemoveUserInput(message, userInput, AgentTypeCodex)
222247
message = removeCodexInputBox(message)
223248
message = trimEmptyLines(message)
224249
return message
@@ -227,19 +252,21 @@ func formatCodexMessage(message string, userInput string) string {
227252
func FormatAgentMessage(agentType AgentType, message string, userInput string) string {
228253
switch agentType {
229254
case AgentTypeClaude:
230-
return formatGenericMessage(message, userInput)
255+
return formatGenericMessage(message, userInput, agentType)
231256
case AgentTypeGoose:
232-
return formatGenericMessage(message, userInput)
257+
return formatGenericMessage(message, userInput, agentType)
233258
case AgentTypeAider:
234-
return formatGenericMessage(message, userInput)
259+
return formatGenericMessage(message, userInput, agentType)
235260
case AgentTypeCodex:
236261
return formatCodexMessage(message, userInput)
237262
case AgentTypeGemini:
238-
return formatGenericMessage(message, userInput)
263+
return formatGenericMessage(message, userInput, agentType)
239264
case AgentTypeAmp:
240-
return formatGenericMessage(message, userInput)
265+
return formatGenericMessage(message, userInput, agentType)
266+
case AgentTypeCursor:
267+
return formatGenericMessage(message, userInput, agentType)
241268
case AgentTypeCustom:
242-
return formatGenericMessage(message, userInput)
269+
return formatGenericMessage(message, userInput, agentType)
243270
default:
244271
return message
245272
}

lib/msgfmt/msgfmt_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ func TestRemoveUserInput(t *testing.T) {
188188
assert.NoError(t, err)
189189
expected, err := testdataDir.ReadFile(path.Join(dir, c.Name(), "expected.txt"))
190190
assert.NoError(t, err)
191-
assert.Equal(t, string(expected), RemoveUserInput(string(msg), string(userInput)))
191+
assert.Equal(t, string(expected), RemoveUserInput(string(msg), string(userInput), AgentTypeCustom))
192192
})
193193
}
194194
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
I'll check the repository's root, name, remotes, and current branch.
2+
3+
4+
5+
6+
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
7+
│ $ git rev-parse --show-toplevel in . │
8+
│ $ basename "$(git rev-parse --show-toplevel)" in . │
9+
│ $ git remote -v in . │
10+
│ $ git rev-parse --abbrev-ref HEAD in . │
11+
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
12+
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
13+
│ Run this command? │
14+
│ Not in allowlist: git │
15+
│ → Run (y) (enter) │
16+
│ Reject (esc or p) │
17+
│ Add Shell(git) to allowlist? (tab) │
18+
│ Auto-run all commands (shift+tab) │
19+
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
Cursor Agent
2+
~/Documents/work/agentapi · feat-cursor-cli
3+
4+
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
5+
│ Which repo is this ? │
6+
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
7+
8+
I'll check the repository's root, name, remotes, and current branch.
9+
10+
11+
12+
13+
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
14+
│ $ git rev-parse --show-toplevel in . │
15+
│ $ basename "$(git rev-parse --show-toplevel)" in . │
16+
│ $ git remote -v in . │
17+
│ $ git rev-parse --abbrev-ref HEAD in . │
18+
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
19+
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
20+
│ Run this command? │
21+
│ Not in allowlist: git │
22+
│ → Run (y) (enter) │
23+
│ Reject (esc or p) │
24+
│ Add Shell(git) to allowlist? (tab) │
25+
│ Auto-run all commands (shift+tab) │
26+
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Which repo is this ?

0 commit comments

Comments
 (0)