Skip to content

Commit 284d955

Browse files
committed
Merge branch 'main' into feat-opencode
# Conflicts: # cmd/server/server.go # cmd/server/server_test.go # lib/msgfmt/msgfmt.go # lib/msgfmt/msgfmt_test.go
2 parents f2b8266 + 0876f7d commit 284d955

File tree

21 files changed

+491
-48
lines changed

21 files changed

+491
-48
lines changed

AGENTS.md

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# AGENTS.md
2+
3+
This file provides guidance to AI agents working with code in this repository.
4+
5+
## Build Commands
6+
7+
- `make build` - Build the binary to `out/agentapi` (includes chat UI build)
8+
- `make embed` - Build the chat UI and embed it into Go
9+
- `go build -o out/agentapi main.go` - Direct Go build without chat UI
10+
- `go generate ./...` - Generate OpenAPI schema and version info
11+
12+
## Testing
13+
14+
- `go test ./...` - Run all Go tests
15+
- Tests are located alongside source files (e.g., `lib/httpapi/server_test.go`)
16+
17+
## Development Commands
18+
19+
- `agentapi server -- claude` - Start server with Claude Code agent
20+
- `agentapi server -- aider --model sonnet` - Start server with Aider agent
21+
- `agentapi server -- goose` - Start server with Goose agent
22+
- `agentapi server --type=codex -- codex` - Start server with Codex (requires explicit type)
23+
- `agentapi server --type=gemini -- gemini` - Start server with Gemini (requires explicit type)
24+
- `agentapi attach --url localhost:3284` - Attach to running agent terminal
25+
- Server runs on port 3284 by default
26+
- Chat UI available at http://localhost:3284/chat
27+
- API documentation at http://localhost:3284/docs
28+
29+
## Architecture
30+
31+
This is a Go HTTP API server that controls coding agents (Claude Code, Aider, Goose, etc.) through terminal emulation.
32+
33+
**Core Components:**
34+
- `main.go` - Entry point using cobra CLI framework
35+
- `cmd/` - CLI command definitions (server, attach)
36+
- `lib/httpapi/` - HTTP server, routes, and OpenAPI schema
37+
- `lib/screentracker/` - Terminal output parsing and message splitting
38+
- `lib/termexec/` - Terminal process execution and management
39+
- `lib/msgfmt/` - Message formatting for different agent types (claude, goose, aider, codex, gemini, amp, cursor-agent, cursor, auggie, custom)
40+
- `chat/` - Next.js web UI (embedded into Go binary)
41+
42+
**Key Architecture:**
43+
- Runs agents in an in-memory terminal emulator
44+
- Translates API calls to terminal keystrokes
45+
- Parses terminal output into structured messages
46+
- Supports multiple agent types with different message formats
47+
- Embeds Next.js chat UI as static assets in Go binary
48+
49+
**Message Flow:**
50+
1. User sends message via HTTP API
51+
2. Server takes terminal snapshot
52+
3. Message sent to agent as terminal input
53+
4. Terminal output changes tracked and parsed
54+
5. New content becomes agent response message
55+
6. SSE events stream updates to clients
56+
57+
## API Endpoints
58+
59+
- GET `/messages` - Get all messages in conversation
60+
- POST `/message` - Send message to agent (content, type fields)
61+
- GET `/status` - Get agent status ("stable" or "running")
62+
- GET `/events` - SSE stream of agent events
63+
- GET `/openapi.json` - OpenAPI schema
64+
- GET `/docs` - API documentation UI
65+
- GET `/chat` - Web chat interface
66+
67+
## Supported Agents
68+
69+
Agents with explicit type requirement (use `--type=<agent>`):
70+
- `codex` - OpenAI Codex
71+
- `gemini` - Google Gemini CLI
72+
- `amp` - Sourcegraph Amp CLI
73+
- `cursor` - Cursor CLI
74+
75+
Agents with auto-detection:
76+
- `claude` - Claude Code (default)
77+
- `goose` - Goose
78+
- `aider` - Aider
79+
80+
## Project Structure
81+
82+
- Go module with standard layout
83+
- Chat UI in `chat/` directory (Next.js + TypeScript)
84+
- OpenAPI schema auto-generated to `openapi.json`
85+
- Version managed via `version.sh` script

README.md

Lines changed: 1 addition & 1 deletion
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), [Codex](https://github.com/openai/codex), [Auggie](https://docs.augmentcode.com/cli/overview), and [Cursor CLI](https://cursor.com/en/cli) with an HTTP API.
3+
Control [Claude Code](https://github.com/anthropics/claude-code), [AmazonQ](https://aws.amazon.com/developer/learning/q-developer-cli/), [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), [Auggie](https://docs.augmentcode.com/cli/overview), 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

cmd/server/server.go

Lines changed: 31 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -23,46 +23,46 @@ import (
2323
type AgentType = msgfmt.AgentType
2424

2525
const (
26-
AgentTypeClaude AgentType = msgfmt.AgentTypeClaude
27-
AgentTypeGoose AgentType = msgfmt.AgentTypeGoose
28-
AgentTypeAider AgentType = msgfmt.AgentTypeAider
29-
AgentTypeCodex AgentType = msgfmt.AgentTypeCodex
30-
AgentTypeGemini AgentType = msgfmt.AgentTypeGemini
31-
AgentTypeAmp AgentType = msgfmt.AgentTypeAmp
32-
AgentTypeCursorAgent AgentType = msgfmt.AgentTypeCursorAgent
33-
AgentTypeCursor AgentType = msgfmt.AgentTypeCursor
34-
AgentTypeAuggie AgentType = msgfmt.AgentTypeAuggie
35-
AgentTypeOpencode AgentType = msgfmt.AgentTypeOpencode
36-
AgentTypeCustom AgentType = msgfmt.AgentTypeCustom
26+
AgentTypeClaude AgentType = msgfmt.AgentTypeClaude
27+
AgentTypeGoose AgentType = msgfmt.AgentTypeGoose
28+
AgentTypeAider AgentType = msgfmt.AgentTypeAider
29+
AgentTypeCodex AgentType = msgfmt.AgentTypeCodex
30+
AgentTypeGemini AgentType = msgfmt.AgentTypeGemini
31+
AgentTypeAmp AgentType = msgfmt.AgentTypeAmp
32+
AgentTypeCursor AgentType = msgfmt.AgentTypeCursor
33+
AgentTypeAuggie AgentType = msgfmt.AgentTypeAuggie
34+
AgentTypeAmazonQ AgentType = msgfmt.AgentTypeAmazonQ
35+
AgentTypeOpencode AgentType = msgfmt.AgentTypeOpencode
36+
AgentTypeCustom AgentType = msgfmt.AgentTypeCustom
3737
)
3838

39-
// exhaustiveness of this map is checked by the exhaustive linter
40-
var agentTypeMap = map[AgentType]bool{
41-
AgentTypeClaude: true,
42-
AgentTypeGoose: true,
43-
AgentTypeAider: true,
44-
AgentTypeCodex: true,
45-
AgentTypeGemini: true,
46-
AgentTypeAmp: true,
47-
AgentTypeCursorAgent: true,
48-
AgentTypeCursor: true,
49-
AgentTypeAuggie: true,
50-
AgentTypeOpencode: true,
51-
AgentTypeCustom: true,
39+
// agentTypeAliases contains the mapping of possible input agent type strings to their canonical AgentType values
40+
var agentTypeAliases = map[string]AgentType{
41+
"claude": AgentTypeClaude,
42+
"goose": AgentTypeGoose,
43+
"aider": AgentTypeAider,
44+
"codex": AgentTypeCodex,
45+
"gemini": AgentTypeGemini,
46+
"amp": AgentTypeAmp,
47+
"auggie": AgentTypeAuggie,
48+
"cursor": AgentTypeCursor,
49+
"cursor-agent": AgentTypeCursor,
50+
"q": AgentTypeAmazonQ,
51+
"amazonq": AgentTypeAmazonQ,
52+
"opencode": AgentTypeOpencode,
53+
"custom": AgentTypeCustom,
5254
}
5355

5456
func parseAgentType(firstArg string, agentTypeVar string) (AgentType, error) {
5557
// if the agent type is provided, use it
56-
castedAgentType := AgentType(agentTypeVar)
57-
if _, ok := agentTypeMap[castedAgentType]; ok {
58+
if castedAgentType, ok := agentTypeAliases[agentTypeVar]; ok {
5859
return castedAgentType, nil
5960
}
6061
if agentTypeVar != "" {
6162
return AgentTypeCustom, fmt.Errorf("invalid agent type: %s", agentTypeVar)
6263
}
6364
// if the agent type is not provided, guess it from the first argument
64-
castedFirstArg := AgentType(firstArg)
65-
if _, ok := agentTypeMap[castedFirstArg]; ok {
65+
if castedFirstArg, ok := agentTypeAliases[firstArg]; ok {
6666
return castedFirstArg, nil
6767
}
6868
return AgentTypeCustom, nil
@@ -146,9 +146,9 @@ func runServer(ctx context.Context, logger *slog.Logger, argsToPass []string) er
146146
}
147147

148148
var agentNames = (func() []string {
149-
names := make([]string, 0, len(agentTypeMap))
150-
for agentType := range agentTypeMap {
151-
names = append(names, string(agentType))
149+
names := make([]string, 0, len(agentTypeAliases))
150+
for agentType := range agentTypeAliases {
151+
names = append(names, agentType)
152152
}
153153
sort.Strings(names)
154154
return names

cmd/server/server_test.go

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,23 @@ func TestParseAgentType(t *testing.T) {
5050
{
5151
firstArg: "cursor-agent",
5252
agentTypeVar: "",
53-
want: AgentTypeCursorAgent,
53+
want: AgentTypeCursor,
5454
},
5555
{
5656
firstArg: "cursor",
5757
agentTypeVar: "",
5858
want: AgentTypeCursor,
5959
},
60+
{
61+
firstArg: "amazonq",
62+
agentTypeVar: "",
63+
want: AgentTypeAmazonQ,
64+
},
65+
{
66+
firstArg: "q",
67+
agentTypeVar: "",
68+
want: AgentTypeAmazonQ,
69+
},
6070
{
6171
firstArg: "opencode",
6272
agentTypeVar: "",
@@ -102,6 +112,16 @@ func TestParseAgentType(t *testing.T) {
102112
agentTypeVar: "gemini",
103113
want: AgentTypeGemini,
104114
},
115+
{
116+
firstArg: "claude",
117+
agentTypeVar: "amazonq",
118+
want: AgentTypeAmazonQ,
119+
},
120+
{
121+
firstArg: "claude",
122+
agentTypeVar: "q",
123+
want: AgentTypeAmazonQ,
124+
},
105125
{
106126
firstArg: "claude",
107127
agentTypeVar: "opencode",
@@ -110,7 +130,7 @@ func TestParseAgentType(t *testing.T) {
110130
{
111131
firstArg: "claude",
112132
agentTypeVar: "cursor-agent",
113-
want: AgentTypeCursorAgent,
133+
want: AgentTypeCursor,
114134
},
115135
{
116136
firstArg: "claude",

lib/msgfmt/msgfmt.go

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ func RemoveUserInput(msgRaw string, userInputRaw string, agentType AgentType) st
192192
if idx, found := skipTrailingInputBoxLine(msgLines, lastUserInputLineIdx, "╯", "╰"); found {
193193
lastUserInputLineIdx = idx
194194
}
195-
} else if agentType == AgentTypeCursorAgent || agentType == AgentTypeCursor {
195+
} else if agentType == AgentTypeCursor {
196196
if idx, found := skipTrailingInputBoxLine(msgLines, lastUserInputLineIdx, "┘", "└"); found {
197197
lastUserInputLineIdx = idx
198198
}
@@ -232,17 +232,17 @@ func trimEmptyLines(message string) string {
232232
type AgentType string
233233

234234
const (
235-
AgentTypeClaude AgentType = "claude"
236-
AgentTypeGoose AgentType = "goose"
237-
AgentTypeAider AgentType = "aider"
238-
AgentTypeCodex AgentType = "codex"
239-
AgentTypeGemini AgentType = "gemini"
240-
AgentTypeAmp AgentType = "amp"
241-
AgentTypeCursorAgent AgentType = "cursor-agent"
242-
AgentTypeCursor AgentType = "cursor"
243-
AgentTypeAuggie AgentType = "auggie"
235+
AgentTypeClaude AgentType = "claude"
236+
AgentTypeGoose AgentType = "goose"
237+
AgentTypeAider AgentType = "aider"
238+
AgentTypeCodex AgentType = "codex"
239+
AgentTypeGemini AgentType = "gemini"
240+
AgentTypeAmp AgentType = "amp"
241+
AgentTypeCursor AgentType = "cursor"
242+
AgentTypeAuggie AgentType = "auggie"
243+
AgentTypeAmazonQ AgentType = "amazonq"
244244
AgentTypeOpencode AgentType = "opencode"
245-
AgentTypeCustom AgentType = "custom"
245+
AgentTypeCustom AgentType = "custom"
246246
)
247247

248248
func formatGenericMessage(message string, userInput string, agentType AgentType) string {
@@ -280,12 +280,12 @@ func FormatAgentMessage(agentType AgentType, message string, userInput string) s
280280
return formatGenericMessage(message, userInput, agentType)
281281
case AgentTypeAmp:
282282
return formatGenericMessage(message, userInput, agentType)
283-
case AgentTypeCursorAgent:
284-
return formatGenericMessage(message, userInput, agentType)
285283
case AgentTypeCursor:
286284
return formatGenericMessage(message, userInput, agentType)
287285
case AgentTypeAuggie:
288286
return formatGenericMessage(message, userInput, agentType)
287+
case AgentTypeAmazonQ:
288+
return formatGenericMessage(message, userInput, agentType)
289289
case AgentTypeOpencode:
290290
return formatOpencodeMessage(message, userInput)
291291
case AgentTypeCustom:

lib/msgfmt/msgfmt_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ func TestTrimEmptyLines(t *testing.T) {
218218

219219
func TestFormatAgentMessage(t *testing.T) {
220220
dir := "testdata/format"
221-
agentTypes := []AgentType{AgentTypeClaude, AgentTypeGoose, AgentTypeAider, AgentTypeGemini, AgentTypeAmp, AgentTypeCodex, AgentTypeCursorAgent, AgentTypeCursor, AgentTypeAuggie, AgentTypeOpencode, AgentTypeCustom}
221+
agentTypes := []AgentType{AgentTypeClaude, AgentTypeGoose, AgentTypeAider, AgentTypeGemini, AgentTypeAmp, AgentTypeCodex, AgentTypeCursor, AgentTypeAuggie, AgentTypeAmazonQ, AgentTypeOpencode, AgentTypeCustom}
222222
for _, agentType := range agentTypes {
223223
t.Run(string(agentType), func(t *testing.T) {
224224
cases, err := testdataDir.ReadDir(path.Join(dir, string(agentType)))
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
🛠️ Using tool: coder_report_task from mcp server coder
2+
3+
● Running coder_report_task with the param:
4+
⋮ {
5+
⋮ "name": "coder_report_task",
6+
⋮ "arguments": {
7+
⋮ "summary": "Checking current directory to identify repository",
8+
⋮ "link": "",
9+
⋮ "state": "working"
10+
⋮ }
11+
⋮ }
12+
13+
Allow this action? Use 't' to trust (always allow) this tool for the session. [y/n/t]:
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
> what repo is this ?
2+
3+
4+
5+
6+
🛠️ Using tool: coder_report_task from mcp server coder
7+
8+
● Running coder_report_task with the param:
9+
⋮ {
10+
⋮ "name": "coder_report_task",
11+
⋮ "arguments": {
12+
⋮ "summary": "Checking current directory to identify repository",
13+
⋮ "link": "",
14+
⋮ "state": "working"
15+
⋮ }
16+
⋮ }
17+
18+
Allow this action? Use 't' to trust (always allow) this tool for the session. [y/n/t]:
19+
20+
>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
what repo is this ?
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
✓ coder loaded in 0.03 s
2+
3+
Welcome to Amazon Q!
4+
5+
💡 Run /prompts to learn how to build & run repeatable workflows
6+
7+
/help all commands
8+
ctrl + j new lines
9+
ctrl + s fuzzy search
10+
11+
12+
🤖 You are chatting with claude-sonnet-4

0 commit comments

Comments
 (0)