| name | ai-cli |
| description | Drive AI CLIs (opencode, pi, codex, claude-code, …) — auth, streaming, tool use, sessions |
| allowed-tools | Bash(agent-tui:*) |
AI CLIs (opencode, pi, codex, claude-code, …)
agent-tui can drive other AI coding agents as the PTY app under it
— useful when you want a meta-agent orchestrating sub-agents, when
you want to record/replay an AI session for audit, or when you want
the snapshot+ref interaction model on top of another AI CLI's UI.
The basic loop is the same as core: spawn the
CLI, wait for its prompt, snapshot, interact. What changes is what
you're waiting for — AI CLIs have characteristic UI shapes that
make detection robust.
Outline shape
Claude Code, Codex, Pi, Aider, and OpenCode have provider-specific terminal manifests.
They share a prompt/response shape, but their refs are intentionally
not collapsed into one namespace:
@claude role=root
├── @claude.response role=response
├── @claude.input role=input focused=true when the cursor is there
├── @claude.approval role=approval-request
├── @claude.tool role=tool-call
├── @claude.file-change role=file-change
└── @claude.done role=done
@codex role=root
└── same child refs under @codex
@pi role=root
└── same child refs under @pi
@aider role=root
└── same child refs under @aider
@opencode role=root
└── same child refs under @opencode
The signal refs are regex-derived from the visible terminal screen.
They are enough for supervising a live session, but they are not the
same thing as SDK/harness event streams with structured permission or
tool-result objects.
Batch modes are intentionally different. pi --print, codex exec,
and Claude print/help/version invocations are stdout-producing commands,
not long-lived live agent screens, so the bundled provider manifests do
not claim them. Capture those with the generic shell/stdout path.
Read agent-tui skills get addressing for selector syntax. Common
patterns:
| Goal | Selector |
|---|
| Wait for Codex to be ready for input | @codex.input[focused] |
| Snapshot just the Codex reply | @codex.response |
| Wait for approval | @codex.approval |
| Wait for a file edit | @codex.file-change |
| Wait for completion | @codex.done |
| Get the input prompt's current line | @codex.input (read .name) |
Agents controlling agents
The useful case is a live session over time: prompt, wait for a plan,
approve, verify a file change, run checks, and wait for done.
agent-tui spawn -- codex
agent-tui wait --ref '@codex.input[focused]'
agent-tui type --to '@codex.input' 'plan the release note and wait for approval'
agent-tui press --to '@codex.input' '<cr>'
agent-tui wait --ref '@codex.approval'
agent-tui type --to '@codex.input' 'approve'
agent-tui press --to '@codex.input' '<cr>'
agent-tui wait --ref '@codex.file-change'
agent-tui type --to '@codex.input' 'run checks'
agent-tui press --to '@codex.input' '<cr>'
agent-tui wait --ref '@codex.done'
Read core first
This skill assumes the core loop. It covers AI-CLI-specific
patterns: auth, streaming detection, session DBs, tool use.
Driving Pi (earendil-works/pi)
Pi is a single-turn-friendly OpenAI-compatible CLI. Its --print
mode runs non-interactively, prints the reply to stdout, exits.
agent-tui spawn -- pi --provider openai --model gpt-4o-mini --print "say hello"
agent-tui wait --text "\$"
agent-tui snapshot
Pi reads provider config from $PI_CODING_AGENT_DIR/models.json.
For multi-provider setups, configure providers there ahead of time.
Driving OpenCode (sst/opencode)
OpenCode is interactive by default but has a run subcommand for
non-interactive use:
agent-tui spawn -- bash -c "cd /work && exec opencode run -m openai/o3-mini 'refactor this'"
agent-tui wait --text "build · o3-mini"
Critical flags for non-interactive automation:
--dangerously-skip-permissions — auto-approve tool calls;
without it, opencode prompts on the TTY and deadlocks under a
programmatic PTY.
--title 'fixed string' — opencode normally fires a hidden
title-generation request to the model before the real prompt;
pass a fixed title to skip it.
--pure — skip plugins (deterministic).
Run cd /work && exec opencode … so the per-project opencode.json
gets picked up (opencode looks for it in the current directory).
Reading the assistant's reply
Two strategies, depending on the CLI:
1. CLI writes the reply to stdout (Pi, claude-code, --format json):
agent-tui snapshot
2. CLI writes only a header, persists the body to a session DB
(OpenCode --format default):
OpenCode persists each session at
~/.local/share/opencode/opencode.db (SQLite, WAL mode). After the
process exits, read the part table:
sqlite3 ~/.local/share/opencode/opencode.db \
"SELECT data FROM part WHERE message_id IN (
SELECT id FROM message ORDER BY time_created DESC LIMIT 1)"
Or, in WAL mode immediately after a fresh run (data may not yet be
checkpointed), grep both files:
grep -a "MARKER_TEXT" ~/.local/share/opencode/opencode.db \
~/.local/share/opencode/opencode.db-wal
Streaming responses
For visible-progress streaming, prefer a CLI mode that writes events
to stdout as they arrive:
agent-tui spawn -- bash -c "cd /work && exec opencode run --format json 'long task'"
agent-tui wait --text "\"type\":\"text\""
This is much more reliable than mid-render screenshots — JSON
deltas are deterministic, screen contents during a stream are not.
Tool use round-trips
When the model calls a tool (bash, edit, glob, …), the CLI executes
it locally then sends the result back in a follow-up request. The
full loop:
- Model returns
function_call(bash, {"command":"…","description":"…"}).
- CLI runs the command. With
--dangerously-skip-permissions no
prompt blocks; otherwise the CLI shows a confirm dialog.
- CLI posts a 2nd request with
function_call_output containing
the stdout.
- Model returns the final assistant text.
- CLI persists tool call + output + assistant text to its session DB.
agent-tui spawn -- bash -c "cd /work && exec opencode run \
--dangerously-skip-permissions \
-m openai/o3-mini 'find files matching pattern X'"
agent-tui wait --text "build · "
OpenCode bash tool schema requires BOTH command AND
description keys in the arguments JSON. The model usually emits
both correctly; if you're hand-crafting the args (test scenarios),
omitting either yields a SchemaError instead of running the command.
Long-lived sessions
Most AI CLIs persist a session ID that lets you resume later:
agent-tui spawn -- bash -c "cd /work && exec opencode run --continue 'follow-up question'"
agent-tui spawn -- bash -c "cd /work && exec opencode run --session <ID> 'specific session'"
agent-tui spawn -- pi --resume <session-id> --print "follow-up"
The session ID surfaces in stdout on creation. For OpenCode it also
appears in the DB's session table.
When the agent reports done
Detection options, in order of robustness:
- Process exit —
agent-tui wait --exit (P2) — best, no ambiguity.
- Return to the shell prompt —
agent-tui wait --text "\$ "
— works when you spawned bash -c "agent …" rather than the
agent directly.
- CLI's "done" marker — opencode's session header redraw,
pi's
\n flush, claude-code's > prompt — fragile to version
changes.
Don't use --idle as the primary "done" signal — long-running
generations have legitimate quiet periods.
Authentication
Today, set credentials before spawn:
agent-tui spawn --env OPENAI_API_KEY=sk-… --env ANTHROPIC_API_KEY=… -- opencode run …
Or write the CLI's own credentials file under $HOME before
spawning. Per-CLI:
| CLI | Credentials path |
|---|
| OpenCode | ~/.config/opencode/auth.json |
| Pi | $PI_CODING_AGENT_DIR/models.json |
| Claude Code | ~/.claude.json |
| Codex | ~/.config/codex/credentials.json |
The P3 auth vault will provide a single agent-tui auth save <name>
API across CLIs.
Testing AI-CLI integrations hermetically
Driving an AI CLI in a TEST should not hit a real model. agent-tui
ships an in-process fake-inference server (under
crates/agent-tui-integration/src/fake_inference.rs) that speaks
the OpenAI Chat Completions API and the Responses API. Point the
CLI's baseURL at it and script canned replies:
let server = FakeServer::start(
Script::new()
.stream(["Hello ", "from ", "the fake"])
.tool_call("bash", r#"{"command":"ls /","description":"list"}"#)
.say("Done.")
).await?;
See crates/agent-tui-integration/tests/opencode_fake_inference.rs
and pi_fake_inference.rs for working scenarios. Lessons learned:
- sha256-pin the CLI binary in your Dockerfile. Don't use
latest.
- Pre-install ripgrep for CLIs that bundle it (OpenCode
symlinks at
~/.cache/opencode/bin/rg).
- Persist
$HOME when you need to inspect the session DB
post-run.
- Read
.db-wal too for content written but not yet
checkpointed.
When NOT to use this skill
- Shell sessions: shell.
- vim/nvim editing: vim.
- htop, less, fzf, lazygit, tig, nano: tui-apps.