aiterm-mcp 0.3.0 → 0.4.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.ja.md +78 -1
- package/README.md +78 -1
- package/dist/core.js +32 -13
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/README.ja.md
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
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
|
+
[](https://packagephobia.com/result?p=aiterm-mcp)
|
|
11
13
|
|
|
12
14
|
> *(English: [README.md](README.md))*
|
|
13
15
|
|
|
@@ -15,6 +17,31 @@
|
|
|
15
17
|
|
|
16
18
|
`pty_open` / `pty_send` / `pty_read` / `pty_key` / `pty_close` / `pty_list` の 6 ツールだけ。バックエンドは tmux なので、MCP サーバや AI クライアントが再起動してもセッションは生き残る。
|
|
17
19
|
|
|
20
|
+
## クイックスタート(約60秒)
|
|
21
|
+
|
|
22
|
+
Claude Code なら 1 コマンドで登録 — clone もビルドも不要、`npx` が毎回取得して起動する:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
claude mcp add --scope user --transport stdio aiterm -- npx -y aiterm-mcp
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Claude Code を再起動して、接続を確認:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
/mcp # aiterm が connected・6 ツール公開、と出る
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
最初のセッション — 4 回の呼び出しで、1 個の永続端末:
|
|
35
|
+
|
|
36
|
+
```text
|
|
37
|
+
pty_open() → { session_id: "t1", attach: "tmux -S … attach -t t1" }
|
|
38
|
+
pty_send("t1", "echo hello") → PTY にコマンドを送る
|
|
39
|
+
pty_read("t1", { wait: true }) → "hello" (トークン削減・完了検出つき)
|
|
40
|
+
pty_close("t1") → 端末を解放
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
これだけ。`t1` の端末は本物で永続 — `ssh`・`docker exec`・REPL は、そこへ `pty_send` で打ち込む“ただのテキスト”([なぜ](#なぜ))。インストールも不要にしたいなら、どの MCP クライアントからでも stdio で `npx -y aiterm-mcp` を起動するだけ。
|
|
44
|
+
|
|
18
45
|
## インストール
|
|
19
46
|
|
|
20
47
|
npm に公開済み。clone もビルドも不要:
|
|
@@ -41,6 +68,43 @@ pty_send(id, "uname -a") → リモートで実行
|
|
|
41
68
|
pty_read(id, { wait: true }) → 削減済みの出力を読む
|
|
42
69
|
```
|
|
43
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 セッションをライブで覗ける([人が覗く](#人が覗く))。
|
|
107
|
+
|
|
44
108
|
## 仕組み
|
|
45
109
|
|
|
46
110
|
```mermaid
|
|
@@ -115,7 +179,7 @@ claude mcp add --scope user --transport stdio aiterm -- aiterm-mcp
|
|
|
115
179
|
|
|
116
180
|
## 既知の制約(バグではなく仕様)
|
|
117
181
|
|
|
118
|
-
- **ネスト中(ssh / docker / REPL)は quiescence が原理的に効かない。** 前面コマンドがシェル集合(bash/sh/zsh/fish/dash
|
|
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)の指定を促す。
|
|
119
183
|
- **`is_complete=False` は失敗ではない。** 「timeout 内に完了を観測できなかった」という意味。長時間コマンドでは `timeout` を伸ばすか `until`/`mark` を使う。
|
|
120
184
|
- **破壊ゲートはサンドボックスではなく tripwire。** よくある破壊形だけを弾く。相対パスの `rm`、`$VAR` 展開後に危険化するもの、ssh 先で実行されるコマンドは捕捉しない。
|
|
121
185
|
- **`pty_send({ rtk: true })` は単行コマンドのみ+外部 `rtk` バイナリが必要**(無ければ素通し)。一方 `pty_read({ rtk: true })` の reducer は自前実装で rtk 非依存。
|
|
@@ -138,6 +202,19 @@ npm link # ローカルで `aiterm-mcp` を PATH に
|
|
|
138
202
|
|
|
139
203
|
ロジックは `src/core.ts`(tmux 制御・削減・完了検出・安全)と `src/rtk.ts`(コマンド別 reducer)、公開は `src/index.ts`。設計の出発点と reducer の移植元(pytest reducer は本家 rtk 0.42.0 と出力が byte 一致するよう移植・回帰テストで固定)は `prototype/python/` を参照。
|
|
140
204
|
|
|
205
|
+
## 試す
|
|
206
|
+
|
|
207
|
+
1 コマンド、clone もビルドも不要:
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
claude mcp add --scope user --transport stdio aiterm -- npx -y aiterm-mcp
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
aiterm がトークンの往復を 1 回でも省けたなら、**[リポジトリに star](https://github.com/kitepon-rgb/aiterm-mcp)** を — 他の人に見つけてもらう一番安い方法です。
|
|
214
|
+
|
|
215
|
+
- **npm:** https://www.npmjs.com/package/aiterm-mcp
|
|
216
|
+
- **Issue / バグ報告:** https://github.com/kitepon-rgb/aiterm-mcp/issues
|
|
217
|
+
|
|
141
218
|
## ライセンス
|
|
142
219
|
|
|
143
220
|
MIT
|
package/README.md
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
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
|
+
[](https://packagephobia.com/result?p=aiterm-mcp)
|
|
11
13
|
|
|
12
14
|
> *(日本語: [README.ja.md](README.ja.md))*
|
|
13
15
|
|
|
@@ -15,6 +17,31 @@
|
|
|
15
17
|
|
|
16
18
|
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.
|
|
17
19
|
|
|
20
|
+
## Quickstart (≈60 seconds)
|
|
21
|
+
|
|
22
|
+
One command registers it in Claude Code — no clone, no build, `npx` fetches it each run:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
claude mcp add --scope user --transport stdio aiterm -- npx -y aiterm-mcp
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Restart Claude Code, then verify the connection:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
/mcp # aiterm should show as connected, exposing 6 tools
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Your first session — four calls, one persistent terminal:
|
|
35
|
+
|
|
36
|
+
```text
|
|
37
|
+
pty_open() → { session_id: "t1", attach: "tmux -S … attach -t t1" }
|
|
38
|
+
pty_send("t1", "echo hello") → command sent into the PTY
|
|
39
|
+
pty_read("t1", { wait: true }) → "hello" (token-reduced, completion detected)
|
|
40
|
+
pty_close("t1") → terminal released
|
|
41
|
+
```
|
|
42
|
+
|
|
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
|
+
|
|
18
45
|
## Install
|
|
19
46
|
|
|
20
47
|
It's on npm — no clone, no build:
|
|
@@ -41,6 +68,43 @@ pty_send(id, "uname -a") → run it on the remote
|
|
|
41
68
|
pty_read(id, { wait: true }) → read the reduced output
|
|
42
69
|
```
|
|
43
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)).
|
|
107
|
+
|
|
44
108
|
## How it works
|
|
45
109
|
|
|
46
110
|
```mermaid
|
|
@@ -115,7 +179,7 @@ Before sending, `pty_send` blocks destructive commands (`rm -rf /`, `mkfs`, `dd
|
|
|
115
179
|
|
|
116
180
|
## Known constraints (by design, not bugs)
|
|
117
181
|
|
|
118
|
-
- **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).
|
|
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.
|
|
119
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`.
|
|
120
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.
|
|
121
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.
|
|
@@ -138,6 +202,19 @@ npm link # put `aiterm-mcp` on PATH locally
|
|
|
138
202
|
|
|
139
203
|
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/`.
|
|
140
204
|
|
|
205
|
+
## Try it
|
|
206
|
+
|
|
207
|
+
One command, no clone, no build:
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
claude mcp add --scope user --transport stdio aiterm -- npx -y aiterm-mcp
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
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.
|
|
214
|
+
|
|
215
|
+
- **npm:** https://www.npmjs.com/package/aiterm-mcp
|
|
216
|
+
- **Issues / bug reports:** https://github.com/kitepon-rgb/aiterm-mcp/issues
|
|
217
|
+
|
|
141
218
|
## License
|
|
142
219
|
|
|
143
220
|
MIT
|
package/dist/core.js
CHANGED
|
@@ -332,7 +332,7 @@ async function settleWinLog(name) {
|
|
|
332
332
|
await sleep(POLL * 1000);
|
|
333
333
|
}
|
|
334
334
|
}
|
|
335
|
-
/**
|
|
335
|
+
/** 完了境界: dead / until 一致 / (出力静止 ∧ シェル復帰)=quiescent / (ネスト中+until無しで出力静止)=nested(未確定・早期返却) / timeout。 */
|
|
336
336
|
async function waitCompletion(name, untilRe, timeout) {
|
|
337
337
|
// 締切は単調時計で測る。Date.now() は NTP 補正やサスペンドで巻き戻り、長時間待ちで誤判定する(Python は time.monotonic)。
|
|
338
338
|
const deadline = performance.now() + timeout * 1000;
|
|
@@ -362,10 +362,22 @@ async function waitCompletion(name, untilRe, timeout) {
|
|
|
362
362
|
}
|
|
363
363
|
if (size === lastSize) {
|
|
364
364
|
stable++;
|
|
365
|
-
if (stable >= STABLE_POLLS
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
365
|
+
if (stable >= STABLE_POLLS) {
|
|
366
|
+
const fg = paneCurrentCommand(name);
|
|
367
|
+
if (SHELLS.has(fg)) {
|
|
368
|
+
if (isWin)
|
|
369
|
+
await settleWinLog(name);
|
|
370
|
+
return [true, "quiescent"]; // 出力静止 ∧ シェル復帰 = 確証つき完了
|
|
371
|
+
}
|
|
372
|
+
// ネスト中(前面が ssh/docker/REPL 等でシェル集合外)は quiescence の「シェル復帰」条件を
|
|
373
|
+
// 原理的に満たせない。until 未指定ならこれ以上待っても確証は増えない(until/dead/quiescent の
|
|
374
|
+
// いずれも発火し得ない)ので、出力が静止した時点で「未確定」のまま早期返却し until/mark を促す。
|
|
375
|
+
// fg==="" は前面コマンド取得失敗=ネスト断定不可なので早期返却せず従来どおり timeout まで待つ。
|
|
376
|
+
if (!until && fg !== "") {
|
|
377
|
+
if (isWin)
|
|
378
|
+
await settleWinLog(name);
|
|
379
|
+
return [false, "nested"];
|
|
380
|
+
}
|
|
369
381
|
}
|
|
370
382
|
}
|
|
371
383
|
else {
|
|
@@ -377,6 +389,17 @@ async function waitCompletion(name, untilRe, timeout) {
|
|
|
377
389
|
await sleep(POLL * 1000);
|
|
378
390
|
}
|
|
379
391
|
}
|
|
392
|
+
// 完了ステータス → is_complete 表記。確証のある層のみ True(until/dead/quiescent)。
|
|
393
|
+
// timeout と nested(ネスト中・出力静止だが確証なし)は False。nested は until/mark を促す注記を添える。
|
|
394
|
+
function completionSuffix(status) {
|
|
395
|
+
const complete = status === "until" || status === "dead" || status === "quiescent";
|
|
396
|
+
let s = ` [is_complete=${complete ? "True" : "False"} via ${status}]`;
|
|
397
|
+
if (status === "nested")
|
|
398
|
+
s +=
|
|
399
|
+
" ネスト中(前面が ssh/docker/REPL 等)は出力静止だけでは完了を確定できません。" +
|
|
400
|
+
"until(リモートのプロンプト等の正規表現)か mark:true で完了を指定してください。";
|
|
401
|
+
return s;
|
|
402
|
+
}
|
|
380
403
|
function rtkRewrite(text) {
|
|
381
404
|
if (text.trim().includes("\n"))
|
|
382
405
|
return text;
|
|
@@ -506,20 +529,16 @@ export async function readOutput(name, o = {}) {
|
|
|
506
529
|
const [reduced, rname] = rtk.reduce(cmd, framed);
|
|
507
530
|
if (reduced !== null) {
|
|
508
531
|
const meta = `[aiterm ${name}: rtk:${rname} 適用 / ~${estimateTokens(reduced)} tok (raw ~${estimateTokens(text)} tok)]`;
|
|
509
|
-
if (status)
|
|
510
|
-
|
|
511
|
-
return reduced + "\n" + meta + ` [is_complete=${complete ? "True" : "False"} via ${status}]`;
|
|
512
|
-
}
|
|
532
|
+
if (status)
|
|
533
|
+
return reduced + "\n" + meta + completionSuffix(status);
|
|
513
534
|
return reduced + "\n" + meta;
|
|
514
535
|
}
|
|
515
536
|
}
|
|
516
537
|
// reducer 非該当 → 汎用削減へフォールバック
|
|
517
538
|
}
|
|
518
539
|
const [body, meta] = reduceOutput(text, name, !o.range);
|
|
519
|
-
if (status)
|
|
520
|
-
|
|
521
|
-
return body + "\n" + meta + ` [is_complete=${complete ? "True" : "False"} via ${status}]`;
|
|
522
|
-
}
|
|
540
|
+
if (status)
|
|
541
|
+
return body + "\n" + meta + completionSuffix(status);
|
|
523
542
|
return body + "\n" + meta;
|
|
524
543
|
}
|
|
525
544
|
export function listSessions() {
|
package/dist/index.js
CHANGED
|
@@ -12,7 +12,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
12
12
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
13
13
|
import { z } from "zod";
|
|
14
14
|
import * as core from "./core.js";
|
|
15
|
-
const server = new McpServer({ name: "aiterm", version: "0.
|
|
15
|
+
const server = new McpServer({ name: "aiterm", version: "0.4.0" });
|
|
16
16
|
function ok(s) {
|
|
17
17
|
return { content: [{ type: "text", text: s }] };
|
|
18
18
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aiterm-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"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
5
|
"keywords": [
|
|
6
6
|
"mcp",
|