# gate4agent
Universal Rust transport library for CLI AI agents. Spawn, stream, resume — for five different CLI agents through one unified API.
**Not a harness. Not a sandbox.** gate4agent is the thin wiring layer between your Rust app and the CLI agent's subprocess: spawn the binary, write the prompt, read structured events, resume by session id. That's it.
## Supported CLI tools
| **Claude Code** | Pipe + PTY | ✓ stream-json | ✓ `--resume <id>` | Prompt via stdin |
| **Codex** | Pipe + PTY | ✓ `--json` | ✓ `exec resume <id>` | Uses `--full-auto` for non-interactive execution |
| **Gemini** | Pipe + PTY | ✓ stream-json | ✓ `--resume <id>` | Prompt via `-p` flag |
| **Cursor Agent** | Pipe | ✓ stream-json | ✓ `--resume <id>` | Claude-compatible schema |
| **OpenCode** (`sst/opencode`) | Pipe | ✓ `--format json` | ✓ `--session ses_XXX` | 5-event NDJSON schema |
Transport classes:
- **Pipe**: spawn the CLI directly, read NDJSON over stdout
- **PTY**: spawn inside a pseudo-terminal, scrape the screen with vt100 (for agents without structured output)
## Quick start
```rust
use gate4agent::{TransportSession, SpawnOptions, CliTool, AgentEvent};
use std::path::PathBuf;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let opts = SpawnOptions {
working_dir: PathBuf::from("."),
prompt: "Say hello in 3 words".into(),
resume_session_id: None,
model: None,
append_system_prompt: None,
extra_args: vec![],
env_vars: vec![],
};
let session = TransportSession::spawn(
CliTool::ClaudeCode,
&opts.working_dir.clone(),
&opts.prompt.clone(),
opts,
).await?;
let mut rx = session.subscribe();
while let Ok(event) = rx.recv().await {
match event {
AgentEvent::Text { text, .. } => print!("{text}"),
AgentEvent::SessionEnd { .. } => break,
_ => {}
}
}
Ok(())
}
```
### Resume an existing session
```rust
let opts = SpawnOptions {
resume_session_id: Some("abc-123-session".into()),
..opts
};
```
Each CLI handles resume in its own way — Codex swaps `exec` → `exec resume <id>`, Claude/Cursor use `--resume <id>`, OpenCode uses `--session <ses_XXX>`. gate4agent hides the difference behind `SpawnOptions::resume_session_id`.
### Using PipeSession directly (backwards-compatible API)
```rust
use gate4agent::{PipeSession, PipeProcessOptions, ClaudeOptions, SessionConfig, CliTool};
let config = SessionConfig {
tool: CliTool::ClaudeCode,
working_dir: std::env::current_dir()?,
env_vars: vec![],
name: None,
};
let opts = PipeProcessOptions {
claude: ClaudeOptions { model: Some("claude-opus-4".into()), ..Default::default() },
..Default::default()
};
let session = PipeSession::spawn(config, "hello", opts).await?;
```
## Features
- **Single API for 5 CLIs** — `TransportSession::spawn(tool, cwd, prompt, options)`
- **Backwards-compatible `PipeSession`** — 0.1.x consumers that used `PipeSession::spawn(config, prompt, options)` compile unchanged
- **SessionEnd synthesis** — Codex has no terminal event; gate4agent synthesizes `SessionEnd { result: "exit_code=N", is_error: N != 0 }` on child exit
- **Transport-neutral events** — `AgentEvent::{Text, ToolStart, ToolResult, Thinking, TurnComplete, SessionStart, SessionEnd}`
- **Cross-platform** — Windows (ConPTY + `cmd /C` argv wrapping) and Unix (POSIX PTY + bare exec)
- **Rate-limit detection** — pattern-based session/daily/weekly limit detection per CLI
- **Zero new dependencies** — gate4agent 0.2.1 removes dead fantasy transports without adding anything new
## Architecture
```
gate4agent/
├── src/
│ ├── lib.rs — Library root, re-exports
│ ├── core/ — AgentEvent, CliTool, SessionConfig, AgentError
│ ├── transport/ — TransportSession (thin router over PipeSession), SpawnOptions
│ ├── pipe/ — PipeSession, PipeProcess, per-CLI NDJSON parsers + command builders
│ │ └── cli/ — claude.rs, codex.rs, gemini.rs, cursor.rs, opencode.rs
│ ├── pty/ — PtyWrapper, PtySession, VTE/screen parsers, per-CLI PTY parsers
│ │ └── cli/ — Per-CLI PTY output parsers
│ ├── history/ — Session history reader
│ └── utils.rs — String utilities
```
## Testing status
| **Claude Code** | ✓ live-verified (0.2.5) | ✗ untested | Full session: init → text → tokens → result |
| **Codex** | ✓ live-verified (0.2.5) | ✗ untested | Full session: thread.started → item.completed → turn.completed |
| **Gemini** | ✓ parser verified (0.2.5) | ✗ untested | Init event parsed; API returned 429 (rate limit) |
| **OpenCode** | ✓ parser verified (0.2.5) | ✗ untested | Error event parsed, session ID tracked; API key misconfigured |
| **Cursor Agent** | ✗ CLI broken on test machine | N/A (no PTY) | `node_sqlite3.node` incompatible (Linux binary on Windows) |
PTY parsers existed in 0.1.x and are structurally simple (screen scraping) — low risk of breakage.
Pipe parsers for Claude and Codex are fully live-verified. Gemini and OpenCode parsers correctly handle real CLI output but need valid API credentials for full end-to-end testing. Cursor parser is structurally correct but the CLI itself is broken on the test machine.
## Windows spawn strategy
On Windows, CLI tools are invoked through the appropriate shell:
- **npm-installed CLIs** (claude, codex, gemini, opencode): `cmd /C program.cmd arg1 arg2` — the `.cmd` batch wrapper is detected via PATH lookup
- **Bash scripts** (cursor-agent): `bash -c 'program arg1 arg2'` — fallback when no `.cmd` wrapper exists
- **Unix**: direct `Command::new("program")` — no shell wrapping needed
Arguments are passed individually (not joined into a shell string) to avoid cmd.exe quote-mangling issues.
## Prerequisites
At least one CLI agent must be installed on the host. gate4agent does not install them.
| Claude Code | `npm install -g @anthropic-ai/claude-code` |
| Codex | `npm install -g @openai/codex` |
| Gemini | `npm install -g @google/gemini-cli` |
| Cursor Agent | See https://cursor.com/docs/cli |
| OpenCode | `npm install -g opencode-ai` (or see https://opencode.ai) |
## Versioning
- **0.1.x** — original 3-CLI library (Claude, Codex, Gemini)
- **0.2.0** — breaking: 6 CLIs, `TransportSession`, `AgentEvent` renamed, `PipeSession` removed, OpenClaw fantasy transport
- **0.2.1** — cleanup: OpenClaw removed (was never functional), `PipeSession` restored for 0.1.x compatibility, `TransportSession` is now a thin router over `PipeSession`
- **0.2.2** — parser isolation: NdjsonParser trait extracted, per-CLI parser modules split out
- **0.2.3** — source tree restructure into core/pty/pipe layout; proper pipe builders+parsers for Codex, Gemini, Cursor, OpenCode (research-based, NOT yet tested against live CLI output)
- **0.2.4** — docs update, Codex flags fixed (`--full-auto` replaces removed `--ask-for-approval`)
- **0.2.5** — live integration tests: fixed Codex flags, OpenCode `run` subcommand, Gemini `-p` flag, Windows `cmd /C` quoting; all parsers verified against real CLI output
See [ROADMAP.md](ROADMAP.md) for what's next and [DEBUGGING.md](DEBUGGING.md) for known issues and mitigations.
## Migration guide
### 0.2.0 → 0.2.1
- **OpenClaw removed** — `CliTool::OpenClaw` no longer exists. If you matched on it, delete that arm. OpenClaw was never functional (unverified daemon protocol, fictional acpx API surface).
- **`PipeSession` restored** — 0.1.x callers that used `PipeSession::spawn(config, prompt, options)` compile again. The `PipeSession` now includes SessionEnd synthesis (previously only in the 0.2.0 `pipe_runner`).
- **`TransportSession`** is now a thin wrapper over `PipeSession`. Its public API (`spawn`, `subscribe`, `session_id`, `send_prompt`, `kill`) is unchanged. Internal: no more `TransportHandle` enum, no dead `Pty` variant.
- **`DaemonNotRunning` / `DaemonProbeTimeout` error variants removed** — they were only reachable via OpenClaw. Remove any match arms for these.
### 0.1.x → 0.2.1
1. **Events**: `AgentEvent::Pipe*` → neutral names. Rename all match arms:
- `PipeText` → `Text`
- `PipeToolStart` → `ToolStart`
- `PipeToolResult` → `ToolResult`
- `PipeThinking` → `Thinking`
- `PipeTurnComplete` → `TurnComplete`
- `PipeSessionStart` → `SessionStart`
- `PipeSessionEnd` → `SessionEnd`
2. **`PipeSession::spawn`** — signature unchanged: `PipeSession::spawn(config, prompt, options)`. Compiles directly.
3. **`SpawnOptions`**: new unified struct. Fields: `working_dir`, `prompt`, `resume_session_id`, `model`, `append_system_prompt`, `extra_args`, `env_vars`.
4. **`CliTool`** is now non-exhaustive in effect (2 new variants: `Cursor`, `OpenCode`). Add arms or a `_ =>` fallback.
## Support the Project
If you find this tool useful, consider supporting development:
| USDT | TRC20 | `TNxMKsvVLYViQ5X5sgCYmkzH4qjhhh5U7X` |
| USDC | Arbitrum | `0xEF3B94Fe845E21371b4C4C5F2032E1f23A13Aa6e` |
| ETH | Ethereum | `0xEF3B94Fe845E21371b4C4C5F2032E1f23A13Aa6e` |
| BTC | Bitcoin | `bc1qjgzthxja8umt5tvrp5tfcf9zeepmhn0f6mnt40` |
| SOL | Solana | `DZJjmH8Cs5wEafz5Ua86wBBkurSA4xdWXa3LWnBUR94c` |
## License
MIT