Skip to content

feat: add support for cursor cli #54

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# AgentAPI

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.
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.

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

Expand Down Expand Up @@ -65,7 +65,7 @@ agentapi server -- goose
```

> [!NOTE]
> When using Codex, always specify the agent type explicitly (`agentapi server --type=codex -- codex`), or message formatting may break.
> When using Codex, Gemini or CursorCLI, always specify the agent type explicitly (eg: `agentapi server --type=codex -- codex`), or message formatting may break.

An OpenAPI schema is available in [openapi.json](openapi.json).

Expand Down
2 changes: 2 additions & 0 deletions cmd/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const (
AgentTypeCodex AgentType = msgfmt.AgentTypeCodex
AgentTypeGemini AgentType = msgfmt.AgentTypeGemini
AgentTypeAmp AgentType = msgfmt.AgentTypeAmp
AgentTypeCursor AgentType = msgfmt.AgentTypeCursor
AgentTypeCustom AgentType = msgfmt.AgentTypeCustom
)

Expand All @@ -40,6 +41,7 @@ var agentTypeMap = map[AgentType]bool{
AgentTypeCodex: true,
AgentTypeGemini: true,
AgentTypeAmp: true,
AgentTypeCursor: true,
AgentTypeCustom: true,
}

Expand Down
10 changes: 10 additions & 0 deletions cmd/server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ func TestParseAgentType(t *testing.T) {
agentTypeVar: "",
want: AgentTypeGemini,
},
{
firstArg: "cursor",
agentTypeVar: "",
want: AgentTypeCursor,
},
{
firstArg: "amp",
agentTypeVar: "",
Expand Down Expand Up @@ -82,6 +87,11 @@ func TestParseAgentType(t *testing.T) {
agentTypeVar: "gemini",
want: AgentTypeGemini,
},
{
firstArg: "claude",
agentTypeVar: "cursor",
want: AgentTypeCursor,
},
{
firstArg: "aider",
agentTypeVar: "claude",
Expand Down
53 changes: 40 additions & 13 deletions lib/msgfmt/msgfmt.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,24 @@ func findUserInputEndIdx(userInputStartIdx int, msg []rune, userInput []rune) in
return msgIdx
}

// skipTrailingInputBoxLine checks if the next line contains all the given markers
// and returns the incremented index if found. In case of Gemini and Cursor, the user
// input is echoed back in a box. This function searches for the markers passed by the
// caller and returns (currentIdx+1, true) if the next line contains all of them in the same order,
// otherwise returns (currentIdx, false).
func skipTrailingInputBoxLine(lines []string, currentIdx int, markers ...string) (idx int, found bool) {
if currentIdx+1 >= len(lines) {
return currentIdx, false
}
line := lines[currentIdx+1]
for _, m := range markers {
if !strings.Contains(line, m) {
return currentIdx, false
}
}
return currentIdx + 1, true
}

// RemoveUserInput removes the user input from the message.
// Goose, Aider, and Claude Code echo back the user's input to
// make it visible in the terminal. This function makes a best effort
Expand All @@ -149,7 +167,7 @@ func findUserInputEndIdx(userInputStartIdx int, msg []rune, userInput []rune) in
// For instance, if there are any leading or trailing lines with only whitespace,
// and each line of the input in msgRaw is preceded by a character like `>`,
// these lines will not be removed.
func RemoveUserInput(msgRaw string, userInputRaw string) string {
func RemoveUserInput(msgRaw string, userInputRaw string, agentType AgentType) string {
if userInputRaw == "" {
return msgRaw
}
Expand All @@ -169,9 +187,15 @@ func RemoveUserInput(msgRaw string, userInputRaw string) string {
// that doesn't contain the echoed user input.
lastUserInputLineIdx := msgRuneLineLocations[userInputEndIdx]

// In case of Gemini, the user input echoed back is wrapped in a rounded box, so we remove it.
if lastUserInputLineIdx+1 < len(msgLines) && strings.Contains(msgLines[lastUserInputLineIdx+1], "╯") && strings.Contains(msgLines[lastUserInputLineIdx+1], "╰") {
lastUserInputLineIdx += 1
// Skip Gemini/Cursor trailing input box line
if agentType == AgentTypeGemini {
if idx, found := skipTrailingInputBoxLine(msgLines, lastUserInputLineIdx, "╯", "╰"); found {
lastUserInputLineIdx = idx
}
} else if agentType == AgentTypeCursor {
if idx, found := skipTrailingInputBoxLine(msgLines, lastUserInputLineIdx, "┘", "└"); found {
lastUserInputLineIdx = idx
}
}

return strings.Join(msgLines[lastUserInputLineIdx+1:], "\n")
Expand Down Expand Up @@ -207,18 +231,19 @@ const (
AgentTypeCodex AgentType = "codex"
AgentTypeGemini AgentType = "gemini"
AgentTypeAmp AgentType = "amp"
AgentTypeCursor AgentType = "cursor"
AgentTypeCustom AgentType = "custom"
)

func formatGenericMessage(message string, userInput string) string {
message = RemoveUserInput(message, userInput)
func formatGenericMessage(message string, userInput string, agentType AgentType) string {
message = RemoveUserInput(message, userInput, agentType)
message = removeMessageBox(message)
message = trimEmptyLines(message)
return message
}

func formatCodexMessage(message string, userInput string) string {
message = RemoveUserInput(message, userInput)
message = RemoveUserInput(message, userInput, AgentTypeCodex)
message = removeCodexInputBox(message)
message = trimEmptyLines(message)
return message
Expand All @@ -227,19 +252,21 @@ func formatCodexMessage(message string, userInput string) string {
func FormatAgentMessage(agentType AgentType, message string, userInput string) string {
switch agentType {
case AgentTypeClaude:
return formatGenericMessage(message, userInput)
return formatGenericMessage(message, userInput, agentType)
case AgentTypeGoose:
return formatGenericMessage(message, userInput)
return formatGenericMessage(message, userInput, agentType)
case AgentTypeAider:
return formatGenericMessage(message, userInput)
return formatGenericMessage(message, userInput, agentType)
case AgentTypeCodex:
return formatCodexMessage(message, userInput)
case AgentTypeGemini:
return formatGenericMessage(message, userInput)
return formatGenericMessage(message, userInput, agentType)
case AgentTypeAmp:
return formatGenericMessage(message, userInput)
return formatGenericMessage(message, userInput, agentType)
case AgentTypeCursor:
return formatGenericMessage(message, userInput, agentType)
case AgentTypeCustom:
return formatGenericMessage(message, userInput)
return formatGenericMessage(message, userInput, agentType)
default:
return message
}
Expand Down
2 changes: 1 addition & 1 deletion lib/msgfmt/msgfmt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ func TestRemoveUserInput(t *testing.T) {
assert.NoError(t, err)
expected, err := testdataDir.ReadFile(path.Join(dir, c.Name(), "expected.txt"))
assert.NoError(t, err)
assert.Equal(t, string(expected), RemoveUserInput(string(msg), string(userInput)))
assert.Equal(t, string(expected), RemoveUserInput(string(msg), string(userInput), AgentTypeCustom))
})
}
}
Expand Down
19 changes: 19 additions & 0 deletions lib/msgfmt/testdata/format/cursor/confirmation_box/expected.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
I'll check the repository's root, name, remotes, and current branch.




┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ $ git rev-parse --show-toplevel in . │
│ $ basename "$(git rev-parse --show-toplevel)" in . │
│ $ git remote -v in . │
│ $ git rev-parse --abbrev-ref HEAD in . │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Run this command? │
│ Not in allowlist: git │
│ → Run (y) (enter) │
│ Reject (esc or p) │
│ Add Shell(git) to allowlist? (tab) │
│ Auto-run all commands (shift+tab) │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
26 changes: 26 additions & 0 deletions lib/msgfmt/testdata/format/cursor/confirmation_box/msg.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
Cursor Agent
~/Documents/work/agentapi · feat-cursor-cli

┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Which repo is this ? │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

I'll check the repository's root, name, remotes, and current branch.




┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ $ git rev-parse --show-toplevel in . │
│ $ basename "$(git rev-parse --show-toplevel)" in . │
│ $ git remote -v in . │
│ $ git rev-parse --abbrev-ref HEAD in . │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Run this command? │
│ Not in allowlist: git │
│ → Run (y) (enter) │
│ Reject (esc or p) │
│ Add Shell(git) to allowlist? (tab) │
│ Auto-run all commands (shift+tab) │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Which repo is this ?
Loading