aiterm-mcp 0.19.0 → 0.19.2

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 CHANGED
@@ -308,7 +308,7 @@ consumer は `aiterm-runtime-errors snapshot` を読み、durable ingestion 後
308
308
 
309
309
  `pty_send` は送信前に破壊的コマンド(`rm -rf /`, `mkfs`, `dd of=/dev/…`, `DROP TABLE` 等)を遮断し(`force: true` で越える)、ESC・ブラケットペースト終端などをサニタイズする。`pty_read` は既定で制御文字を無害化して返す(`raw: true` はバイトをそのまま返す)。これは**サンドボックスではなく tripwire**([既知の制約](#既知の制約バグではなく仕様)参照)。
310
310
 
311
- 1回の `pty_send` が受理する本文はUTF-8で最大64KiB。同一sessionへの送信はaiterm processをまたいで直列化し、chunk同士の混線を防ぐ。macOSでは長いPTY入力の欠落を避けるためUTF-8境界を壊さない256-byte単位でtmux pasteし、Linux/WSLでは上限内を1回でpasteする。agent dispatch の paste はさらに tmux bracketed paste(`paste-buffer -p`)を使う: bracketed paste mode を要求している pane(vendor TUI)へは各 chunk を `ESC[200~/201~` で包んで届け、チャンク投入中のキー解釈による語中文字化け・submit 取り落としを抑える。通常の shell 送信は不変。途中chunkが失敗した場合は部分送信済みであることを明示し、自動でEnterを押さない。送信processの異常終了でlockが残った場合は送信前にfail-closedする。そのsessionを `pty_close` して作り直すか、全sessionを破棄できる場合だけ `pty_kill_all` で安全に掃除する。
311
+ 1回の `pty_send` が受理する本文はUTF-8で最大64KiB。同一sessionへの送信はaiterm processをまたいで直列化し、chunk同士の混線を防ぐ。macOSでは長いPTY入力の欠落を避けるためUTF-8境界を壊さない256-byte単位でtmux pasteし、Linux/WSLでは上限内を1回でpasteする。POSIX shellが前面にいる時のsanitize済み複数行は、改行を含まない単一の`eval`入力へ符号化する。shellがscript全体を所有してから先頭行を実行するため、途中で起動したpager/REPLが後続行を対話キーとして奪わない。単一行、`raw:true`、非shell前面は従来どおり直接PTYへpasteする。agent dispatch の paste はさらに tmux bracketed paste(`paste-buffer -p`)を使う: bracketed paste mode を要求している pane(vendor TUI)へは各 chunk を `ESC[200~/201~` で包んで届け、チャンク投入中のキー解釈による語中文字化け・submit 取り落としを抑える。途中chunkが失敗した場合は部分送信済みであることを明示し、自動でEnterを押さない。送信processの異常終了でlockが残った場合は送信前にfail-closedする。そのsessionを `pty_close` して作り直すか、全sessionを破棄できる場合だけ `pty_kill_all` で安全に掃除する。
312
312
 
313
313
  ## 人が覗く
314
314
 
package/README.md CHANGED
@@ -338,7 +338,7 @@ As of v0.16 a parent agent **never blocks** on aiterm — there is no wait param
338
338
 
339
339
  Before sending, `pty_send` blocks destructive commands (`rm -rf /`, `mkfs`, `dd of=/dev/…`, `DROP TABLE`, …) — pass `force: true` to override — and sanitizes ESC / bracketed-paste terminators. `pty_read` neutralizes control characters in what it returns by default (`raw: true` returns the bytes verbatim). This is a **tripwire, not a sandbox** (see [Known constraints](#known-constraints-by-design-not-bugs)).
340
340
 
341
- Each `pty_send` accepts at most 64 KiB of UTF-8 text. Sends to the same session are serialized across aiterm processes so chunks cannot interleave. On macOS, text is pasted through tmux in UTF-8-safe 256-byte chunks to avoid the platform PTY truncation observed with long input; Linux and WSL use one bounded paste. Agent dispatches additionally paste with tmux bracketed paste (`paste-buffer -p`): panes that requested bracketed-paste mode (the vendor TUIs) receive each chunk wrapped in `ESC[200~/201~`, hardening prompt injection against mid-word key-interpretation corruption and dropped submits; ordinary shell sends are unchanged. If a later chunk fails, aiterm reports the partial-send state and does not press Enter automatically. A lock left by a terminated sender fails closed before sending; close and recreate that session (or use `pty_kill_all` when every session is disposable) to clean it up safely.
341
+ Each `pty_send` accepts at most 64 KiB of UTF-8 text. Sends to the same session are serialized across aiterm processes so chunks cannot interleave. On macOS, text is pasted through tmux in UTF-8-safe 256-byte chunks to avoid the platform PTY truncation observed with long input; Linux and WSL use one bounded paste. Sanitized multiline text sent while a POSIX shell is in the foreground is encoded as one newline-free `eval` input: the shell receives the complete script before it runs the first line, so a pager or REPL started mid-script cannot consume later lines as interactive keystrokes. Single-line input, `raw:true`, and non-shell frontends remain direct PTY pastes. Agent dispatches additionally paste with tmux bracketed paste (`paste-buffer -p`): panes that requested bracketed-paste mode (the vendor TUIs) receive each chunk wrapped in `ESC[200~/201~`, hardening prompt injection against mid-word key-interpretation corruption and dropped submits. If a later chunk fails, aiterm reports the partial-send state and does not press Enter automatically. A lock left by a terminated sender fails closed before sending; close and recreate that session (or use `pty_kill_all` when every session is disposable) to clean it up safely.
342
342
 
343
343
  ## A human can watch
344
344
 
package/dist/core.js CHANGED
@@ -33,6 +33,11 @@ export const DEFAULT_TIMEOUT = 10.0;
33
33
  const POLL = 0.25;
34
34
  const STABLE_POLLS = 2; // 連続でログサイズ不変ならば静止とみなす回数
35
35
  const SHELLS = new Set(["bash", "sh", "zsh", "fish", "dash"]);
36
+ // 通常PTYへ複数行をそのままpasteすると、途中で起動したpager/REPLが後続行を
37
+ // キー入力として消費する。POSIX shell前面では改行を持たないeval 1行へ包み、
38
+ // script全体をshell内部へ取り込んでから実行する。fish等の非POSIX shellや
39
+ // ssh/REPL前面は従来の生pasteを維持する。
40
+ const ATOMIC_MULTILINE_SHELLS = new Set(["bash", "sh", "zsh", "dash"]);
36
41
  // mark の sentinel は POSIX シェル構文(; と "$?")に依存する。これらの非 POSIX 対話シェルが
37
42
  // 前面のときは "$?" が正しく展開されず sentinel が壊れるので mark を拒否する(B8)。ssh/docker で
38
43
  // リモート POSIX シェルに入っている場合は前面が "ssh"/"docker" 等で本集合に含まれず=許可される。
@@ -307,6 +312,19 @@ function splitPtyText(text) {
307
312
  chunks.push(chunk);
308
313
  return chunks;
309
314
  }
315
+ function quoteForPrintfB(text) {
316
+ return text
317
+ .replace(/\\/g, "\\\\")
318
+ .replace(/\n/g, "\\n")
319
+ .replace(/\r/g, "\\r")
320
+ .replace(/\t/g, "\\t")
321
+ .replace(/'/g, `'"'"'`);
322
+ }
323
+ function atomicShellMultiline(text) {
324
+ // POSIX printf %bで元の改行・backslashを復元し、evalは現在shell内で実行する。
325
+ // command substitutionが末尾LFを落とす点は、pty_sendのsubmitを担うEnterと同値。
326
+ return `eval "$(command printf '%b' '${quoteForPrintfB(text)}')"`;
327
+ }
310
328
  function assertSendTextSize(text, context = "送信文字列") {
311
329
  const bytes = Buffer.byteLength(text, "utf8");
312
330
  if (bytes > MAX_SEND_BYTES) {
@@ -960,6 +978,10 @@ export function send(name, text, o = {}) {
960
978
  if (o.mark)
961
979
  text = text + `; printf '\\n<<<AITERM_DONE rc=%d>>>\\n' "$?"`;
962
980
  assertSendTextSize(text, o.rtk || o.mark ? "変換後の送信文字列" : "送信文字列");
981
+ const reportedText = text;
982
+ if (!o.raw && text.includes("\n") && ATOMIC_MULTILINE_SHELLS.has(paneCurrentCommand(name))) {
983
+ text = atomicShellMultiline(text);
984
+ }
963
985
  if (o.mark) {
964
986
  try {
965
987
  fs.writeFileSync(markpath(name), "1"); // waitCompletion に sentinel 完了検出を有効化させる
@@ -1018,7 +1040,7 @@ export function send(name, text, o = {}) {
1018
1040
  }
1019
1041
  }
1020
1042
  // コードポイント数で数える(JS の .length は UTF-16 単位で絵文字等がズレる。Python は len()=コードポイント)。
1021
- return `sent ${[...text].length} chars to ${name}` + (enter ? " (+Enter)" : "");
1043
+ return `sent ${[...reportedText].length} chars to ${name}` + (enter ? " (+Enter)" : "");
1022
1044
  }
1023
1045
  finally {
1024
1046
  releaseSendLock();
@@ -1212,16 +1234,16 @@ export function listSessions() {
1212
1234
  * session 名・前面コマンド・PTY 出力を診断応答へ持ち出さないため、factory が安全に readiness を
1213
1235
  * 見られる。socket 不在は「セッション未設定」であって障害ではない。
1214
1236
  */
1215
- export function readOnlyPtyListDiagnostic() {
1237
+ export function readOnlyPtyListDiagnostic(runTmux = tmux) {
1216
1238
  try {
1217
- const r = tmux("list-sessions", "-F", "#{session_name}");
1239
+ const r = runTmux("list-sessions", "-F", "#{session_name}");
1218
1240
  if (r.code === 0) {
1219
1241
  return { status: "ready", session_count: r.stdout.split(/\r?\n/).filter(Boolean).length };
1220
1242
  }
1221
1243
  // tmux は専用 socket に server がいない通常状態を exit 1 で返す。その他の失敗を「空」と
1222
1244
  // 偽装しないため、メッセージを公開せず unverified に留める。
1223
1245
  if (/no server running|failed to connect/i.test(r.stderr)) {
1224
- return { status: "not_applicable", session_count: 0 };
1246
+ return { status: "not_applicable", session_count: null };
1225
1247
  }
1226
1248
  }
1227
1249
  catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiterm-mcp",
3
- "version": "0.19.0",
3
+ "version": "0.19.2",
4
4
  "mcpName": "io.github.kitepon-rgb/aiterm-mcp",
5
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. Also launches interactive Claude/Codex/Grok/Composer agent TUIs in a persistent terminal. Token-reducing reads.",
6
6
  "keywords": [