aiterm-mcp 0.4.0 → 0.4.1

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.
Files changed (3) hide show
  1. package/README.ja.md +73 -88
  2. package/README.md +73 -88
  3. package/package.json +7 -2
package/README.ja.md CHANGED
@@ -8,15 +8,70 @@
8
8
  [![npm](https://img.shields.io/npm/v/aiterm-mcp.svg)](https://www.npmjs.com/package/aiterm-mcp)
9
9
  [![node](https://img.shields.io/node/v/aiterm-mcp)](https://nodejs.org)
10
10
  [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
11
- [![npm downloads](https://img.shields.io/npm/dm/aiterm-mcp.svg)](https://www.npmjs.com/package/aiterm-mcp)
12
11
  [![install size](https://packagephobia.com/badge?p=aiterm-mcp)](https://packagephobia.com/result?p=aiterm-mcp)
13
12
 
14
13
  > *(English: [README.md](README.md))*
15
14
 
16
- > AI が握る**ローカル永続端末**を stdio MCP サーバとして公開する。1 個のローカル端末を握り、SSH もコンテナも「その端末に打つ 1 コマンド」に格下げする。読み取りはトークン削減つき。
15
+ > **Claude(や任意の MCP クライアント)に、本物の永続シェルを握らせる。** 端末は 1 個を開きっぱなしにし、`ssh`・`docker exec`・REPL は「その中へ送るただのテキスト」。だから AI はコマンドごとに接続し直さずに済む。読み取りはトークン削減つきで返る。
16
+ >
17
+ > *MCP = Model Context Protocol — Claude Code のようなツールが AI に機能を差し込むためのオープン標準。*
17
18
 
18
19
  `pty_open` / `pty_send` / `pty_read` / `pty_key` / `pty_close` / `pty_list` の 6 ツールだけ。バックエンドは tmux なので、MCP サーバや AI クライアントが再起動してもセッションは生き残る。
19
20
 
21
+ **状態:** 開発継続中 · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
22
+
23
+ ## なぜ
24
+
25
+ AI にコマンドを 1 個ずつ投げる方式だと、SSH では 1 コマンドごとに「接続→認証→切断」になる。痛みは 3 つ — **毎回再認証**(鍵のパスフレーズもワンタイムコードも毎回)、**短命なセッションが次々増殖**、そして接続が立て込むと**自分の防御に締め出される**(`fail2ban` で BAN、`MaxStartups`/`MaxSessions` で拒否、アカウントロック)。攻撃者を止めるための仕組みに、自分が締め出される。(自宅サーバで実際にやられた。この再認証地獄なしに自宅環境を Claude Code から操作したくて aiterm を作った。)
26
+
27
+ aiterm は **1 個の PTY を永続的に握り**、その中へ一度だけ `ssh host`(や `docker exec -it x bash`)で入る。以降のコマンドは同じ認証済みセッションを通る — **認証は 1 回、セッションは 1 本、防御に引っかかる隙が無い**。セッション種別をツールで区別しない。
28
+
29
+ ```
30
+ pty_open() → ローカル端末を 1 個握る
31
+ pty_send(id, "ssh 192.168.1.2") → その端末の中で一度だけ認証して入る
32
+ pty_send(id, "uname -a") → 以降のコマンドは同じセッションを通る
33
+ pty_read(id, { wait: true }) → 削減済みの出力を読む
34
+ ```
35
+
36
+ ## デモ
37
+
38
+ 実際のライブセッションから採取した本物の出力 — トークン削減も完了検出も実物で、モックではない。角括弧のメタ行は `pty_read` が実際に付けるもの。
39
+
40
+ ノイズの多い `git log` を、トークン削減して読む(458 → 273 トークン):
41
+
42
+ ```text
43
+ → pty_send("demo", "git log --oneline -12")
44
+ → 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 コミット …
49
+ [aiterm demo: 13 行 / ~273 tok (raw 13 行 / ~458 tok)] [is_complete=True via quiescent]
50
+ ```
51
+
52
+ `grep` を、コマンド別 reducer でヒット行だけに畳む(127 → 46 トークン):
53
+
54
+ ```text
55
+ → pty_send("demo", "grep -rn capture-pane src/ test/")
56
+ → pty_read("demo", { wait: true, rtk: true })
57
+ ← 2 matches in 1 files:
58
+ src/core.ts:159: // maxBuffer … capture-pane(大きなスクロールバック)…
59
+ src/core.ts:329: const args = ["capture-pane", "-p", "-J", "-t", name];
60
+ [aiterm demo: rtk:grep 適用 / ~46 tok (raw ~127 tok)] [is_complete=True via quiescent]
61
+ ```
62
+
63
+ ネストは「中へ送るただのテキスト」 — 同じ PTY の*中で* Python REPL に入る:
64
+
65
+ ```text
66
+ → pty_send("demo", "python3")
67
+ → pty_read("demo", { until: ">>> " }) # 入れ子側のプロンプト = 「内側シェルが応答できる」
68
+ → pty_send("demo", "print(sum(range(1_000_000)))")
69
+ → pty_read("demo", { until: ">>> " })
70
+ ← 499999500000 [is_complete=True via until]
71
+ ```
72
+
73
+ `ssh host` や `docker exec -it … bash` もまったく同じ要領でネストする([なぜ](#なぜ))。SSH 全行程の動く GIF は準備中だが、上はすべて本物の出力で、台本ではない。ネスト中は `until`(内側プロンプト)か `mark: true` を渡すこと — そこでは quiescence が原理的に効かないため([完了検出](#完了検出4-層) / [既知の制約](#既知の制約バグではなく仕様))。同じ tmux ソケットに人が `attach` すれば、これらをライブで覗ける([人が覗く](#人が覗く))。
74
+
20
75
  ## クイックスタート(約60秒)
21
76
 
22
77
  Claude Code なら 1 コマンドで登録 — clone もビルドも不要、`npx` が毎回取得して起動する:
@@ -40,70 +95,17 @@ pty_read("t1", { wait: true }) → "hello" (トークン削減・完了
40
95
  pty_close("t1") → 端末を解放
41
96
  ```
42
97
 
43
- これだけ。`t1` の端末は本物で永続 — `ssh`・`docker exec`・REPL は、そこへ `pty_send` で打ち込む“ただのテキスト”([なぜ](#なぜ))。インストールも不要にしたいなら、どの MCP クライアントからでも stdio で `npx -y aiterm-mcp` を起動するだけ。
44
-
45
- ## インストール
98
+ これだけ。`t1` の端末は本物で永続 — `ssh`・`docker exec`・REPL は、そこへ `pty_send` で打ち込む“ただのテキスト”。
46
99
 
47
- npm に公開済み。clone もビルドも不要:
100
+ **グローバル導入や別クライアントが良い場合は:**
48
101
 
49
102
  ```bash
50
- # Claude Code — 推奨(インストール不要、npx が毎回取得して起動)
51
- claude mcp add --scope user --transport stdio aiterm -- npx -y aiterm-mcp
52
-
53
- # またはグローバル導入してコマンド名で登録
103
+ # グローバル導入してコマンド名で登録
54
104
  npm i -g aiterm-mcp
55
105
  claude mcp add --scope user --transport stdio aiterm -- aiterm-mcp
56
106
  ```
57
107
 
58
- **Node 18** **tmux** が必要(Windows ネイティブでは WSL とその中の tmux。下の「要件」参照)。他の MCP クライアントは stdio で `npx -y aiterm-mcp` を起動するだけ(詳細は下の「インストール / 登録」)。
59
-
60
- ## なぜ
61
-
62
- AI にコマンドを 1 個ずつ投げて結果を受け取る往復は、SSH では毎回「接続→認証→切断」を繰り返し遅く、トークンも食う。aiterm は **1 個の PTY を永続的に握り**、その中で `ssh host` や `docker exec -it x bash` と打って入る(ネスト)。セッション種別をツールで区別しない。
63
-
64
- ```
65
- pty_open() → ローカル端末を 1 個握る
66
- pty_send(id, "ssh 192.168.1.2") → その端末の中で SSH に入る
67
- pty_send(id, "uname -a") → リモートで実行
68
- pty_read(id, { wait: true }) → 削減済みの出力を読む
69
- ```
70
-
71
- ## デモ
72
-
73
- <!-- demo gif: drop docs/demo.gif here (asciinema cast or animated GIF of the flow below) -->
74
-
75
- 決め手の流れ: PTY を 1 個開き、その**中で** SSH にネストし、リモートでコマンドを実行し、**すでにトークン削減された**出力を読む — コマンドごとの接続し直しは無い。
76
-
77
- ```text
78
- # 1 — ローカル端末を 1 個握る(tmux 上・再起動を跨ぐ)
79
- → pty_open()
80
- ← { session_id: "t1", attach: "tmux -S /…/claude.sock attach -t t1" }
81
-
82
- # 2 — その同じ端末の中で SSH にネスト(別ツールではない)
83
- → pty_send("t1", "ssh 192.168.1.2")
84
- ← sent
85
- → pty_read("t1", { until: "\\$ $" }) # リモートのプロンプト = 「シェルに戻った」
86
- ← user@remote:~$
87
-
88
- # 3 — 同じ PTY のまま、リモートでコマンド実行(接続し直し無し)
89
- → pty_send("t1", "uname -a")
90
- → pty_read("t1", { until: "\\$ $" })
91
- ← Linux remote 6.1.0 #1 SMP x86_64 GNU/Linux
92
-
93
- # 4 — ノイズの多いコマンドを、コマンド別 reducer で読む
94
- → pty_send("t1", "git status")
95
- → pty_read("t1", { until: "\\$ $", rtk: true }) # 自前実装・rtk バイナリ不要
96
- ← ## main…origin/main [ahead 1]
97
- M src/core.ts
98
- ?? notes.txt
99
- [reduced: 制御文字除去 · 重複圧縮 · git-status reducer]
100
- ```
101
-
102
- この流れが「デモのための嘘」にならない理由:
103
-
104
- - ステップ 2/3 が **`until`**(リモートのプロンプト)を使うのは、**ネスト中は quiescence が原理的に効かない**ため([完了検出](#完了検出4-層) / [既知の制約](#既知の制約バグではなく仕様))。ローカルシェルなら `{ wait: true }` だけで足りるが、ネスト中は `until`(または `mark: true`)が要る。
105
- - 角括弧の `[reduced: …]` 行は `pty_read` が付けるメタ/復元ヒントの例示で、実際の文言は出力に応じて変わる。reducer は **自前実装**の `pty_read({ rtk: true })` 経路で、外部 `rtk` バイナリは不要。
106
- - `t1` のソケットに人が `attach` すれば、同じ SSH セッションをライブで覗ける([人が覗く](#人が覗く))。
108
+ `~/.claude.json` に登録され、初回に承認プロンプトが出る。**他の MCP クライアント**(Cursor / Cline / Claude Desktop …)でも、stdio で `npx -y aiterm-mcp`(または `aiterm-mcp`)を起動するだけ。**Node 18** と **tmux** が必要 — [要件](#要件)参照。
107
109
 
108
110
  ## 仕組み
109
111
 
@@ -119,10 +121,10 @@ flowchart LR
119
121
 
120
122
  ## 既存手段との比較
121
123
 
122
- | | **aiterm-mcp** | 1 コマンド毎の往復 | 一般的な terminal / tmux MCP |
124
+ | | **aiterm-mcp** | 1 コマンド毎の往復<br/>(例: `mcp-server-commands`) | terminal / SSH / tmux MCP<br/>(例: `iterm-mcp`, `ssh-mcp`, `tmux-mcp`) |
123
125
  | --- | --- | --- | --- |
124
126
  | 永続セッション | ✅ tmux・再起動を跨ぐ | ❌ 毎回新シェル | ⚠️ まちまち |
125
- | SSH / コンテナ | `pty_send` 1 回でネスト | 毎コマンド接続し直し | ⚠️ ツールが分かれがち |
127
+ | SSH / コンテナ | `pty_send` 1 回でネスト | 毎コマンド接続し直し | ⚠️ ツールが分かれる/毎回接続しがち |
126
128
  | トークン削減読取 | ✅ コマンド別 reducer | ❌ 生出力 | ⚠️ ほぼ無し |
127
129
  | 完了検出 | 4 層: 終了 / `until` / 静止 / timeout | 無し(毎回ブロック) | ⚠️ プロンプト一致・脆い |
128
130
  | 人が同時操作 | ✅ 共有 tmux ソケット | ❌ | ⚠️ まちまち |
@@ -135,23 +137,6 @@ flowchart LR
135
137
  - **Windows ネイティブ**には tmux が無いため、aiterm は裏で **WSL の中の tmux** を透過的に使う。[WSL](https://learn.microsoft.com/ja-jp/windows/wsl/) を導入・初期化し、**WSL のディストリ内に tmux を入れる**こと(`sudo apt install tmux`)。`wsl tmux -V` で確認できる。セッション・ソケット・人の `attach` はすべて WSL 側にあり、AI は Windows 側のコマンドから操作するだけ。(Windows のツールは SSH と同じく入れ子で握る: `pty_send "powershell.exe …"` で PowerShell に入る。)
136
138
  - 任意: [`rtk`](https://github.com/rtk-ai/rtk) バイナリ(`pty_send` の `rtk: true` 委譲で使う。無くても動く)
137
139
 
138
- ## インストール / 登録
139
-
140
- Claude Code(CLI)にユーザースコープ(全プロジェクトで利用可)で登録する例:
141
-
142
- ```bash
143
- # 推奨: インストール不要、npx が毎回取得して起動
144
- claude mcp add --scope user --transport stdio aiterm -- npx -y aiterm-mcp
145
-
146
- # またはグローバルインストールしてコマンド名で
147
- npm i -g aiterm-mcp
148
- claude mcp add --scope user --transport stdio aiterm -- aiterm-mcp
149
- ```
150
-
151
- `~/.claude.json` に登録される。Claude Code を再起動すると、初回に承認プロンプトが出る。`/mcp` で接続状態を確認できる。
152
-
153
- 他の MCP クライアントでも、stdio で `npx -y aiterm-mcp`(または `aiterm-mcp`)を起動するだけ。
154
-
155
140
  ## ツール
156
141
 
157
142
  | ツール | 役割 | 主な引数 |
@@ -177,16 +162,6 @@ claude mcp add --scope user --transport stdio aiterm -- aiterm-mcp
177
162
 
178
163
  `pty_send` は送信前に破壊的コマンド(`rm -rf /`, `mkfs`, `dd of=/dev/…`, `DROP TABLE` 等)を遮断し(`force: true` で越える)、ESC・ブラケットペースト終端などをサニタイズする。`pty_read` は制御文字を無害化して返す。
179
164
 
180
- ## 既知の制約(バグではなく仕様)
181
-
182
- - **ネスト中(ssh / docker / REPL)は quiescence が原理的に効かない。** 前面コマンドがシェル集合(bash/sh/zsh/fish/dash)の外になるため。ネスト中で `until` 未指定のときは、待っても完了を確定できる信号が無いので、`pty_read({ wait: true })` はフル `timeout` を空費せず出力静止時点で `is_complete=False via nested` と早期に返し、`until`(プロンプト等の正規表現)か `mark: true`(終了コード付き sentinel)の指定を促す。
183
- - **`is_complete=False` は失敗ではない。** 「timeout 内に完了を観測できなかった」という意味。長時間コマンドでは `timeout` を伸ばすか `until`/`mark` を使う。
184
- - **破壊ゲートはサンドボックスではなく tripwire。** よくある破壊形だけを弾く。相対パスの `rm`、`$VAR` 展開後に危険化するもの、ssh 先で実行されるコマンドは捕捉しない。
185
- - **`pty_send({ rtk: true })` は単行コマンドのみ+外部 `rtk` バイナリが必要**(無ければ素通し)。一方 `pty_read({ rtk: true })` の reducer は自前実装で rtk 非依存。
186
- - **`pytest` reducer は件数・罫線・`FAILURES` ブロック整形が rtk 0.42.0 と byte 一致**(回帰テストで固定)。ただし `-ra`/`-rf` 時の `FAILED` 要約行の理由は**全文を保持する**(rtk 0.42.0 は最初の `" - "` 区切りで切るが、本実装は可読性優先で情報を残すため、この行は意図的に rtk と完全一致させない)。rtk が大出力時に付ける `[full output: …]`(tee ポインタ)行は read 側では再現しない。
187
- - **tmux は `-f /dev/null` 起動**なので `~/.tmux.conf` を読まない(環境差を排除するため)。
188
- - **全セッションが単一 socket(`claude.sock`)上にある。** `tmux … kill-server` は全セッションを消す。
189
-
190
165
  ## 人が覗く
191
166
 
192
167
  セッションは共有 tmux ソケット上にある。`pty_open` の戻り値に表示される `tmux -S … attach -t <id>` で人間が同じ端末に入って介入できる(抜けるのは `Ctrl-b d`)。Windows ネイティブではセッションが WSL 内にあるため、表示は WSL 形(`wsl tmux -S … attach -t <id>`)になる。
@@ -202,6 +177,16 @@ npm link # ローカルで `aiterm-mcp` を PATH に
202
177
 
203
178
  ロジックは `src/core.ts`(tmux 制御・削減・完了検出・安全)と `src/rtk.ts`(コマンド別 reducer)、公開は `src/index.ts`。設計の出発点と reducer の移植元(pytest reducer は本家 rtk 0.42.0 と出力が byte 一致するよう移植・回帰テストで固定)は `prototype/python/` を参照。
204
179
 
180
+ ## 既知の制約(バグではなく仕様)
181
+
182
+ - **ネスト中(ssh / docker / REPL)は quiescence が原理的に効かない。** 前面コマンドがシェル集合(bash/sh/zsh/fish/dash)の外になるため。ネスト中で `until` 未指定のときは、待っても完了を確定できる信号が無いので、`pty_read({ wait: true })` はフル `timeout` を空費せず出力静止時点で `is_complete=False via nested` と早期に返し、`until`(プロンプト等の正規表現)か `mark: true`(終了コード付き sentinel)の指定を促す。
183
+ - **`is_complete=False` は失敗ではない。** 「timeout 内に完了を観測できなかった」という意味。長時間コマンドでは `timeout` を伸ばすか `until`/`mark` を使う。
184
+ - **破壊ゲートはサンドボックスではなく tripwire。** よくある破壊形だけを弾く。相対パスの `rm`、`$VAR` 展開後に危険化するもの、ssh 先で実行されるコマンドは捕捉しない。
185
+ - **`pty_send({ rtk: true })` は単行コマンドのみ+外部 `rtk` バイナリが必要**(無ければ素通し)。一方 `pty_read({ rtk: true })` の reducer は自前実装で rtk 非依存。
186
+ - **`pytest` reducer は件数・罫線・`FAILURES` ブロック整形が rtk 0.42.0 と byte 一致**(回帰テストで固定)。ただし `-ra`/`-rf` 時の `FAILED` 要約行の理由は**全文を保持する**(rtk 0.42.0 は最初の `" - "` 区切りで切るが、本実装は可読性優先で情報を残すため、この行は意図的に rtk と完全一致させない)。rtk が大出力時に付ける `[full output: …]`(tee ポインタ)行は read 側では再現しない。
187
+ - **tmux は `-f /dev/null` 起動**なので `~/.tmux.conf` を読まない(環境差を排除するため)。
188
+ - **全セッションが単一 socket(`claude.sock`)上にある。** `tmux … kill-server` は全セッションを消す。
189
+
205
190
  ## 試す
206
191
 
207
192
  1 コマンド、clone もビルドも不要:
package/README.md CHANGED
@@ -8,15 +8,70 @@
8
8
  [![npm](https://img.shields.io/npm/v/aiterm-mcp.svg)](https://www.npmjs.com/package/aiterm-mcp)
9
9
  [![node](https://img.shields.io/node/v/aiterm-mcp)](https://nodejs.org)
10
10
  [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
11
- [![npm downloads](https://img.shields.io/npm/dm/aiterm-mcp.svg)](https://www.npmjs.com/package/aiterm-mcp)
12
11
  [![install size](https://packagephobia.com/badge?p=aiterm-mcp)](https://packagephobia.com/result?p=aiterm-mcp)
13
12
 
14
13
  > *(日本語: [README.ja.md](README.ja.md))*
15
14
 
16
- > Give an AI a **persistent local terminal** as a stdio MCP server. It holds **one** local PTY; SSH and containers are demoted to "a command you send into that terminal." Reads are token-reduced.
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.
16
+ >
17
+ > *MCP = Model Context Protocol — the open standard that lets tools like Claude Code plug capabilities into an AI.*
17
18
 
18
19
  Just six tools — `pty_open` / `pty_send` / `pty_read` / `pty_key` / `pty_close` / `pty_list`. The backend is **tmux**, so sessions survive even if the MCP server or the AI client restarts.
19
20
 
21
+ **Status:** actively maintained · runs on Linux · WSL2 · macOS · native Windows · MIT · see the [CHANGELOG](CHANGELOG.md).
22
+
23
+ ## Why
24
+
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.)
26
+
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.
28
+
29
+ ```
30
+ pty_open() → grab one local terminal
31
+ pty_send(id, "ssh 192.168.1.2") → authenticate once, inside that terminal
32
+ pty_send(id, "uname -a") → every later command rides the SAME session
33
+ pty_read(id, { wait: true }) → read the reduced output
34
+ ```
35
+
36
+ ## Demo
37
+
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.
39
+
40
+ A noisy `git log`, read back token-reduced (458 → 273 tokens):
41
+
42
+ ```text
43
+ → pty_send("demo", "git log --oneline -12")
44
+ → 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]
50
+ ```
51
+
52
+ A `grep`, folded by the per-command reducer to just the hits (127 → 46 tokens):
53
+
54
+ ```text
55
+ → pty_send("demo", "grep -rn capture-pane src/ test/")
56
+ → pty_read("demo", { wait: true, rtk: true })
57
+ ← 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]
61
+ ```
62
+
63
+ Nesting is just text you send in — here a Python REPL *inside* the same PTY:
64
+
65
+ ```text
66
+ → pty_send("demo", "python3")
67
+ → pty_read("demo", { until: ">>> " }) # nested prompt = "the inner shell is ready"
68
+ → pty_send("demo", "print(sum(range(1_000_000)))")
69
+ → pty_read("demo", { until: ">>> " })
70
+ ← 499999500000 [is_complete=True via until]
71
+ ```
72
+
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)).
74
+
20
75
  ## Quickstart (≈60 seconds)
21
76
 
22
77
  One command registers it in Claude Code — no clone, no build, `npx` fetches it each run:
@@ -40,70 +95,17 @@ pty_read("t1", { wait: true }) → "hello" (token-reduced, completion det
40
95
  pty_close("t1") → terminal released
41
96
  ```
42
97
 
43
- That's it. The terminal in `t1` is real and persistent — `ssh`, `docker exec`, a REPL are just text you `pty_send` into it (see [Why](#why)). Prefer no install? Any MCP client can launch `npx -y aiterm-mcp` over stdio directly.
44
-
45
- ## Install
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.
46
99
 
47
- It's on npm no clone, no build:
100
+ **Prefer a global install, or a different client?**
48
101
 
49
102
  ```bash
50
- # Claude Code recommended (no install; npx fetches it each run)
51
- claude mcp add --scope user --transport stdio aiterm -- npx -y aiterm-mcp
52
-
53
- # or install globally, then register the command name
103
+ # install globally, then register the command name
54
104
  npm i -g aiterm-mcp
55
105
  claude mcp add --scope user --transport stdio aiterm -- aiterm-mcp
56
106
  ```
57
107
 
58
- Needs **Node 18** and **tmux** (on native Windows: WSL with tmux inside it see [Requirements](#requirements)). Other MCP clients: just run `npx -y aiterm-mcp` over stdio. More detail in [Install / register](#install--register) below.
59
-
60
- ## Why
61
-
62
- Sending an AI one command at a time and reading back the result means, over SSH, repeating connect → authenticate → disconnect every round — slow, and it burns tokens. aiterm **holds one PTY persistently** and you type `ssh host` or `docker exec -it x bash` *inside it* (nesting). Session kind is never a tool-level distinction.
63
-
64
- ```
65
- pty_open() → grab one local terminal
66
- pty_send(id, "ssh 192.168.1.2") → enter SSH inside that terminal
67
- pty_send(id, "uname -a") → run it on the remote
68
- pty_read(id, { wait: true }) → read the reduced output
69
- ```
70
-
71
- ## Demo
72
-
73
- <!-- demo gif: drop docs/demo.gif here (asciinema cast or animated GIF of the flow below) -->
74
-
75
- The killer flow: open one PTY, nest into SSH *inside it*, run a command on the remote, and read back output that's already been token-reduced — no reconnect per command.
76
-
77
- ```text
78
- # 1 — grab one local terminal (lives in tmux; survives restarts)
79
- → pty_open()
80
- ← { session_id: "t1", attach: "tmux -S /…/claude.sock attach -t t1" }
81
-
82
- # 2 — nest SSH *inside* that same terminal (not a separate tool)
83
- → pty_send("t1", "ssh 192.168.1.2")
84
- ← sent
85
- → pty_read("t1", { until: "\\$ $" }) # remote prompt = "shell is back"
86
- ← user@remote:~$
87
-
88
- # 3 — run a command on the remote, over the SAME PTY (no reconnect)
89
- → pty_send("t1", "uname -a")
90
- → pty_read("t1", { until: "\\$ $" })
91
- ← Linux remote 6.1.0 #1 SMP x86_64 GNU/Linux
92
-
93
- # 4 — a noisy command, read with the per-command reducer
94
- → pty_send("t1", "git status")
95
- → pty_read("t1", { until: "\\$ $", rtk: true }) # self-contained, no rtk binary needed
96
- ← ## main…origin/main [ahead 1]
97
- M src/core.ts
98
- ?? notes.txt
99
- [reduced: control chars stripped · repeats collapsed · git-status reducer]
100
- ```
101
-
102
- Notes that make this accurate, not a demo lie:
103
-
104
- - Step 2/3 use **`until`** with the remote prompt because **while nested, quiescence cannot fire by design** — see [Completion detection](#completion-detection-4-layers) and [Known constraints](#known-constraints-by-design-not-bugs). `{ wait: true }` alone works at the local shell; nested needs `until` (or `mark: true`).
105
- - The bracketed `[reduced: …]` line is illustrative of the meta/restore hint `pty_read` appends; the exact text comes from your output. The reducer is the **self-contained** `pty_read({ rtk: true })` path — no external `rtk` binary required.
106
- - A human can `attach` to the `t1` socket and watch the same SSH session live (see [A human can watch](#a-human-can-watch)).
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).
107
109
 
108
110
  ## How it works
109
111
 
@@ -119,10 +121,10 @@ One PTY is the only primitive. Everything else — SSH, containers, REPLs — is
119
121
 
120
122
  ## vs. the alternatives
121
123
 
122
- | | **aiterm-mcp** | one-shot shell tool (per command) | generic terminal / tmux MCPs |
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`) |
123
125
  | --- | --- | --- | --- |
124
126
  | Persistent session | ✅ tmux, survives restarts | ❌ new shell every call | ⚠️ varies |
125
- | SSH / containers | nest with one `pty_send` | reconnect every command | ⚠️ often separate tools |
127
+ | SSH / containers | nest with one `pty_send` | reconnect every command | ⚠️ often separate tools / per-call connect |
126
128
  | Token-reduced reads | ✅ per-command reducers | ❌ raw output | ⚠️ rarely |
127
129
  | Completion detection | 4-layer: exit / `until` / quiescence / timeout | n/a (blocks per call) | ⚠️ prompt-match, fragile |
128
130
  | Human can co-drive | ✅ shared tmux socket (`attach`) | ❌ | ⚠️ varies |
@@ -135,23 +137,6 @@ One PTY is the only primitive. Everything else — SSH, containers, REPLs — is
135
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.)
136
138
  - Optional: the [`rtk`](https://github.com/rtk-ai/rtk) binary (used by `pty_send`'s `rtk: true` delegation; works fine without it)
137
139
 
138
- ## Install / register
139
-
140
- Register with Claude Code (CLI) at user scope (available in every project):
141
-
142
- ```bash
143
- # Recommended — no install, npx fetches it each run
144
- claude mcp add --scope user --transport stdio aiterm -- npx -y aiterm-mcp
145
-
146
- # Or install globally and use the command name
147
- npm i -g aiterm-mcp
148
- claude mcp add --scope user --transport stdio aiterm -- aiterm-mcp
149
- ```
150
-
151
- This registers it in `~/.claude.json`. Restart Claude Code; you'll get an approval prompt the first time. Check the connection with `/mcp`.
152
-
153
- Any other MCP client works too — just launch `npx -y aiterm-mcp` (or `aiterm-mcp`) over stdio.
154
-
155
140
  ## Tools
156
141
 
157
142
  | Tool | Role | Key args |
@@ -177,16 +162,6 @@ Any other MCP client works too — just launch `npx -y aiterm-mcp` (or `aiterm-m
177
162
 
178
163
  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.
179
164
 
180
- ## Known constraints (by design, not bugs)
181
-
182
- - **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.
183
- - **`is_complete=False` is not a failure.** It means "completion was not observed within `timeout`." For long commands, raise `timeout` or use `until`/`mark`.
184
- - **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.
185
- - **`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.
186
- - **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.
187
- - **tmux is started with `-f /dev/null`**, so it does not read `~/.tmux.conf` (to keep behavior reproducible across machines).
188
- - **All sessions live on a single socket (`claude.sock`).** `tmux … kill-server` removes them all.
189
-
190
165
  ## A human can watch
191
166
 
192
167
  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.
@@ -202,6 +177,16 @@ npm link # put `aiterm-mcp` on PATH locally
202
177
 
203
178
  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/`.
204
179
 
180
+ ## Known constraints (by design, not bugs)
181
+
182
+ - **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.
183
+ - **`is_complete=False` is not a failure.** It means "completion was not observed within `timeout`." For long commands, raise `timeout` or use `until`/`mark`.
184
+ - **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.
185
+ - **`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.
186
+ - **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.
187
+ - **tmux is started with `-f /dev/null`**, so it does not read `~/.tmux.conf` (to keep behavior reproducible across machines).
188
+ - **All sessions live on a single socket (`claude.sock`).** `tmux … kill-server` removes them all.
189
+
205
190
  ## Try it
206
191
 
207
192
  One command, no clone, no build:
package/package.json CHANGED
@@ -1,16 +1,21 @@
1
1
  {
2
2
  "name": "aiterm-mcp",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
+ "mcpName": "io.github.kitepon-rgb/aiterm-mcp",
4
5
  "description": "AI-driven persistent terminal as a local stdio MCP server (tmux-backed). Holds one local PTY; SSH and containers are just commands you send into it. Token-reducing reads.",
5
6
  "keywords": [
6
7
  "mcp",
8
+ "mcp-server",
7
9
  "model-context-protocol",
8
10
  "terminal",
9
11
  "tmux",
10
12
  "pty",
13
+ "ssh",
11
14
  "claude",
15
+ "claude-code",
16
+ "cursor",
12
17
  "ai-agent",
13
- "ssh"
18
+ "devtools"
14
19
  ],
15
20
  "license": "MIT",
16
21
  "author": "kitepon",