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.
- package/README.ja.md +73 -88
- package/README.md +73 -88
- package/package.json +7 -2
package/README.ja.md
CHANGED
|
@@ -8,15 +8,70 @@
|
|
|
8
8
|
[](https://www.npmjs.com/package/aiterm-mcp)
|
|
9
9
|
[](https://nodejs.org)
|
|
10
10
|
[](LICENSE)
|
|
11
|
-
[](https://www.npmjs.com/package/aiterm-mcp)
|
|
12
11
|
[](https://packagephobia.com/result?p=aiterm-mcp)
|
|
13
12
|
|
|
14
13
|
> *(English: [README.md](README.md))*
|
|
15
14
|
|
|
16
|
-
>
|
|
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`
|
|
44
|
-
|
|
45
|
-
## インストール
|
|
98
|
+
これだけ。`t1` の端末は本物で永続 — `ssh`・`docker exec`・REPL は、そこへ `pty_send` で打ち込む“ただのテキスト”。
|
|
46
99
|
|
|
47
|
-
|
|
100
|
+
**グローバル導入や別クライアントが良い場合は:**
|
|
48
101
|
|
|
49
102
|
```bash
|
|
50
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
|
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
|
[](https://www.npmjs.com/package/aiterm-mcp)
|
|
9
9
|
[](https://nodejs.org)
|
|
10
10
|
[](LICENSE)
|
|
11
|
-
[](https://www.npmjs.com/package/aiterm-mcp)
|
|
12
11
|
[](https://packagephobia.com/result?p=aiterm-mcp)
|
|
13
12
|
|
|
14
13
|
> *(日本語: [README.ja.md](README.ja.md))*
|
|
15
14
|
|
|
16
|
-
>
|
|
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
|
|
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
|
-
|
|
100
|
+
**Prefer a global install, or a different client?**
|
|
48
101
|
|
|
49
102
|
```bash
|
|
50
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
|
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.
|
|
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
|
-
"
|
|
18
|
+
"devtools"
|
|
14
19
|
],
|
|
15
20
|
"license": "MIT",
|
|
16
21
|
"author": "kitepon",
|