Coding agent cli backend interface
Skill kjuhwa/skills-hub/skills/agents/coding-agent-cli-backend-interface
Unified Go/TS interface for invoking any coding-agent CLI (Claude, Codex, OpenCode, Gemini, Cursor, etc.) behind one streaming Execute/Session/Result contract.From its SKILL.md
npx -y skills add kjuhwa/skills-hub --skill coding-agent-cli-backend-interfaceAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
4.7 KB, 987 tokens by cl100k_base, as published. Nobody here has run it
When to use
- Your product spawns multiple coding-agent CLIs and needs a single API to drive them.
- Each agent speaks a slightly different stream format (JSON-lines, event-stream, custom).
- You want to add a new agent provider without touching the UI or server routing layer.
Steps
- Define the contract once:
type Backend interface { Execute(ctx context.Context, prompt string, opts ExecOptions) (*Session, error) } type ExecOptions struct { Cwd, Model, SystemPrompt string MaxTurns int Timeout time.Duration ResumeSessionID string CustomArgs []string McpConfig json.RawMessage } type Session struct { Messages <-chan Message // streaming, closed on completion Result <-chan Result // exactly one value then closed } type Message struct { Type MessageType // text | thinking | tool-use | tool-result | status | error | log Content string Tool string CallID string Input map[string]any Output string Level string } type Result struct { Status string // completed | failed | aborted | timeout Output string Error string DurationMs int64 SessionID string Usage map[string]TokenUsage } - Factory dispatches by name:
func New(agentType string, cfg Config) (Backend, error) { switch agentType { case "claude": return &claudeBackend{cfg}, nil case "codex": return &codexBackend{cfg}, nil case "opencode": return &opencodeBackend{cfg}, nil // ... default: return nil, fmt.Errorf("unknown agent type: %q", agentType) } } - Each backend follows the same pattern:
- Resolve binary path;
exec.LookPathearly to fail fast with a clear error. - Build args (protocol-critical flags hardcoded; user
CustomArgspass through a deny-list filter). - Spawn with a wrapping context + timeout +
WaitDelay. - Stream stdout line-by-line through a buffered scanner with a 10MB max-token size.
- Push parsed messages onto the Messages channel (non-blocking drop if full — final output is accumulated separately).
- On exit, classify outcome:
context.DeadlineExceeded→ "timeout",context.Canceled→ "aborted", exit error → "failed", else "completed".
- Resolve binary path;
- Common helpers (shared by all backends):
mergeEnvthat strips parent-agent env keys (e.g.CLAUDECODE_*) to prevent child-in-parent session leakage.filterCustomArgsper-backend deny-list for protocol flags.writeMcpConfigToTempfor agents that accept--mcp-config <path>.resolveSessionIDto distinguish "resume succeeded" from "fresh session with same failure".
- Provide a stable launch header per provider for UI display:
var launchHeaders = map[string]string{ "claude": "claude (stream-json)", "codex": "codex app-server", "opencode": "opencode run (json)", // ... }
Example
backend, err := agent.New("claude", agent.Config{ ExecutablePath: "/usr/local/bin/claude" })
session, err := backend.Execute(ctx, "fix the login bug", agent.ExecOptions{ Cwd: wd, Timeout: 20*time.Minute })
for msg := range session.Messages {
switch msg.Type {
case agent.MessageText: uiStream.Text(msg.Content)
case agent.MessageToolUse: uiStream.ToolStart(msg.Tool, msg.CallID, msg.Input)
case agent.MessageToolResult: uiStream.ToolEnd(msg.CallID, msg.Output)
}
}
result := <-session.Result
persistUsage(result.SessionID, result.Usage)
Caveats
- Bounded channels (e.g. 256) prevent memory blowup under slow consumers but mean some streaming messages can be dropped — always accumulate final output into
Result.Outputas a safety net. - Token usage is per-model, not per-session: a run that switches Claude models mid-conversation has a map with multiple keys.
- When adding a new backend, mirror
claudeBackendstructure exactly; the similarity is on purpose — it's what lets the orchestrator treat all providers uniformly.
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.