aiterm-mcp 0.1.0 → 0.2.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 +21 -2
- package/README.md +21 -2
- package/dist/core.js +109 -9
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/README.ja.md
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
[](https://github.com/kitepon-rgb/aiterm-mcp/actions/workflows/ci.yml)
|
|
8
8
|
[](https://www.npmjs.com/package/aiterm-mcp)
|
|
9
|
+
[](https://nodejs.org)
|
|
10
|
+
[](LICENSE)
|
|
9
11
|
|
|
10
12
|
> *(English: [README.md](README.md))*
|
|
11
13
|
|
|
@@ -13,6 +15,21 @@
|
|
|
13
15
|
|
|
14
16
|
`pty_open` / `pty_send` / `pty_read` / `pty_key` / `pty_close` / `pty_list` の 6 ツールだけ。バックエンドは tmux なので、MCP サーバや AI クライアントが再起動してもセッションは生き残る。
|
|
15
17
|
|
|
18
|
+
## インストール
|
|
19
|
+
|
|
20
|
+
npm に公開済み。clone もビルドも不要:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
# Claude Code — 推奨(インストール不要、npx が毎回取得して起動)
|
|
24
|
+
claude mcp add --scope user --transport stdio aiterm -- npx -y aiterm-mcp
|
|
25
|
+
|
|
26
|
+
# またはグローバル導入してコマンド名で登録
|
|
27
|
+
npm i -g aiterm-mcp
|
|
28
|
+
claude mcp add --scope user --transport stdio aiterm -- aiterm-mcp
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Node ≥ 18** と **tmux** が必要(Windows ネイティブでは WSL とその中の tmux。下の「要件」参照)。他の MCP クライアントは stdio で `npx -y aiterm-mcp` を起動するだけ(詳細は下の「インストール / 登録」)。
|
|
32
|
+
|
|
16
33
|
## なぜ
|
|
17
34
|
|
|
18
35
|
AI にコマンドを 1 個ずつ投げて結果を受け取る往復は、SSH では毎回「接続→認証→切断」を繰り返し遅く、トークンも食う。aiterm は **1 個の PTY を永続的に握り**、その中で `ssh host` や `docker exec -it x bash` と打って入る(ネスト)。セッション種別をツールで区別しない。
|
|
@@ -50,6 +67,8 @@ flowchart LR
|
|
|
50
67
|
|
|
51
68
|
- **Node.js >= 18**
|
|
52
69
|
- **tmux**(実行時の前提。`tmux -V` で確認。未導入なら `apt install tmux` / `brew install tmux`)
|
|
70
|
+
- **macOS / Linux / WSL2** は tmux を直接使う。
|
|
71
|
+
- **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 に入る。)
|
|
53
72
|
- 任意: [`rtk`](https://github.com/rtk-ai/rtk) バイナリ(`pty_send` の `rtk: true` 委譲で使う。無くても動く)
|
|
54
73
|
|
|
55
74
|
## インストール / 登録
|
|
@@ -57,7 +76,7 @@ flowchart LR
|
|
|
57
76
|
Claude Code(CLI)にユーザースコープ(全プロジェクトで利用可)で登録する例:
|
|
58
77
|
|
|
59
78
|
```bash
|
|
60
|
-
#
|
|
79
|
+
# 推奨: インストール不要、npx が毎回取得して起動
|
|
61
80
|
claude mcp add --scope user --transport stdio aiterm -- npx -y aiterm-mcp
|
|
62
81
|
|
|
63
82
|
# またはグローバルインストールしてコマンド名で
|
|
@@ -106,7 +125,7 @@ claude mcp add --scope user --transport stdio aiterm -- aiterm-mcp
|
|
|
106
125
|
|
|
107
126
|
## 人が覗く
|
|
108
127
|
|
|
109
|
-
セッションは共有 tmux ソケット上にある。`pty_open` の戻り値に表示される `tmux -S … attach -t <id>` で人間が同じ端末に入って介入できる(抜けるのは `Ctrl-b d`)。
|
|
128
|
+
セッションは共有 tmux ソケット上にある。`pty_open` の戻り値に表示される `tmux -S … attach -t <id>` で人間が同じ端末に入って介入できる(抜けるのは `Ctrl-b d`)。Windows ネイティブではセッションが WSL 内にあるため、表示は WSL 形(`wsl tmux -S … attach -t <id>`)になる。
|
|
110
129
|
|
|
111
130
|
## 開発
|
|
112
131
|
|
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
[](https://github.com/kitepon-rgb/aiterm-mcp/actions/workflows/ci.yml)
|
|
8
8
|
[](https://www.npmjs.com/package/aiterm-mcp)
|
|
9
|
+
[](https://nodejs.org)
|
|
10
|
+
[](LICENSE)
|
|
9
11
|
|
|
10
12
|
> *(日本語: [README.ja.md](README.ja.md))*
|
|
11
13
|
|
|
@@ -13,6 +15,21 @@
|
|
|
13
15
|
|
|
14
16
|
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.
|
|
15
17
|
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
It's on npm — no clone, no build:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
# Claude Code — recommended (no install; npx fetches it each run)
|
|
24
|
+
claude mcp add --scope user --transport stdio aiterm -- npx -y aiterm-mcp
|
|
25
|
+
|
|
26
|
+
# or install globally, then register the command name
|
|
27
|
+
npm i -g aiterm-mcp
|
|
28
|
+
claude mcp add --scope user --transport stdio aiterm -- aiterm-mcp
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
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.
|
|
32
|
+
|
|
16
33
|
## Why
|
|
17
34
|
|
|
18
35
|
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.
|
|
@@ -50,6 +67,8 @@ One PTY is the only primitive. Everything else — SSH, containers, REPLs — is
|
|
|
50
67
|
|
|
51
68
|
- **Node.js >= 18**
|
|
52
69
|
- **tmux** (runtime prerequisite; check with `tmux -V`. Install with `apt install tmux` / `brew install tmux`)
|
|
70
|
+
- **macOS / Linux / WSL2** run tmux directly.
|
|
71
|
+
- **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.)
|
|
53
72
|
- Optional: the [`rtk`](https://github.com/rtk-ai/rtk) binary (used by `pty_send`'s `rtk: true` delegation; works fine without it)
|
|
54
73
|
|
|
55
74
|
## Install / register
|
|
@@ -57,7 +76,7 @@ One PTY is the only primitive. Everything else — SSH, containers, REPLs — is
|
|
|
57
76
|
Register with Claude Code (CLI) at user scope (available in every project):
|
|
58
77
|
|
|
59
78
|
```bash
|
|
60
|
-
#
|
|
79
|
+
# Recommended — no install, npx fetches it each run
|
|
61
80
|
claude mcp add --scope user --transport stdio aiterm -- npx -y aiterm-mcp
|
|
62
81
|
|
|
63
82
|
# Or install globally and use the command name
|
|
@@ -106,7 +125,7 @@ Before sending, `pty_send` blocks destructive commands (`rm -rf /`, `mkfs`, `dd
|
|
|
106
125
|
|
|
107
126
|
## A human can watch
|
|
108
127
|
|
|
109
|
-
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).
|
|
128
|
+
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.
|
|
110
129
|
|
|
111
130
|
## Development
|
|
112
131
|
|
package/dist/core.js
CHANGED
|
@@ -10,9 +10,22 @@
|
|
|
10
10
|
import { spawnSync } from "node:child_process";
|
|
11
11
|
import * as fs from "node:fs";
|
|
12
12
|
import * as path from "node:path";
|
|
13
|
+
import * as os from "node:os";
|
|
14
|
+
import { createHash } from "node:crypto";
|
|
13
15
|
import * as rtk from "./rtk.js";
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
// Windows ネイティブには tmux が無い。その場合だけ全 tmux 呼び出しを WSL 経由へ橋渡しする
|
|
17
|
+
// (POSIX = Linux/WSL2/macOS は従来どおり tmux を直接叩く)。
|
|
18
|
+
const isWin = process.platform === "win32";
|
|
19
|
+
// .log/.offset/.lastcmd を置くディレクトリ(Node が直接読み書きする)。
|
|
20
|
+
// POSIX は従来どおり。Windows は TMPDIR→TEMP→os.tmpdir() の順で Windows 側の一時領域に置く。
|
|
21
|
+
const SOCKDIR = path.join(process.env.TMPDIR ?? (isWin ? process.env.TEMP ?? os.tmpdir() : "/tmp"), "claude-tmux-sockets");
|
|
22
|
+
// tmux -S に渡すソケットパス。POSIX はログと同じツリーに置く。
|
|
23
|
+
// Windows は tmux が WSL 内で動くため、ソケットは WSL ネイティブ fs に置く必要がある
|
|
24
|
+
// (/mnt drvfs(9p) 上では AF_UNIX 非対応)。SOCKDIR から短い安定名を導出し、
|
|
25
|
+
// TMPDIR ごとの隔離(テスト)と再起動跨ぎ再接続を両立する。
|
|
26
|
+
const SOCK = isWin
|
|
27
|
+
? `/tmp/aiterm-${createHash("sha1").update(SOCKDIR).digest("hex").slice(0, 12)}.sock`
|
|
28
|
+
: path.join(SOCKDIR, "claude.sock");
|
|
16
29
|
// 完了検出
|
|
17
30
|
export const DEFAULT_TIMEOUT = 10.0;
|
|
18
31
|
const POLL = 0.25;
|
|
@@ -55,10 +68,47 @@ export class AitermError extends Error {
|
|
|
55
68
|
this.code = code;
|
|
56
69
|
}
|
|
57
70
|
}
|
|
71
|
+
// Windows のドライブパス (C:\a\b) を WSL から見える /mnt/c/a/b へ変換する。
|
|
72
|
+
// 一時領域は常にドライブ直下なので UNC は想定外=弾く(黙って壊れた //server パスを作らない)。
|
|
73
|
+
export function toWslPath(p) {
|
|
74
|
+
const m = /^([A-Za-z]):\/(.*)$/.exec(p.replace(/\\/g, "/"));
|
|
75
|
+
if (!m)
|
|
76
|
+
throw new AitermError(`WSL へ橋渡しできない一時パスです(ドライブ直下のみ対応): ${p}`, 2);
|
|
77
|
+
return `/mnt/${m[1].toLowerCase()}/${m[2]}`;
|
|
78
|
+
}
|
|
79
|
+
// Windows で最初の tmux 呼び出し前に一度だけ WSL+tmux の可用性を確かめ、失敗は原因別に投げる。
|
|
80
|
+
// -e(ログインシェル非経由)+短い timeout で、初回セットアップ未完了の wsl によるハングも防ぐ。
|
|
81
|
+
let winBridgeOk = false;
|
|
82
|
+
function ensureWinBridge() {
|
|
83
|
+
if (winBridgeOk)
|
|
84
|
+
return;
|
|
85
|
+
const r = spawnSync("wsl.exe", ["-e", "tmux", "-V"], { encoding: "utf8", timeout: 10000 });
|
|
86
|
+
if (r.error) {
|
|
87
|
+
const code = r.error.code;
|
|
88
|
+
if (code === "ETIMEDOUT")
|
|
89
|
+
throw new AitermError("WSL が応答しません(初回セットアップ未完了の可能性)。一度 `wsl` を起動してから再実行してください。", 2);
|
|
90
|
+
if (code === "ENOENT")
|
|
91
|
+
throw new AitermError("wsl.exe が見つかりません。Windows では WSL 上の tmux 経由で動作します。WSL と tmux を導入してください。", 2);
|
|
92
|
+
throw new AitermError(`wsl.exe を起動できませんでした(${code ?? "unknown"})。`, 2);
|
|
93
|
+
}
|
|
94
|
+
// wsl.exe は System32 にあるので「起動」は成功するが、ディストリ未導入や distro 内に tmux が無いと
|
|
95
|
+
// 非ゼロで終わる。両方を区別せず(wsl の出力は UTF-16 で文字化けし得るため)正直に表す。
|
|
96
|
+
if (r.status !== 0)
|
|
97
|
+
throw new AitermError("WSL 経由で tmux を起動できませんでした。WSL のディストリ未導入、または distro 内に tmux が無い可能性があります。`wsl tmux -V` が通るか確認してください(tmux 導入例: sudo apt install tmux)。", 2);
|
|
98
|
+
winBridgeOk = true;
|
|
99
|
+
}
|
|
58
100
|
function tmux(...args) {
|
|
59
101
|
// maxBuffer は既定 1MiB。capture-pane(大きなスクロールバック)や多セッションの list-sessions で
|
|
60
102
|
// 頭打ちになり stdout が切れる/空になる。Python の subprocess.run は無制限だったので 64MiB へ広げる。
|
|
61
|
-
|
|
103
|
+
// Windows は同じ tmux を WSL 経由(-e でログインシェル非経由=$ 展開やクオート崩れを防ぐ)で叩く。
|
|
104
|
+
let r;
|
|
105
|
+
if (isWin) {
|
|
106
|
+
ensureWinBridge();
|
|
107
|
+
r = spawnSync("wsl.exe", ["-e", "tmux", "-S", SOCK, ...args], { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
r = spawnSync("tmux", ["-S", SOCK, ...args], { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
|
|
111
|
+
}
|
|
62
112
|
// ENOBUFS(出力が 64MiB 超)を「code=1 の失敗」へ握り潰すと部分/空 stdout を正常扱いしてしまう。区別して投げる。
|
|
63
113
|
// EXPECTED-FAILURE: 外部システム境界(tmux 出力過大)
|
|
64
114
|
if (r.error && r.error.code === "ENOBUFS") {
|
|
@@ -73,6 +123,12 @@ function paneCurrentCommand(name) {
|
|
|
73
123
|
const r = tmux("display-message", "-p", "-t", name, "#{pane_current_command}");
|
|
74
124
|
return r.code === 0 ? r.stdout.trim() : "";
|
|
75
125
|
}
|
|
126
|
+
// session 名はファイルパス(logpath 等)と pipe-pane の /bin/sh 文字列へ流れる。英数 _ - のみ・64字に
|
|
127
|
+
// 限定し、パストラバーサル(../)とシェルインジェクション(' でのクオート破り・$・; 等)を全入口で断つ。
|
|
128
|
+
function assertSessionName(name) {
|
|
129
|
+
if (!/^[A-Za-z0-9_-]{1,64}$/.test(name))
|
|
130
|
+
throw new AitermError(`session 名は英数字と _ - のみ・64文字以内にしてください: ${JSON.stringify(name)}`, 2);
|
|
131
|
+
}
|
|
76
132
|
function logpath(name) {
|
|
77
133
|
return path.join(SOCKDIR, name + ".log");
|
|
78
134
|
}
|
|
@@ -111,8 +167,10 @@ function readLastcmd(name) {
|
|
|
111
167
|
}
|
|
112
168
|
}
|
|
113
169
|
export function attachHint(name) {
|
|
170
|
+
// Windows は WSL 内 tmux なので、人が打つのは wsl 経由(SOCK は WSL パス)。
|
|
171
|
+
const cmd = isWin ? `wsl tmux -S ${SOCK} attach -t ${name}` : `tmux -S ${SOCK} attach -t ${name}`;
|
|
114
172
|
return (`このセッションを自分の目で見る/介入する:\n` +
|
|
115
|
-
`
|
|
173
|
+
` ${cmd}\n` +
|
|
116
174
|
` (抜けるには Ctrl-b d)`);
|
|
117
175
|
}
|
|
118
176
|
// ---------------------------------------------------------------- 出力削減
|
|
@@ -204,6 +262,25 @@ function captureScreen(name, lines) {
|
|
|
204
262
|
return r.code === 0 ? r.stdout : "";
|
|
205
263
|
}
|
|
206
264
|
const sleep = (ms) => new Promise((res) => setTimeout(res, ms));
|
|
265
|
+
// Windows 専用: pipe-pane のログは /mnt/c (9p) 境界を越えて書かれるため、tmux が完了/セッション
|
|
266
|
+
// 消滅を報告した後も最後の数百バイトが少し遅れて現れる。完了と判定する直前にログサイズが
|
|
267
|
+
// 伸びなくなるまで待ち、readOutput が末尾欠けの出力を返さないようにする(POSIX は同一fsゆえ不要)。
|
|
268
|
+
async function settleWinLog(name) {
|
|
269
|
+
let prev = -1;
|
|
270
|
+
for (let i = 0; i < 8; i++) {
|
|
271
|
+
let sz = 0;
|
|
272
|
+
try {
|
|
273
|
+
sz = fs.statSync(logpath(name)).size;
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
sz = 0;
|
|
277
|
+
}
|
|
278
|
+
if (sz === prev)
|
|
279
|
+
return;
|
|
280
|
+
prev = sz;
|
|
281
|
+
await sleep(POLL * 1000);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
207
284
|
/** 完了境界の4層: dead / until 一致 / (出力静止 ∧ シェルに戻った) / timeout。 */
|
|
208
285
|
async function waitCompletion(name, untilRe, timeout) {
|
|
209
286
|
// 締切は単調時計で測る。Date.now() は NTP 補正やサスペンドで巻き戻り、長時間待ちで誤判定する(Python は time.monotonic)。
|
|
@@ -227,12 +304,18 @@ async function waitCompletion(name, untilRe, timeout) {
|
|
|
227
304
|
if (until.test(stripControl(neu)))
|
|
228
305
|
return [true, "until"];
|
|
229
306
|
}
|
|
230
|
-
if (!alive)
|
|
307
|
+
if (!alive) {
|
|
308
|
+
if (isWin)
|
|
309
|
+
await settleWinLog(name);
|
|
231
310
|
return [true, "dead"];
|
|
311
|
+
}
|
|
232
312
|
if (size === lastSize) {
|
|
233
313
|
stable++;
|
|
234
|
-
if (stable >= STABLE_POLLS && SHELLS.has(paneCurrentCommand(name)))
|
|
314
|
+
if (stable >= STABLE_POLLS && SHELLS.has(paneCurrentCommand(name))) {
|
|
315
|
+
if (isWin)
|
|
316
|
+
await settleWinLog(name);
|
|
235
317
|
return [true, "quiescent"];
|
|
318
|
+
}
|
|
236
319
|
}
|
|
237
320
|
else {
|
|
238
321
|
stable = 0;
|
|
@@ -247,7 +330,10 @@ function rtkRewrite(text) {
|
|
|
247
330
|
if (text.trim().includes("\n"))
|
|
248
331
|
return text;
|
|
249
332
|
// timeout 無しだと rtk がハングしたとき send() ごと凍結する。Python は timeout=5。
|
|
250
|
-
|
|
333
|
+
// Windows はコマンドが WSL 内で走るので rtk も WSL 側(-e)で評価する。不在は素通し。
|
|
334
|
+
const r = isWin
|
|
335
|
+
? spawnSync("wsl.exe", ["-e", "rtk", "rewrite", text], { encoding: "utf8", timeout: 5000, maxBuffer: 1024 * 1024 })
|
|
336
|
+
: spawnSync("rtk", ["rewrite", text], { encoding: "utf8", timeout: 5000, maxBuffer: 1024 * 1024 });
|
|
251
337
|
if (r.error)
|
|
252
338
|
return text; // rtk 不在(ENOENT)・タイムアウト(ETIMEDOUT)等は元テキストへ素通し
|
|
253
339
|
if ((r.status === 0 || r.status === 3) && (r.stdout ?? "").trim())
|
|
@@ -258,17 +344,24 @@ function rtkRewrite(text) {
|
|
|
258
344
|
export function openSession(name, shell = "bash") {
|
|
259
345
|
fs.mkdirSync(SOCKDIR, { recursive: true });
|
|
260
346
|
const nm = name || autoName();
|
|
347
|
+
assertSessionName(nm);
|
|
261
348
|
if (sessionExists(nm))
|
|
262
349
|
throw new AitermError(`session '${nm}' は既に存在します(list で確認)`, 2);
|
|
263
350
|
const r = tmux("new-session", "-d", "-s", nm, "-f", "/dev/null", shell);
|
|
264
351
|
if (r.code !== 0)
|
|
265
352
|
throw new AitermError("tmux new-session 失敗: " + r.stderr.trim(), 2);
|
|
266
353
|
fs.closeSync(fs.openSync(logpath(nm), "a")); // touch
|
|
267
|
-
|
|
354
|
+
// pipe-pane の引数は tmux 内部の /bin/sh -c で再解釈される(argv ではない)。パスは単一引用符で包み、
|
|
355
|
+
// パス自身の ' は '\'' イディオムでエスケープする(名前は検証済みだが、Windows ユーザー名 O'Brien 等が
|
|
356
|
+
// 一時パスに ' を持ち込み redirect を壊すのを防ぐ。空白対策も兼ねる)。Windows は WSL から見える /mnt/c 形へ。
|
|
357
|
+
const pipeTarget = isWin ? toWslPath(logpath(nm)) : logpath(nm);
|
|
358
|
+
const quoted = `'${pipeTarget.replace(/'/g, "'\\''")}'`;
|
|
359
|
+
tmux("pipe-pane", "-t", nm, "-o", `cat >> ${quoted}`);
|
|
268
360
|
writeOffset(nm, 0);
|
|
269
361
|
return [nm, attachHint(nm)];
|
|
270
362
|
}
|
|
271
363
|
export function send(name, text, o = {}) {
|
|
364
|
+
assertSessionName(name);
|
|
272
365
|
const enter = o.enter ?? true;
|
|
273
366
|
if (!sessionExists(name))
|
|
274
367
|
throw new AitermError(`session '${name}' が無い(open してください)`, 2);
|
|
@@ -295,6 +388,7 @@ export function send(name, text, o = {}) {
|
|
|
295
388
|
return `sent ${[...text].length} chars to ${name}` + (enter ? " (+Enter)" : "");
|
|
296
389
|
}
|
|
297
390
|
export function sendKey(name, key) {
|
|
391
|
+
assertSessionName(name);
|
|
298
392
|
if (!sessionExists(name))
|
|
299
393
|
throw new AitermError(`session '${name}' が無い`, 2);
|
|
300
394
|
const k = KEYMAP[key.toLowerCase()] ?? key;
|
|
@@ -302,6 +396,7 @@ export function sendKey(name, key) {
|
|
|
302
396
|
return `sent key ${k} to ${name}`;
|
|
303
397
|
}
|
|
304
398
|
export async function readOutput(name, o = {}) {
|
|
399
|
+
assertSessionName(name);
|
|
305
400
|
const timeout = o.timeout ?? DEFAULT_TIMEOUT;
|
|
306
401
|
if (!sessionExists(name) && !fs.existsSync(logpath(name)))
|
|
307
402
|
throw new AitermError(`session '${name}' が無い`, 2);
|
|
@@ -333,7 +428,11 @@ export async function readOutput(name, o = {}) {
|
|
|
333
428
|
}
|
|
334
429
|
}
|
|
335
430
|
else {
|
|
336
|
-
|
|
431
|
+
let off = readOffset(name);
|
|
432
|
+
// WSL 再起動等でログが作り直されると、Windows 側に残った旧 offset が新ログ長を超え、
|
|
433
|
+
// subarray が空を返して「何も読めない」状態になる。末尾越えは先頭から読み直す(POSIX では no-op)。
|
|
434
|
+
if (off > data.length)
|
|
435
|
+
off = 0;
|
|
337
436
|
text = data.subarray(off).toString("utf8");
|
|
338
437
|
if (o.lines)
|
|
339
438
|
text = text.split("\n").slice(-o.lines).join("\n");
|
|
@@ -373,6 +472,7 @@ export function listSessions() {
|
|
|
373
472
|
return "(セッション無し)";
|
|
374
473
|
}
|
|
375
474
|
export function closeSession(name) {
|
|
475
|
+
assertSessionName(name);
|
|
376
476
|
tmux("kill-session", "-t", name);
|
|
377
477
|
for (const p of [logpath(name), offsetpath(name), lastcmdpath(name)]) {
|
|
378
478
|
try {
|
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.2.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.2.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",
|