Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
56 changes: 43 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,18 @@ 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 trailing input box line
if agentType == AgentTypeGemini {
if idx, found := skipTrailingInputBoxLine(msgLines, lastUserInputLineIdx, "╯", "╰"); found {
lastUserInputLineIdx = idx
}
}

// Skip Cursor trailing input box line
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 +234,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 +255,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