aiterm-mcp 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img src=".github/og.svg" alt="aiterm-mcp — AI holds one persistent terminal as a stdio MCP server (tmux-backed)" width="100%">
2
+ <img src=".github/og.svg" alt="aiterm-mcp — one persistent MCP terminal your AI drives, and launches other coding agents (Codex/Grok/Composer) into (tmux-backed stdio MCP server)" width="100%">
3
3
  </p>
4
4
 
5
5
  # aiterm-mcp
@@ -12,65 +12,108 @@
12
12
 
13
13
  > *(日本語: [README.ja.md](README.ja.md))*
14
14
 
15
- > **Let Claude — or any MCP client — drive a real, persistent shell.** One terminal stays open; `ssh`, `docker exec`, a REPL are just text you send into it, so the AI stops reconnecting for every single command. Reads come back token-reduced.
15
+ > **Let your AI orchestrate other AIs.** From Claude Code — or any MCP client — one call spawns a coding agent (Codex, Grok, or Composer) inside a persistent terminal and hands you a session to drive: read what it's doing token-reduced, send it the next instruction.
16
+ >
17
+ > **What it is:** one persistent MCP terminal your AI drives — and can launch other coding agents into. `ssh`, `docker exec`, a REPL, or another agent's TUI all nest inside that one terminal as just text you send in. The mechanism is deliberately plain — your MCP client drives the other agent's terminal turn by turn: no hidden protocol, no shared memory, no autonomous negotiation.
18
+ >
19
+ > **No human at a tmux required.** aiterm is driven programmatically over MCP, so an AI can launch and drive another agent with no one sitting in the terminal — from an orchestration loop, a CI step, or a cron job.
16
20
  >
17
21
  > *MCP = Model Context Protocol — the open standard that lets tools like Claude Code plug capabilities into an AI.*
18
22
 
19
- Six PTY tools — `pty_open` / `pty_send` / `pty_read` / `pty_key` / `pty_close` / `pty_list` — plus three interactive **agent launchers** (`codex_agent` / `grok_agent` / `composer_agent`) that start a coding-agent TUI inside that same persistent terminal. The backend is **tmux**, so sessions survive even if the MCP server or the AI client restarts.
23
+ Nine tools: six **PTY tools** — `pty_open` / `pty_send` / `pty_read` / `pty_key` / `pty_close` / `pty_list` — to open, drive, and read one persistent terminal, plus three **agent launchers** `codex_agent` / `grok_agent` / `composer_agent` that each start another coding agent's TUI inside a fresh one. The backend is **tmux**, so sessions survive even if the MCP server or the AI client restarts.
24
+
25
+ **Status:** actively maintained · the newcomer here, betting on a different shape (see [vs. the alternatives](#vs-the-alternatives)) · runs on Linux · WSL2 · macOS · native Windows · MIT · see the [CHANGELOG](CHANGELOG.md).
26
+
27
+ ## Why now
20
28
 
21
- **Status:** actively maintained · runs on Linux · WSL2 · macOS · native Windows · MIT · see the [CHANGELOG](CHANGELOG.md).
29
+ A lot of 2026's agent tooling is converging on orchestration: a lead model delegating a mechanical refactor to Codex, running Composer on a bulk edit while it reviews the diff, fanning one task across several agents to spare its own context window. All of those agents already live in a terminal. aiterm makes that terminal a first-class, MCP-native tool — so the model doing the orchestrating can **spawn and steer the others without a human wiring up panes.**
22
30
 
23
- ## Why
31
+ ## Two ways to use it
24
32
 
25
- Send an AI **one command at a time** and, over SSH, every command becomes its own `connect → authenticate → disconnect`. That stings three ways: you **re-authenticate every time** (passphrase, one-time code, all of it), **short-lived sessions pile up**, and once connections come too fast your own defenses lock you out `fail2ban` bans you, `MaxStartups`/`MaxSessions` reject you, the account gets locked. The security meant to stop attackers ends up stopping you. (Yes — this bit me on my own box. I built aiterm to drive my homelab from Claude Code without that re-auth hell.)
33
+ ### 1. Drive SSH, containers, and REPLs in one persistent terminal — the primitive
26
34
 
27
- aiterm **holds one PTY persistently** and you `ssh host` (or `docker exec -it x bash`) *inside it*, **once**. Every command after that rides the same already-authenticated session: **authenticate once, one session, nothing for the defenses to trip on.** Session kind is never a tool-level distinction.
35
+ This is the base, and it works with just tmux — no other CLI. `pty_open` grabs one local terminal; `ssh host`, `docker exec -it x bash`, or a REPL are just text you `pty_send` into it **once**. Every command after that rides the same already-authenticated session. Session kind is never a tool-level distinction.
28
36
 
29
37
  ```
30
38
  pty_open() → grab one local terminal
31
39
  pty_send(id, "ssh 192.168.1.2") → authenticate once, inside that terminal
32
40
  pty_send(id, "uname -a") → every later command rides the SAME session
33
- pty_read(id, { wait: true }) → read the reduced output
41
+ pty_read(id, { wait: true }) → read the token-reduced output, completion detected
34
42
  ```
35
43
 
44
+ <sub>**Origin.** I built aiterm for exactly this. Driving my homelab from Claude Code one command at a time meant every SSH command became its own `connect → authenticate → disconnect`: re-typing the passphrase and one-time code each time, short-lived sessions piling up, and eventually my own defenses (`fail2ban`, `MaxStartups`/`MaxSessions`, account lockout) locking me out — the security meant to stop attackers ended up stopping me. Holding one authenticated session fixes all three at once. That pain is why the persistent terminal exists; launching whole other agents inside it is what it grew into.</sub>
45
+
46
+ ### 2. Launch other coding agents into that terminal — the orchestration flagship
47
+
48
+ The same primitive hosts another agent's TUI. Three launchers each start one vendor's interactive coding-agent TUI inside a fresh persistent terminal and return a `session_id`. From there you drive it with the same `pty_read` / `pty_send` you'd use on any shell: read its output token-reduced, send it the next step. (The TUIs are full-screen apps, so `pty_read({ screen: true })` gives you the rendered view.) This one needs the vendor's own CLI installed and authenticated — see [Requirements](#requirements).
49
+
50
+ ```text
51
+ codex_agent({ session_name: "codex1", cwd: "/repo",
52
+ prompt: "port test/legacy.py to vitest" })
53
+ → { session_id: "codex1", … } # Codex now live in a persistent terminal
54
+ pty_read("codex1", { screen: true }) → read what it's doing (token-reduced)
55
+ pty_send("codex1", "also fix the imports it broke") → steer it, mid-task — you are driving its TUI
56
+ ```
57
+
58
+ One call per model, so the tool name itself tells you which model you get:
59
+
60
+ | Tool | Launches | Key args |
61
+ | --- | --- | --- |
62
+ | `codex_agent` | Codex CLI (OpenAI; the CLI's default model) | `prompt?`, `reasoning_effort?`, `cwd?`, `session_name?` |
63
+ | `grok_agent` | Grok Build, model `grok-build` (xAI) | `prompt?`, `reasoning_effort?` (`low`/`medium`/`high`/`xhigh`/`max`), `cwd?`, `session_name?` |
64
+ | `composer_agent` | Grok Build, model `grok-composer-2.5-fast` (xAI) | same as `grok_agent` |
65
+
66
+ The vendor CLI must be installed and authenticated (`codex` for `codex_agent`; `grok` for both Grok tools). aiterm resolves the binary via `CODEX_BIN` / `GROK_BIN`, then `~/.local/bin/codex` / `~/.grok/bin/grok`, then `PATH`. Prerequisites are checked **before** a session exists: for `grok_agent` / `composer_agent` an out-of-range `reasoning_effort` is rejected up front (the value set is fixed — `low`/`medium`/`high`/`xhigh`/`max`); a missing CLI binary or a nonexistent `cwd` fails for all three. A rejected launch — bad `reasoning_effort`, an unresolvable binary, or a nonexistent `cwd` — leaves **zero leftover session** behind, and if the launch keystroke itself can't be delivered aiterm tears the session down. (aiterm does *not* then verify the vendor CLI actually started or authenticated — `openAgent` returns once the launch command is sent; read the session to confirm the agent came up.) (`codex_agent` forwards `reasoning_effort` to the Codex CLI as a config override — `-c model_reasoning_effort=…` — rather than validating it, since Codex's accepted values vary by version.) Pass an absolute path for `cwd` — `~` is not expanded.
67
+
68
+ There is deliberately **no Claude launcher**, and no protocol between the agents: aiterm's job is to let your Claude (or any MCP client) reach for *other* agents and drive their terminals. Because a launched agent is just another persistent session, everything else in this README applies to it: token-reduced reads, completion detection, and a human `attach` to watch or take over.
69
+
36
70
  ## Demo
37
71
 
38
- Real captured output from a live session — the token reduction and completion detection are genuine, not mocked. The bracketed meta line is exactly what `pty_read` appends.
72
+ <p align="center">
73
+ <img src=".github/demo.gif" alt="aiterm-mcp demo: pty_open, a token-reduced grep read, then a nested Python REPL — all in one persistent session" width="100%">
74
+ </p>
39
75
 
40
- A noisy `git log`, read back token-reduced (458 273 tokens):
76
+ Real captured output — each block below was just run through aiterm in this repo; the numbers, the elision marker, and every `is_complete` verdict are the tool's own, not mocked. The bracketed meta line is what `pty_read` appends; its labels are Japanese in the actual output, translated here for readability (the [Japanese README](README.ja.md) shows them verbatim).
77
+
78
+ A long output folded head+tail — the middle is elided by the reducer, not by me (166 → 56 tokens):
41
79
 
42
80
  ```text
43
- → pty_send("demo", "git log --oneline -12")
81
+ → pty_send("demo", "seq 1 150")
44
82
  → pty_read("demo", { wait: true })
45
- 3ce487e (HEAD -> main, origin/main) docs(readme): lead the Why with the SSH pain …
46
- 39a9668 (tag: v0.4.0) release: v0.4.0 — nested completion early-return …
47
- c1ed87b feat(completion): early-return nested status when nested + no until …
48
- 9 more commits
49
- [aiterm demo: 13 lines / ~273 tok (raw 13 lines / ~458 tok)] [is_complete=True via quiescent]
83
+ 1
84
+ 2
85
+ 3
86
+ ⋮ (head runs to line 29 — abbreviated in this README)
87
+ ⟨102 lines elided · full=true, or line_range="A:B"⟩ … ← the tool's own marker
88
+ ⋮ (tail resumes at line 132 — abbreviated in this README)
89
+ 149
90
+ 150
91
+ [aiterm demo: 51 lines / ~56 tok (raw 152 lines / ~166 tok); 102 lines hidden] [is_complete=True via quiescent]
50
92
  ```
51
93
 
52
- A `grep`, folded by the per-command reducer to just the hits (127 46 tokens):
94
+ A `grep`, folded by the per-command reducer to a count header plus just the hits:
53
95
 
54
96
  ```text
55
97
  → pty_send("demo", "grep -rn capture-pane src/ test/")
56
98
  → pty_read("demo", { wait: true, rtk: true })
57
99
  ← 2 matches in 1 files:
58
- src/core.ts:159: // maxBuffer … capture-pane (large scrollback) …
59
- src/core.ts:329: const args = ["capture-pane", "-p", "-J", "-t", name];
60
- [aiterm demo: rtk:grep applied / ~46 tok (raw ~127 tok)] [is_complete=True via quiescent]
100
+
101
+ src/core.ts:159:// maxBuffer defaults to 1 MiB; capture-pane (large scrollback) (line truncated here)
102
+ src/core.ts:335:const args = ["capture-pane", "-p", "-J", "-t", name];
103
+ [aiterm demo: rtk:grep applied / ~46 tok (raw ~53 tok)] [is_complete=True via quiescent]
61
104
  ```
62
105
 
63
- Nesting is just text you send in — here a Python REPL *inside* the same PTY:
106
+ Nesting is just text you send in — here a Python REPL *inside* the same PTY (an `ssh host`, a `docker exec -it … bash`, or a launched coding-agent TUI nests exactly the same way):
64
107
 
65
108
  ```text
66
109
  → pty_send("demo", "python3")
67
- → pty_read("demo", { until: ">>> " }) # nested prompt = "the inner shell is ready"
110
+ → pty_read("demo", { until: ">>>" }) # nested prompt = "the inner shell is ready"
68
111
  → pty_send("demo", "print(sum(range(1_000_000)))")
69
- → pty_read("demo", { until: ">>> " })
70
- ← 499999500000 [is_complete=True via until]
112
+ → pty_read("demo", { wait: true, until: ">>>" })
113
+ ← 499999500000 [is_complete=True via until]
71
114
  ```
72
115
 
73
- `ssh host` and `docker exec -it bash` nest exactly the same way (see [Why](#why)) an animated GIF of the full SSH flow is on the way; everything above is real output, not a script. While nested, pass `until` (the inner prompt) or `mark: true`, because quiescence cannot fire there by design — see [Completion detection](#completion-detection-4-layers) and [Known constraints](#known-constraints-by-design-not-bugs). A human can `attach` to the same tmux socket and watch any of this live (see [A human can watch](#a-human-can-watch)).
116
+ The only edits to the captures above are the two `⋮` lines (a long head/tail run abbreviated for the README) and one over-long grep line truncated to fit — the `⟨…⟩` marker, the token counts, and every `is_complete` verdict are exactly what the tool printed. (Use `until: ">>>"` without a trailing space the captured prompt is trimmed, so `">>> "` would miss and fall through to `timeout`.) While nested, pass `until` (the inner prompt) or `mark: true`, because quiescence cannot fire there by design — see [Completion detection](#completion-detection-5-layers) and [Known constraints](#known-constraints-by-design-not-bugs). A human can `attach` to the same tmux socket and watch any of this live (see [A human can watch](#a-human-can-watch)).
74
117
 
75
118
  ## Quickstart (≈60 seconds)
76
119
 
@@ -95,7 +138,7 @@ pty_read("t1", { wait: true }) → "hello" (token-reduced, completion det
95
138
  pty_close("t1") → terminal released
96
139
  ```
97
140
 
98
- That's it. The terminal in `t1` is real and persistent — `ssh`, `docker exec`, a REPL are just text you `pty_send` into it.
141
+ That's it. The terminal in `t1` is real and persistent — `ssh`, `docker exec`, a REPL, or a launched agent's TUI are just things that live inside it. To launch a worker agent instead, one call does it: `codex_agent()` returns a `session_id` you drive with the same `pty_read` / `pty_send`.
99
142
 
100
143
  **Prefer a global install, or a different client?**
101
144
 
@@ -105,37 +148,58 @@ npm i -g aiterm-mcp
105
148
  claude mcp add --scope user --transport stdio aiterm -- aiterm-mcp
106
149
  ```
107
150
 
108
- This registers it in `~/.claude.json`; you'll get an approval prompt the first time. **Any other MCP client** (Cursor, Cline, Claude Desktop, …) works too — just launch `npx -y aiterm-mcp` (or `aiterm-mcp`) over stdio. Needs **Node ≥ 18** and **tmux** — see [Requirements](#requirements).
151
+ This registers it in `~/.claude.json`; you'll get an approval prompt the first time. **Any other MCP client** (Cursor, Cline, Claude Desktop, …) should work too — just launch `npx -y aiterm-mcp` (or `aiterm-mcp`) over stdio. Needs **Node ≥ 18** and **tmux** — see [Requirements](#requirements).
152
+
153
+ ## Headless: no human at the terminal
154
+
155
+ Because an MCP client drives aiterm programmatically over stdio, everything above can run with **nobody sitting at a tmux**. Your Claude Code session can `codex_agent()` a task, `pty_read` the result, and act on it — unattended. That makes aiterm a fit for exactly the places a human-driven terminal isn't:
156
+
157
+ - **Multi-agent orchestration** — an orchestrator hands sub-tasks to Codex / Grok / Composer, each in its own persistent session, and reads them all back.
158
+ - **CI** — a job step can spin up an agent, drive it, and tear it down.
159
+ - **cron** — a scheduled run can launch an agent and collect its output.
160
+
161
+ The terminal is real and shared, so a human *can* jump in ([A human can watch](#a-human-can-watch)) — but nothing requires one to.
109
162
 
110
163
  ## How it works
111
164
 
112
165
  ```mermaid
113
166
  flowchart LR
114
- AI["AI / MCP client"] -->|"pty_send"| S["aiterm-mcp<br/>stdio MCP · 9 tools"]
167
+ AI["AI / MCP client<br/>(the orchestrator)"] -->|"pty_send · codex_agent<br/>grok_agent · composer_agent"| S["aiterm-mcp<br/>stdio MCP · 9 tools"]
115
168
  S -->|"pty_read<br/>token-reduced"| AI
116
- S -->|"tmux send-keys<br/>capture-pane"| P["one local PTY<br/>tmux · persistent"]
169
+ S -->|"tmux send-keys<br/>capture-pane"| P["persistent PTYs<br/>tmux · survive restarts"]
117
170
  P -->|"ssh · docker · repl"| R["nested<br/>remote · container · REPL"]
171
+ P -->|"launches a fresh PTY per agent"| A["another coding-agent TUI<br/>Codex · Grok · Composer"]
118
172
  ```
119
173
 
120
- One PTY is the only primitive. Everything else — SSH, containers, REPLs — is just text you `pty_send` into it. Because the PTY lives in tmux, sessions outlive the MCP server and the AI client.
174
+ One PTY is the only primitive. Everything else — SSH, containers, REPLs, and the launched agent TUIs — is just something interactive running inside a persistent terminal, driven with the same `pty_send` / `pty_read`. Each launcher opens its own fresh PTY. Because the PTYs live in tmux, sessions outlive the MCP server and the AI client.
121
175
 
122
176
  ## vs. the alternatives
123
177
 
124
- | | **aiterm-mcp** | one-shot shell MCP<br/>(e.g. `mcp-server-commands`) | terminal / SSH / tmux MCPs<br/>(e.g. `iterm-mcp`, `ssh-mcp`, `tmux-mcp`) |
125
- | --- | --- | --- | --- |
126
- | Persistent session | ✅ tmux, survives restarts | ❌ new shell every call | ⚠️ varies |
127
- | SSH / containers | nest with one `pty_send` | reconnect every command | ⚠️ often separate tools / per-call connect |
128
- | Token-reduced reads | ✅ per-command reducers | ❌ raw output | ⚠️ rarely |
129
- | Completion detection | 4-layer: exit / `until` / quiescence / timeout | n/a (blocks per call) | ⚠️ prompt-match, fragile |
130
- | Human can co-drive | ✅ shared tmux socket (`attach`) | ❌ | ⚠️ varies |
178
+ aiterm sits at the intersection of two families: terminal-driving MCP servers, and the newer "agents talk to each other through a shared terminal" idea (see [Where aiterm fits](#where-aiterm-fits)). Here's how the axes line up honestly, including where the others are strong.
131
179
 
132
- ## Requirements
180
+ | | **aiterm-mcp** | one-shot shell MCP<br/>(e.g. `mcp-server-commands`) | terminal / SSH / tmux MCPs<br/>(e.g. `iterm-mcp`, `ssh-mcp`, `tmux-mcp`) | shared-tmux agent-to-agent<br/>(e.g. `smux`) |
181
+ | --- | --- | --- | --- | --- |
182
+ | Persistent session | ✅ tmux, survives restarts | ❌ new shell every call | ⚠️ varies | ✅ tmux |
183
+ | SSH / containers / REPLs | nest with one `pty_send` | reconnect every command | ⚠️ often separate tools | ✅ tmux (human drives) |
184
+ | Launch another agent in one call | ✅ `codex_agent` / `grok_agent` / `composer_agent` | ❌ | ❌ | ⚠️ agents join a human-run tmux via a CLI + skills |
185
+ | Headless (no human at a tmux) | ✅ MCP-driven, programmatic | ✅ | ⚠️ varies | ❌ built around a human in the tmux |
186
+ | MCP-native (any MCP client) | ✅ one `claude mcp add` | ✅ | ✅ (they are MCPs) | ❌ tmux config + CLI + Agent Skills |
187
+ | Token-reduced reads | ✅ per-command reducers | ❌ raw output | ⚠️ rarely | ❌ raw tmux |
188
+ | Completion detection | 5-layer: exit / `mark` / `until` / quiescence / timeout | n/a (blocks per call) | ⚠️ prompt-match, fragile | ❌ agent reads the pane |
189
+ | Destructive-command gate | ✅ tripwire (override with `force`) | ❌ | ⚠️ varies | ❌ |
190
+ | Human can co-drive | ✅ shared tmux socket (`attach`) | ❌ | ⚠️ varies | ✅ (its core model) |
133
191
 
134
- - **Node.js >= 18**
135
- - **tmux** (runtime prerequisite; check with `tmux -V`. Install with `apt install tmux` / `brew install tmux`)
136
- - **macOS / Linux / WSL2** run tmux directly. On macOS install it with `brew install tmux` (stock macOS ships none). If your MCP client is launched from the **GUI** rather than a terminal, Homebrew's bin (`/opt/homebrew/bin` on Apple Silicon, `/usr/local/bin` on Intel) may be off its `PATH`; aiterm auto-searches those locations, or set **`AITERM_TMUX=/path/to/tmux`** to point at it explicitly.
137
- - **Native Windows** has no tmux, so aiterm transparently runs tmux **inside WSL**. It needs [WSL](https://learn.microsoft.com/windows/wsl/) installed and initialized, with **tmux installed inside your WSL distro** (`sudo apt install tmux`); verify with `wsl tmux -V`. Sessions, the socket, and human `attach` all live on the WSL side — the AI just drives them from the Windows-side command. (You reach Windows tools the same way you reach SSH: `pty_send "powershell.exe …"` nests into PowerShell.)
138
- - Optional: the [`rtk`](https://github.com/rtk-ai/rtk) binary (used by `pty_send`'s `rtk: true` delegation; works fine without it)
192
+ ## Where aiterm fits
193
+
194
+ "AIs talking to each other through a shared terminal" is becoming its own category and it's a genuinely good idea. The terminal is a universal interface every coding agent already speaks, so no bespoke agent-to-agent protocol is needed; the shell *is* the shared surface. `smux` (by @shawn_pana) popularized this framing as a one-command shared tmux environment a human sets up, that agents then join via a `tmux-bridge` CLI and Agent Skills. It's good at the in-the-loop, shared-pane workflow it's built for, and it has real traction.
195
+
196
+ aiterm takes the same core insight the terminal as the meeting point and makes three deliberate, different choices:
197
+
198
+ 1. **Headless by construction.** Because aiterm is driven programmatically over MCP, an AI can launch and drive another agent with *no human sitting in the tmux* — from an orchestration loop, a CI step, or a cron job. The shared-tmux tools lead with a human at the keyboard (their docs center on interactive pane navigation), so unattended operation isn't their native mode; aiterm's is.
199
+ 2. **MCP-native, not a workflow you adopt.** aiterm is a stdio MCP server: one `claude mcp add` line and it works as structured tools in any MCP client that speaks stdio (tested in Claude Code; Cursor, Cline, and Claude Desktop speak the same protocol and should work the same way). It doesn't ask you to adopt a tmux config, learn pane navigation, or install skills into your setup — the client already knows how to call tools.
200
+ 3. **Launching an agent is one tool call — an orchestration primitive.** `codex_agent()` spawns Codex in a persistent terminal and returns a session you drive immediately. You don't arrange panes or paste between them by hand; the launch, the steering, and the reads are all tool calls the orchestrating model can make on its own.
201
+
202
+ On top of that sits a productized layer a raw tmux bridge doesn't have: **token-reduced reads**, **5-layer completion detection**, and a **destructive-command tripwire**. None of this makes the human-in-the-tmux model wrong — it's a different, complementary bet on where the human is standing.
139
203
 
140
204
  ## Tools
141
205
 
@@ -143,14 +207,14 @@ One PTY is the only primitive. Everything else — SSH, containers, REPLs — is
143
207
  | --- | --- | --- |
144
208
  | `pty_open` | Grab one terminal, return a `session_id` | `name?`, `shell="bash"` |
145
209
  | `pty_send` | Send text (a command) | `session_id`, `text`, `enter=true`, `mark`, `force`, `rtk`, `raw` |
146
- | `pty_read` | Read output, token-reduced (incremental by default) | `session_id`, `wait`, `until`, `timeout`, `screen`, `full`, `lines`, `line_range`, `raw`, `rtk` |
210
+ | `pty_read` | Read output, token-reduced (incremental by default) | `session_id`, `wait`, `until`, `until_regex`, `timeout`, `screen`, `full`, `lines`, `line_range`, `raw`, `rtk` |
147
211
  | `pty_key` | Send a control key | `session_id`, `key` (`C-c`/`Enter`/`Up`…) |
148
212
  | `pty_close` | Close a session | `session_id` |
149
213
  | `pty_list` | List sessions | (none) |
150
214
 
151
215
  ### Interactive agent launchers
152
216
 
153
- Each launcher starts a specific vendor's interactive coding-agent TUI inside a fresh persistent PTY and returns its `session_id` — from there you drive it with plain `pty_read` / `pty_send`, exactly like any other session. One tool per model, so the tool name itself tells you which model you get.
217
+ Each launcher starts a specific vendor's interactive coding-agent TUI inside a fresh persistent PTY and returns its `session_id` — from there you drive it with plain `pty_read` / `pty_send`, exactly like any other session. One tool per model, so the tool name itself tells you which model you get. The TUI is a full-screen app, so read it with `pty_read({ screen: true })` for the rendered view.
154
218
 
155
219
  | Tool | Launches | Key args |
156
220
  | --- | --- | --- |
@@ -158,11 +222,11 @@ Each launcher starts a specific vendor's interactive coding-agent TUI inside a f
158
222
  | `grok_agent` | Grok Build, model `grok-build` (xAI) | `prompt?`, `reasoning_effort?` (`low`/`medium`/`high`/`xhigh`/`max`), `cwd?`, `session_name?` |
159
223
  | `composer_agent` | Grok Build, model `grok-composer-2.5-fast` (xAI) | same as `grok_agent` |
160
224
 
161
- The vendor CLI must be installed and authenticated (`codex` for `codex_agent`; `grok` for both Grok tools). aiterm resolves the binary via `CODEX_BIN` / `GROK_BIN`, then `~/.local/bin/codex` / `~/.grok/bin/grok`, then `PATH`. All prerequisites are validated **before** a session is created an invalid `reasoning_effort`, a missing CLI, or a nonexistent `cwd` fails with an explicit error and leaves no session behind.
225
+ The vendor CLI must be installed and authenticated (`codex` for `codex_agent`; `grok` for both Grok tools). aiterm resolves the binary via `CODEX_BIN` / `GROK_BIN`, then `~/.local/bin/codex` / `~/.grok/bin/grok`, then `PATH`. Prerequisites are validated before a session is created (grok/composer reject an out-of-range `reasoning_effort`; a missing CLI or a nonexistent `cwd` fails for all three), and a failed launch leaves no session behind. Full details under [Launch other coding agents into that terminal](#2-launch-other-coding-agents-into-that-terminal--the-orchestration-flagship). Pass an absolute path for `cwd` — `~` is not expanded.
162
226
 
163
- ### Completion detection (4 layers)
227
+ ### Completion detection (5 layers)
164
228
 
165
- `pty_read({ wait: true })` decides "is the command done?" via four layers: process exit / `until` regex match / output is quiescent ∧ the shell is back (quiescence) / timeout. While nested (inside SSH), the "shell is back" check cannot fire, so pass `until` with the remote prompt for a clean decision.
229
+ `pty_read({ wait: true })` decides "is the command done?" via five layers: process exit / a `mark:true` sentinel (auto-detected — see below) / an `until` match (a literal substring by default; pass `until_regex: true` for a regex) / output is quiescent ∧ the shell is back (quiescence) / timeout. While nested (inside SSH, a container, a REPL, or a launched agent's TUI), the "shell is back" check cannot fire, so pass `until` with the inner prompt — or send with `mark: true` and `pty_read({ wait: true })` auto-detects the completion sentinel (no `until` needed, works nested too) — or, for a full-screen agent TUI, read `{ screen: true }` once its output settles.
166
230
 
167
231
  ### Token reduction
168
232
 
@@ -172,11 +236,31 @@ The vendor CLI must be installed and authenticated (`codex` for `codex_agent`; `
172
236
 
173
237
  ### Safety
174
238
 
175
- Before sending, `pty_send` blocks destructive commands (`rm -rf /`, `mkfs`, `dd of=/dev/…`, `DROP TABLE`, …) — pass `force: true` to override — and sanitizes ESC / bracketed-paste terminators. `pty_read` neutralizes control characters in what it returns.
239
+ Before sending, `pty_send` blocks destructive commands (`rm -rf /`, `mkfs`, `dd of=/dev/…`, `DROP TABLE`, …) — pass `force: true` to override — and sanitizes ESC / bracketed-paste terminators. `pty_read` neutralizes control characters in what it returns by default (`raw: true` returns the bytes verbatim). This is a **tripwire, not a sandbox** (see [Known constraints](#known-constraints-by-design-not-bugs)).
176
240
 
177
241
  ## A human can watch
178
242
 
179
- Sessions live on a shared tmux socket. The `tmux -S … attach -t <id>` line printed by `pty_open` lets a human attach to the same terminal and intervene (`Ctrl-b d` to detach). On native Windows the printed line is the WSL form — `wsl tmux -S … attach -t <id>` — since the session lives inside WSL.
243
+ Sessions live on a shared tmux socket. The `tmux -S … attach -t <id>` line printed by `pty_open` (and by each agent launcher) lets a human attach to the same terminal and intervene (`Ctrl-b d` to detach) — including watching a launched Codex/Grok/Composer session run and taking the keyboard from your AI mid-task. On native Windows the printed line is the WSL form — `wsl tmux -S … attach -t <id>` — since the session lives inside WSL.
244
+
245
+ ## Requirements
246
+
247
+ - **Node.js >= 18**
248
+ - **tmux** (runtime prerequisite; check with `tmux -V`. Install with `apt install tmux` / `brew install tmux`)
249
+ - **macOS / Linux / WSL2** run tmux directly. On macOS install it with `brew install tmux` (stock macOS ships none). If your MCP client is launched from the **GUI** rather than a terminal, Homebrew's bin (`/opt/homebrew/bin` on Apple Silicon, `/usr/local/bin` on Intel) may be off its `PATH`; aiterm auto-searches those locations, or set **`AITERM_TMUX=/path/to/tmux`** to point at it explicitly.
250
+ - **Native Windows** has no tmux, so aiterm transparently runs tmux **inside WSL**. It needs [WSL](https://learn.microsoft.com/windows/wsl/) installed and initialized, with **tmux installed inside your WSL distro** (`sudo apt install tmux`); verify with `wsl tmux -V`. Sessions, the socket, and human `attach` all live on the WSL side — the AI just drives them from the Windows-side command. (You reach Windows tools the same way you reach SSH: `pty_send "powershell.exe …"` nests into PowerShell.)
251
+ - For the **agent launchers**: the corresponding vendor CLI, installed and authenticated — `codex` for `codex_agent`, `grok` for `grok_agent` / `composer_agent`. (Not needed if you only use the PTY tools.)
252
+ - Optional: the [`rtk`](https://github.com/rtk-ai/rtk) binary (used by `pty_send`'s `rtk: true` delegation; works fine without it)
253
+
254
+ ## Known constraints (by design, not bugs)
255
+
256
+ - **While nested (ssh / docker / REPL / a launched agent TUI), quiescence cannot fire by design**, because the foreground command is no longer in the shell set (bash/sh/zsh/fish/dash). When nested with no `until` and no `mark`, `pty_read({ wait: true })` returns early as `is_complete=False via nested` (rather than burning the full `timeout`, since no signal can confirm completion there) with a note to pass `until` (a literal substring by default; `until_regex: true` for a regex) or `mark: true` (an exit-code sentinel, auto-detected) for a confirmed completion. For a full-screen agent TUI, read `{ screen: true }` once its output settles.
257
+ - **`is_complete=False` is not a failure.** It means "completion was not observed within `timeout`." For long commands, raise `timeout` or use `until`/`mark`.
258
+ - **The destructive gate is a tripwire, not a sandbox.** It blocks common destructive forms only. It does **not** catch relative-path `rm`, things that become dangerous after `$VAR` expansion, or commands run on the far side of an SSH session — and it does not police what a launched coding agent does inside its own session.
259
+ - **The agent launchers spawn a vendor TUI; they don't wrap or proxy it.** aiterm validates prerequisites and starts the CLI in a persistent PTY — the model, auth, and behavior are the vendor CLI's. There is no `claude` launcher and no protocol between agents; "conversation" is your MCP client driving the TUI (send input, read output).
260
+ - **`pty_send({ rtk: true })` is single-line only and needs the external `rtk` binary** (passthrough without it). The `pty_read({ rtk: true })` reducer, by contrast, is self-contained and rtk-independent.
261
+ - **The `pytest` reducer matches rtk 0.42.0** on test counts, the rule line, and `FAILURES`-block formatting (locked by regression tests). It **deliberately preserves the full failure reason** on the `FAILED` summary lines (emitted under `-ra`/`-rf`), whereas rtk 0.42.0 truncates the reason at the first `" - "` — a readability choice, so those lines are intentionally not byte-identical to rtk. The `[full output: …]` tee-pointer line rtk appends on large output is not reproduced on the read side.
262
+ - **tmux is started with `-f /dev/null`**, so it does not read `~/.tmux.conf` (to keep behavior reproducible across machines).
263
+ - **All sessions live on a single socket (`claude.sock` on POSIX).** `tmux … kill-server` removes them all.
180
264
 
181
265
  ## Development
182
266
 
@@ -187,17 +271,7 @@ npm test # build, then the node:test regression suite (requires tmux)
187
271
  npm link # put `aiterm-mcp` on PATH locally
188
272
  ```
189
273
 
190
- Logic lives in `src/core.ts` (tmux control, reduction, completion detection, safety) and `src/rtk.ts` (per-command reducers); `src/index.ts` is the MCP surface. The design origin and the reducer's porting source (the pytest reducer is ported to be byte-exact with upstream rtk 0.42.0, locked by regression tests) are in `prototype/python/`.
191
-
192
- ## Known constraints (by design, not bugs)
193
-
194
- - **While nested (ssh / docker / REPL), quiescence cannot fire by design**, because the foreground command is no longer in the shell set (bash/sh/zsh/fish/dash). When nested with no `until`, `pty_read({ wait: true })` returns early as `is_complete=False via nested` (rather than burning the full `timeout`, since no signal can confirm completion there) with a note to pass `until` (a regex for the prompt) or `mark: true` (an exit-code sentinel) for a confirmed completion.
195
- - **`is_complete=False` is not a failure.** It means "completion was not observed within `timeout`." For long commands, raise `timeout` or use `until`/`mark`.
196
- - **The destructive gate is a tripwire, not a sandbox.** It blocks common destructive forms only. It does **not** catch relative-path `rm`, things that become dangerous after `$VAR` expansion, or commands run on the far side of an SSH session.
197
- - **`pty_send({ rtk: true })` is single-line only and needs the external `rtk` binary** (passthrough without it). The `pty_read({ rtk: true })` reducer, by contrast, is self-contained and rtk-independent.
198
- - **The `pytest` reducer matches rtk 0.42.0** on test counts, the rule line, and `FAILURES`-block formatting (locked by regression tests). It **deliberately preserves the full failure reason** on the `FAILED` summary lines (emitted under `-ra`/`-rf`), whereas rtk 0.42.0 truncates the reason at the first `" - "` — a readability choice, so those lines are intentionally not byte-identical to rtk. The `[full output: …]` tee-pointer line rtk appends on large output is not reproduced on the read side.
199
- - **tmux is started with `-f /dev/null`**, so it does not read `~/.tmux.conf` (to keep behavior reproducible across machines).
200
- - **All sessions live on a single socket (`claude.sock`).** `tmux … kill-server` removes them all.
274
+ Logic lives in `src/core.ts` (tmux control, reduction, completion detection, safety, agent launch) and `src/rtk.ts` (per-command reducers); `src/index.ts` is the MCP surface. The design origin and the reducer's porting source (the pytest reducer is ported to match upstream rtk 0.42.0, except the deliberate `FAILED`-line difference noted above, and is locked by regression tests) are in `prototype/python/`.
201
275
 
202
276
  ## Try it
203
277
 
@@ -207,7 +281,7 @@ One command, no clone, no build:
207
281
  claude mcp add --scope user --transport stdio aiterm -- npx -y aiterm-mcp
208
282
  ```
209
283
 
210
- If aiterm saved you a round-trip of tokens, **[star the repo](https://github.com/kitepon-rgb/aiterm-mcp)** — it's the cheapest way to help others find it.
284
+ If aiterm let your AI hand a task to another agent — or saved you a round-trip of tokens **[star the repo](https://github.com/kitepon-rgb/aiterm-mcp)**. It's the cheapest way to help others find it.
211
285
 
212
286
  - **npm:** https://www.npmjs.com/package/aiterm-mcp
213
287
  - **Issues / bug reports:** https://github.com/kitepon-rgb/aiterm-mcp/issues