aiterm-mcp 0.29.30 → 0.30.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/CHANGELOG.md CHANGED
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.30.0] - 2026-09-04
11
+
12
+ ### Added
13
+
14
+ - `pty_send`(agent dispatch)、`agent_steer`、`agent_launch`に`image`(画像ファイルの絶対パスの配列)を追加した。aitermが本文末尾へ添付行を付け、Claude Code/Codex/Grok/Cursorはいずれも自分のfile読取toolでそのpathを画像として開く(実測 2026-09-04)。呼出し側が入力欄へパスを先打鍵する等のharness別手順を持つ必要を無くす。相対パス・未対応拡張子・不在fileは送信前にtyped errorで拒否し、通常PTY送信とforce送信では指定できない。
15
+
16
+ ## [0.29.31] - 2026-09-04
17
+
18
+ ### Fixed
19
+
20
+ - agent session への打鍵前に、pane tty 上の agent が前面プロセスグループでなければ `fg` で前面へ戻し、tty が cooked(icanon/echo)なら `raw -echo` へ戻してから送る(`pty_send` dispatch・起動時 prompt・`agent_steer`)。Codex 0.153 はツール実行後に前面を bash へ返したまま動き続ける/termios を cooked のまま残すことがあり、貼付本文が bash に落ちて `^[[200~` が生で残っていた(実測 2026-09-04)。dispatch receipt に `pane_input_recovery` を追加。判定は `ps -t` の STAT と `stty -a` だけを使う。
21
+
22
+ ### Changed
23
+
24
+ - push時のCIをLinux 1環境へ絞り、WindowsはWindows固有ファイルの変更時だけ、3環境の全テストは週1回の定期実行だけにした。tag起点のnpm公開はmain CIの成功を待たず、既定ブランチの祖先確認だけで進める。
25
+ - `npm run release -- <version>`でversion同期・CHANGELOG見出し・commit・tag・MCPB・GitHub Releaseを一回で行う。
26
+
10
27
  ## [0.29.30] - 2026-09-02
11
28
 
12
29
  ### Fixed
@@ -1529,7 +1546,9 @@ prototype (preserved under `prototype/python/` as the porting source and referen
1529
1546
  `ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
1530
1547
  provenance.
1531
1548
 
1532
- [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.29.30...HEAD
1549
+ [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.30.0...HEAD
1550
+ [0.30.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.29.31...v0.30.0
1551
+ [0.29.31]: https://github.com/kitepon/aiterm-mcp/compare/v0.29.30...v0.29.31
1533
1552
  [0.29.30]: https://github.com/kitepon/aiterm-mcp/compare/v0.29.29...v0.29.30
1534
1553
  [0.29.29]: https://github.com/kitepon/aiterm-mcp/compare/v0.29.28...v0.29.29
1535
1554
  [0.29.28]: https://github.com/kitepon/aiterm-mcp/compare/v0.29.27...v0.29.28
package/README.ja.md CHANGED
@@ -153,7 +153,7 @@ runtime-error store は canonical dotagents config の `collection.enabled: true
153
153
  場合だけ収集し、既定OFF、network送信は行いません。tag起点CIのnpm provenance(OIDC Trusted
154
154
  Publishing)で公開し、GitHub Release が Official MCP Registry を再登録します。
155
155
 
156
- **状態:** 開発継続中 · 現行公開版 **v0.29.30** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
156
+ **状態:** 開発継続中 · 現行公開版 **v0.30.0** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
157
157
 
158
158
  ### 更新と巻き戻し
159
159
 
@@ -189,6 +189,8 @@ pty_read(id, { wait: true }) → 削減済みの出力を読む(完了
189
189
 
190
190
  `aiterm.agent-launch-result.v1`は正規`harness`を返し、旧`provider`は互換fieldとして残す。同じ`harness`はagent dispatch、`aiterm-wait`、`agent_configure`、`pty_list`のagent行にも載り、旧vendor/provider/agent fieldは互換用に残る。Codexは通常rollout、Grok CLIは通常session event、Claudeはlaunch固有Stop hook、Cursorは通常agent transcript末尾の`turn_ended`を完了正本に使う。`pty_send`は非ブロックdispatchで、vendor別完了境界を表すopaqueな整数`event_cursor`を返し、完了通知は`aiterm-wait`を親のターンを塞がない別processで受ける。Cursorのsubmitキーはadapterが現行CLIのextended keyboard protocolへ変換する。送信textがCursorのcomposerへ残った場合は成功receiptを返さず失敗する。
191
191
 
192
+ `agent_launch`・`pty_send`(agent dispatch)・`agent_steer`は任意の`image`(画像ファイルの絶対パスの配列。png/jpg/jpeg/gif/webp)を受ける。aitermが本文末尾へ添付行を付け、どのharnessも自分のfile読取toolでそのpathを画像として開く。呼出し側はharness別の添付手順を覚えない。不正なpathは送信前に拒否する。
193
+
192
194
  `agent_launch`は任意の`write_scope`も受ける。Codex/Grokのread-onlyは`--sandbox read-only`、Cursorは公式`--mode ask`で実効化する。path説明は同等CLI引数がないためdeclaration-only。
193
195
 
194
196
  ```text
@@ -496,10 +498,11 @@ npm test # build してから node:test 回帰スイート(tmux ま
496
498
  npm link # ローカルで `aiterm-mcp` を PATH に
497
499
  ```
498
500
 
499
- 開発中は変更に直結するfocused testを先にローカルで実行します。GitHub Actionsは実装の依存関係から
500
- 必要なテストを選び、依存を確定できない変更とrelease変更だけ3環境の全テストへ広げます。
501
- tag起点のnpm公開は、同じcommitのmain CIが3環境greenであり、tagged commitが`origin/main`の
502
- 祖先であることを確認した後だけ実行します。tag側で同じ3環境試験は繰り返しません。
501
+ 開発中は変更に直結するfocused testを先にローカルで実行します。GitHub Actionsはpushごとにself-hostedの
502
+ `linux-workstation` 1環境で試験を回し、Windows固有ファイルを触った変更だけ`windows-native`を加え、
503
+ 3環境(`macos-native`、`linux-workstation`、`windows-native`)の全テストは週1回の健康診断だけで回します。
504
+ `npm run release -- <version>`がversion同期・commit・tag・GitHub Releaseを一回で行い、tag起点のnpm公開は
505
+ tagged commitが`origin/main`の祖先であることだけを確認して、他のCI結果を待ちません。
503
506
 
504
507
  共通進行は`src/core.ts`、harness固有は`src/harnesses/`、OS差は`src/tmux-runtime.ts`/`src/agent-resolver.ts`、reducerは`src/rtk.ts`、公開面は`src/index.ts`が所有する。現行設計は[`docs/DESIGN.md`](docs/DESIGN.md)、release手順は[`docs/RELEASE.md`](docs/RELEASE.md)を正とする。`prototype/python/`はreducerの歴史的移植元であり、pytest reducerは本家rtk 0.42.0と一致する(上記の`FAILED`行の差異だけは意図的・回帰テストで固定)。
505
508
 
package/README.md CHANGED
@@ -169,7 +169,7 @@ collection is off by default and performs no network I/O. It ships via
169
169
  tag-triggered CI with npm provenance (OIDC Trusted Publishing); the GitHub
170
170
  Release re-registers the Official MCP Registry entry.
171
171
 
172
- **Status:** actively maintained · current public release **v0.29.30** · runs on Linux · WSL2 · macOS · native Windows (tmux on POSIX, the tmux-CLI-compatible [psmux](https://github.com/psmux/psmux) on native Windows — no WSL required) · MIT · see the [CHANGELOG](CHANGELOG.md).
172
+ **Status:** actively maintained · current public release **v0.30.0** · runs on Linux · WSL2 · macOS · native Windows (tmux on POSIX, the tmux-CLI-compatible [psmux](https://github.com/psmux/psmux) on native Windows — no WSL required) · MIT · see the [CHANGELOG](CHANGELOG.md).
173
173
 
174
174
  ### Update and rollback
175
175
 
@@ -211,6 +211,8 @@ The same primitive hosts another agent's TUI. `agent_launch` starts a selected e
211
211
 
212
212
  The human-readable launch text is accompanied by an `aiterm.agent-launch-result.v1` structured receipt containing the canonical `harness`; the old `provider` field remains for compatibility. The same `harness` is carried by agent dispatch, `aiterm-wait`, `agent_configure`, and agent rows in `pty_list`, while their old vendor/provider/agent fields remain compatibility fields. Codex completion comes from its normal durable rollout transcript, Grok CLI from its normal session events, Claude Code from a launch-specific Stop hook settings addition, and Cursor from its normal agent transcript's terminal `turn_ended` record. Sending to any agent session is a non-blocking **dispatch** — the call returns immediately with an opaque, harness-specific integer `event_cursor`, and completion arrives via [`aiterm-wait`](#completion-push-for-parent-agents-aiterm-wait). The Cursor adapter translates submit into the current CLI's extended keyboard protocol. If submitted text remains in Cursor's composer, its dispatch fails instead of returning a successful receipt.
213
213
 
214
+ `agent_launch`, `pty_send` (agent dispatch), and `agent_steer` accept an optional `image`: an array of absolute paths to image files (png/jpg/jpeg/gif/webp). Aiterm appends an attachment block to the prompt, and every harness opens the path with its own file-reading tool and sees the image; the caller never learns harness-specific attachment tricks. Invalid paths are rejected before anything is sent.
215
+
214
216
  `agent_launch` accepts an optional `write_scope`: either `"read-only"` or a human-readable description of writable paths. Codex/Grok use `--sandbox read-only`; Cursor uses its official read-only `--mode ask`. A path description remains declaration-only because these CLI launch surfaces provide no equivalent path allowlist flag.
215
217
 
216
218
  For a correlated Claude turn stopped at `Do you want to proceed?`, use `claude_approval(action: "inspect", ...)` to capture the active operation and SHA-256 screen digest, review the displayed command, then call `respond` with that exact digest and either `approve_once` or `deny`. The relay rechecks the operation and screen under the send lock, never exposes arbitrary input or permanent approval, keeps the active marker intact, and records a prompt-free owner-only receipt. `pty_send(force: true)` does not bypass this boundary.
@@ -542,11 +544,12 @@ npm test # build, then the node:test regression suite (requires tmux o
542
544
  npm link # put `aiterm-mcp` on PATH locally
543
545
  ```
544
546
 
545
- Development uses focused local tests first. GitHub Actions selects tests from the changed implementation's
546
- dependency graph and expands unknown or release changes to the full suite on the self-hosted
547
- `macos-native`, `linux-workstation`, and `windows-native` runners. Tag-triggered npm publishing reuses
548
- the successful main CI for the same commit instead of repeating it, and runs only after the tagged commit
549
- is confirmed on `origin/main`. The native
547
+ Development uses focused local tests first. GitHub Actions runs the suite on the self-hosted
548
+ `linux-workstation` runner for every push, adds `windows-native` only when Windows-specific files change,
549
+ and runs all three runners (`macos-native`, `linux-workstation`, `windows-native`) once a week as a health
550
+ check. `npm run release -- <version>` syncs the version, commits, tags, and publishes the GitHub Release in
551
+ one command; tag-triggered npm publishing checks only that the tagged commit is on `origin/main` and does not
552
+ wait for another CI run. The native
550
553
  Windows runner needs psmux ≥ 3.3.8 and Git for Windows on its PATH, and must run as an
551
554
  interactive Windows user; `NETWORK SERVICE` lacks the per-user environment the pane shell and
552
555
  harness CLIs rely on and is not a valid runner identity.
package/dist/core.js CHANGED
@@ -108,6 +108,129 @@ function paneCurrentCommand(name) {
108
108
  return "";
109
109
  return normalizePaneCommand(r.stdout.trim());
110
110
  }
111
+ // ---- pane 入力の到達性(agent が tty の前面か・tty が raw か)----
112
+ // 打鍵が届く先は pane tty の前面プロセスグループだけであり、TUI は raw termios で読む。
113
+ // Codex 0.153 はツール実行のあと、①前面を子 shell(bash)へ返したまま agent が背面(STAT S)で
114
+ // 動き続ける、②前面へ戻っても termios を子 shell の cooked(icanon/echo)のまま残す、の2形を出す
115
+ // (実測 2026-09-04: Codex 席3つが順に該当。貼付本文が bash に落ちて `^[[200~` が生で残り、
116
+ // 打鍵が行バッファに溜まって `^L` が echo された)。どちらも PTY の状態であり aiterm が所有する。
117
+ // 停止(T)だけでなく背面で動く(S)agent も `fg` で前面へ戻せる。判定は OS が直接教える
118
+ // `ps -t <tty>` の STAT `+`/`T` と `stty -f <tty> -a` だけを使い、画面の文字列で推測しない。
119
+ const PANE_TTY_SHELLS = new Set([...SHELLS, "tcsh", "csh", "ksh"]);
120
+ const AGENT_FOREGROUND_RECOVERY_TIMEOUT_MS = 3_000;
121
+ const AGENT_FOREGROUND_RECOVERY_POLL_MS = 200;
122
+ // harness の実プロセスを command 行から見分ける。agent が起動した子(bash -lc、ssh、git 等)は agent ではない。
123
+ const AGENT_COMMAND_PATTERNS = {
124
+ codex: /(^|[\s/])codex([\s]|$)/,
125
+ claude: /(^|[\s/])claude([\s]|$)/,
126
+ grok: /(^|[\s/])grok(-[^\s/]+)?([\s]|$)/,
127
+ composer: /(^|[\s/])(composer|grok(-[^\s/]+)?)([\s]|$)/,
128
+ cursor: /(^|[\s/])(cursor-agent|agent)([\s]|$)/,
129
+ };
130
+ /**
131
+ * `ps -t <tty> -o stat=,command=` の出力を分類する(純粋関数)。
132
+ * ログイン shell は command が `-zsh` のように先頭 `-` 付きで出るので剥がして判定する。
133
+ */
134
+ export function classifyPaneTtyProcesses(psOutput, agentPattern = /./) {
135
+ const state = {
136
+ agentPresent: false, agentForeground: false, agentStopped: false, toolForeground: false, foregroundShell: null,
137
+ };
138
+ for (const raw of psOutput.split("\n")) {
139
+ const m = raw.trim().match(/^(\S+)\s+(.*)$/);
140
+ if (!m)
141
+ continue;
142
+ const stat = m[1];
143
+ const command = m[2];
144
+ const base = ((command.split(/\s+/, 1)[0] ?? "").split("/").pop() ?? "").replace(/^-/, "");
145
+ if (PANE_TTY_SHELLS.has(base)) {
146
+ // session leader(STAT の `s`)が pane の login shell。agent が起動した子 shell(`bash -lc …`)は
147
+ // `s` を持たない。前面の子 shell は「ツール実行中」であり、fg も stty も触らない。
148
+ if (stat.includes("+")) {
149
+ if (stat.includes("s"))
150
+ state.foregroundShell = base;
151
+ else
152
+ state.toolForeground = true;
153
+ }
154
+ continue;
155
+ }
156
+ if (!agentPattern.test(command)) {
157
+ if (stat.includes("+"))
158
+ state.toolForeground = true;
159
+ continue;
160
+ }
161
+ state.agentPresent = true;
162
+ if (stat.includes("+"))
163
+ state.agentForeground = true;
164
+ if (stat.startsWith("T"))
165
+ state.agentStopped = true;
166
+ }
167
+ return state;
168
+ }
169
+ /** `stty -a` の出力が cooked(icanon または echo が有効)かを判定する(純粋関数)。 */
170
+ export function termiosIsCooked(sttyOutput) {
171
+ const tokens = sttyOutput.split(/[\s;]+/);
172
+ return tokens.includes("icanon") || tokens.includes("echo");
173
+ }
174
+ function paneTtyOf(name) {
175
+ const r = tmux("display-message", "-p", "-t", name, "#{pane_tty}");
176
+ if (r.code !== 0)
177
+ return null;
178
+ const tty = r.stdout.trim();
179
+ return tty.startsWith("/dev/") ? tty : null;
180
+ }
181
+ /**
182
+ * agent session への打鍵前に、agent が tty の前面で raw で読める状態にする。
183
+ * 戻り値は行った回復("fg" / "fg_stopped" / "stty_raw")。何もしなければ空。
184
+ * agent が tty 上に実在しない時は触らない(ready gate が入力受付不能として止める)。
185
+ */
186
+ export async function ensureAgentOwnsPaneInput(name, kind) {
187
+ if (isWin)
188
+ return [];
189
+ const tty = paneTtyOf(name);
190
+ if (!tty)
191
+ return [];
192
+ const ttyId = tty.replace(/^\/dev\//, "");
193
+ const psEnv = { ...process.env, LC_ALL: "C" };
194
+ const observe = () => classifyPaneTtyProcesses(spawnSync("/bin/ps", ["-t", ttyId, "-o", "stat=,command="], { encoding: "utf8", timeout: 5000, env: psEnv }).stdout ?? "", AGENT_COMMAND_PATTERNS[kind]);
195
+ const recovery = [];
196
+ let state = observe();
197
+ // agent が起動したツール/子 shell が前面なら、その tty 状態はツールの物。fg も stty も触らず ready gate に任せる。
198
+ if (state.toolForeground)
199
+ return [];
200
+ if (state.agentPresent && !state.agentForeground) {
201
+ const wasStopped = state.agentStopped;
202
+ tmux("send-keys", "-t", name, "C-u");
203
+ tmux("send-keys", "-l", "-t", name, "fg");
204
+ tmux("send-keys", "-t", name, "Enter");
205
+ const deadline = performance.now() + AGENT_FOREGROUND_RECOVERY_TIMEOUT_MS;
206
+ for (;;) {
207
+ await sleep(AGENT_FOREGROUND_RECOVERY_POLL_MS);
208
+ state = observe();
209
+ if (state.agentForeground)
210
+ break;
211
+ if (performance.now() >= deadline) {
212
+ throw new AitermError(`AGENT_TUI_BACKGROUNDED session=${name} foreground=${state.foregroundShell ?? "不明"}\n` +
213
+ "pane tty 上に agent が実在するが前面プロセスグループでなく、fg でも前面へ戻りません。" +
214
+ "打鍵は shell に落ちるため送信していません。pty_read(screen:true) で画面を確認してください。", 2);
215
+ }
216
+ }
217
+ recovery.push(wasStopped ? "fg_stopped" : "fg");
218
+ }
219
+ if (state.agentPresent && !state.toolForeground) {
220
+ // 別 tty を指す flag は macOS/BSD が `-f`、Linux(coreutils)が `-F`。OS 差はここ一箇所で吸収する。
221
+ const ttyFlag = process.platform === "linux" ? "-F" : "-f";
222
+ const stty = spawnSync("/bin/stty", [ttyFlag, tty, "-a"], { encoding: "utf8", timeout: 5000, env: psEnv });
223
+ if (stty.status === 0 && termiosIsCooked(stty.stdout ?? "")) {
224
+ const set = spawnSync("/bin/stty", [ttyFlag, tty, "raw", "-echo"], { encoding: "utf8", timeout: 5000, env: psEnv });
225
+ if (set.status !== 0) {
226
+ throw new AitermError(`AGENT_TTY_COOKED session=${name}\n` +
227
+ `tty が icanon/echo のままで raw へ戻せません: ${(set.stderr ?? "").trim() || `code=${set.status}`}。送信していません。`, 2);
228
+ }
229
+ recovery.push("stty_raw");
230
+ }
231
+ }
232
+ return recovery;
233
+ }
111
234
  function paneCurrentCommandForMark(name) {
112
235
  const foreground = paneCurrentCommand(name);
113
236
  if (!isWin || foreground === "powershell" || foreground === "pwsh")
@@ -2658,6 +2781,9 @@ export async function sendInitialAgentPrompt(name, text, o = {}) {
2658
2781
  throw new AitermError(`agent session '${name}' は起動時 prompt の完了待ちです。初回応答完了後に再度操作してください。`, 2);
2659
2782
  }
2660
2783
  setInitialPromptState(meta, "not_sent");
2784
+ // 起動直後の hooks 実行で bash が前面に残ると ready gate が恒久 false になり、brief 未送信で
2785
+ // 席が巻き戻る(実測 2026-09-04: Codex 0.153 の初回起動が3回中2回)。打鍵の前に前面と raw を整える。
2786
+ await ensureAgentOwnsPaneInput(name, meta.kind);
2661
2787
  let ready = await waitAgentTuiReady(name, meta, o.ready_timeout ?? AGENT_TUI_READY_TIMEOUT_MS);
2662
2788
  // 無人Claude起動でCLI自身が順に出す、bypass mode確認とworkspace trustだけを
2663
2789
  // 起動処理の一部として進める。通常turn中の権限確認やMCP承認には触れない。
@@ -2923,6 +3049,37 @@ export function __testCodexConfigureChoices(screen, model, effort) {
2923
3049
  // v0.16.0: 親をブロックする wait 経路は廃止した。send は ready gate と submit 分離を内蔵した
2924
3050
  // dispatch として即返り、event_cursor(送信直前のharness完了正本境界)を receipt で返す。
2925
3051
  // 完了通知は aiterm-wait(--cursor で境界を渡す)、回収は pty_read / claude_turn recover が担う。
3052
+ const IMAGE_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp"]);
3053
+ /**
3054
+ * 画像添付。Claude Code/Codex/Grok/Cursorの4 harnessは、本文に書かれた画像ファイルの絶対パスを
3055
+ * 自分のfile読取toolで開いて画像として見る(実測 2026-09-04: 赤青の試験画像を全harnessが正しく答えた)。
3056
+ * 入力欄へパスを先打鍵する等のharness別操作は不要であり、呼出し側にharnessの癖を覚えさせない。
3057
+ * 添付の表現とpath検査はこの1箇所だけが所有する。検査は外部入力(呼出し側が渡すpath)の境界。
3058
+ */
3059
+ export function attachImages(text, images) {
3060
+ if (!images || images.length === 0)
3061
+ return text;
3062
+ const lines = images.map((image, index) => {
3063
+ if (typeof image !== "string" || !path.isAbsolute(image)) {
3064
+ throw new AitermError(`image[${index}] は画像ファイルの絶対パスで指定してください: ${String(image)}`, 2);
3065
+ }
3066
+ if (!IMAGE_EXTENSIONS.has(path.extname(image).toLowerCase())) {
3067
+ throw new AitermError(`image[${index}] の拡張子に対応していません(png/jpg/jpeg/gif/webp): ${image}`, 2);
3068
+ }
3069
+ let st;
3070
+ try {
3071
+ st = fs.statSync(image);
3072
+ }
3073
+ catch {
3074
+ throw new AitermError(`image[${index}] が読めません: ${image}`, 2);
3075
+ }
3076
+ if (!st.isFile())
3077
+ throw new AitermError(`image[${index}] はfileではありません: ${image}`, 2);
3078
+ return `[aiterm 添付画像 ${index + 1}/${images.length}] ${image}`;
3079
+ });
3080
+ const body = text.trim().length > 0 ? text : "添付画像を確認してください。";
3081
+ return `${body}\n\n${lines.join("\n")}\n添付画像は上のファイルを読んで確認する。`;
3082
+ }
2926
3083
  export async function dispatchAgentTurn(name, text, o = {}) {
2927
3084
  assertSessionName(name);
2928
3085
  const meta = loadAgentMetadata(name);
@@ -2941,6 +3098,7 @@ export async function dispatchAgentTurn(name, text, o = {}) {
2941
3098
  const claudeColdStart = meta.kind === "claude"
2942
3099
  && agentCompletionCursor(meta) === 0
2943
3100
  && readClaudeOperationMarker(meta) === null;
3101
+ const paneInputRecovery = await ensureAgentOwnsPaneInput(name, meta.kind);
2944
3102
  if (meta.kind !== "claude" || claudeColdStart) {
2945
3103
  const ready = await waitAgentTuiReady(name, meta, o.ready_timeout ?? AGENT_TUI_READY_TIMEOUT_MS);
2946
3104
  if (!ready.ready) {
@@ -3001,6 +3159,7 @@ export async function dispatchAgentTurn(name, text, o = {}) {
3001
3159
  event_cursor: startOffset,
3002
3160
  operation_id: operationId,
3003
3161
  submit_residue: residue.residue,
3162
+ pane_input_recovery: paneInputRecovery,
3004
3163
  };
3005
3164
  }
3006
3165
  export async function steerAgentTurn(name, text) {
@@ -3016,8 +3175,11 @@ export async function steerAgentTurn(name, text) {
3016
3175
  vendor: meta.kind,
3017
3176
  harness: agentHarness(meta.kind),
3018
3177
  };
3019
- if (!isAgentTuiBusy(meta.kind, captureScreen(name, AGENT_TUI_READY_LINES)))
3020
- return { ...receipt, delivery: "idle" };
3178
+ // 前面回復は busy 判定より先(bash 前面のままだと画面の実行中マーカーを読んでも打鍵が届かない)。
3179
+ const paneInputRecovery = await ensureAgentOwnsPaneInput(name, meta.kind);
3180
+ if (!isAgentTuiBusy(meta.kind, captureScreen(name, AGENT_TUI_READY_LINES))) {
3181
+ return { ...receipt, delivery: "idle", pane_input_recovery: paneInputRecovery };
3182
+ }
3021
3183
  send(name, text, {
3022
3184
  enter: false,
3023
3185
  force: true,
@@ -3028,7 +3190,7 @@ export async function steerAgentTurn(name, text) {
3028
3190
  });
3029
3191
  await sleep(AGENT_SUBMIT_DELAY_MS);
3030
3192
  sendKey(name, "Enter");
3031
- return { ...receipt, delivery: "steered" };
3193
+ return { ...receipt, delivery: "steered", pane_input_recovery: paneInputRecovery };
3032
3194
  }
3033
3195
  export async function runClaudeOperation({ session_id: name, action, operation_id: operationIdInput, text, }) {
3034
3196
  assertSessionName(name);
package/dist/index.js CHANGED
@@ -136,6 +136,11 @@ server.registerTool("pty_send", {
136
136
  .describe("非Claude agent sessionでは自動dispatchせず素送信する。aiterm相関付きClaudeのactive turnには使えない"),
137
137
  rtk: z.boolean().default(false).describe("既知コマンドを rtk 形へ委譲して送る(rtk 不在なら素通し)"),
138
138
  raw: z.boolean().default(false).describe("送信前サニタイズを無効化"),
139
+ image: z
140
+ .array(z.string())
141
+ .optional()
142
+ .describe("添付する画像ファイルの絶対パス(png/jpg/jpeg/gif/webp)。agent session への dispatch だけで使え、" +
143
+ "harness別の添付手順はaitermが吸収する。通常PTY送信やforce送信では指定できない"),
139
144
  },
140
145
  outputSchema: {
141
146
  schema: z.literal("aiterm.pty-send-result.v1"),
@@ -149,8 +154,10 @@ server.registerTool("pty_send", {
149
154
  // dispatch後のsubmit座礁観測(additive)。true=composerに送信textの残存を確認(submit未成立の疑い)/
150
155
  // false=残存を観測せず(成立の保証ではない)/ null=通常送信・判定不能。
151
156
  submit_residue: z.boolean().nullable(),
157
+ // dispatch前に行った pane 入力の回復("fg" / "fg_stopped" / "stty_raw")。通常送信では省略(additive)。
158
+ pane_input_recovery: z.array(z.string()).optional(),
152
159
  },
153
- }, async ({ session_id, text, enter, mark, force, rtk, raw }) => {
160
+ }, async ({ session_id, text, enter, mark, force, rtk, raw, image }) => {
154
161
  try {
155
162
  if (!force && core.isAgentSession(session_id)) {
156
163
  if (enter === false)
@@ -159,7 +166,7 @@ server.registerTool("pty_send", {
159
166
  throw new Error("agent session への dispatch は mark:true と併用できません");
160
167
  if (rtk)
161
168
  throw new Error("agent session への dispatch は rtk:true と併用できません");
162
- const receipt = await core.dispatchAgentTurn(session_id, text, { raw });
169
+ const receipt = await core.dispatchAgentTurn(session_id, core.attachImages(text, image), { raw });
163
170
  const waitProcess = core.agentWaitProcess(receipt.session_id, receipt.event_cursor);
164
171
  return {
165
172
  content: [
@@ -180,9 +187,13 @@ server.registerTool("pty_send", {
180
187
  vendor: receipt.vendor,
181
188
  harness: receipt.harness,
182
189
  submit_residue: receipt.submit_residue,
190
+ pane_input_recovery: receipt.pane_input_recovery,
183
191
  },
184
192
  };
185
193
  }
194
+ if (image && image.length > 0) {
195
+ throw new Error("image は agent session への dispatch(forceなし)だけで使えます。通常PTY送信では本文にpathを書いてください");
196
+ }
186
197
  const out = core.send(session_id, text, { enter, mark, force, rtk, raw });
187
198
  return {
188
199
  content: [{ type: "text", text: out }],
@@ -209,6 +220,7 @@ server.registerTool("agent_steer", {
209
220
  inputSchema: {
210
221
  session_id: z.string(),
211
222
  text: z.string().describe("現在のターンへ追加する文字列。UTF-8で最大64KiB"),
223
+ image: z.array(z.string()).optional().describe("添付する画像ファイルの絶対パス(png/jpg/jpeg/gif/webp)"),
212
224
  },
213
225
  outputSchema: {
214
226
  schema: z.literal("aiterm.agent-steer.v1"),
@@ -218,9 +230,9 @@ server.registerTool("agent_steer", {
218
230
  harness: z.enum(["codex-cli", "grok-cli"]),
219
231
  delivery: z.enum(["steered", "idle"]),
220
232
  },
221
- }, async ({ session_id, text }) => {
233
+ }, async ({ session_id, text, image }) => {
222
234
  try {
223
- const receipt = await core.steerAgentTurn(session_id, text);
235
+ const receipt = await core.steerAgentTurn(session_id, core.attachImages(text, image));
224
236
  return {
225
237
  content: [{ type: "text", text: `${receipt.delivery} ${receipt.session_id}` }],
226
238
  structuredContent: receipt,
@@ -544,13 +556,14 @@ const agentEnvironmentDesc = `通常CLIと同じHOME・cwd・project/user/local
544
556
  `delegation depth/lineage、delegation_allowed=trueを注入し、必要な追加委譲は許可する。`;
545
557
  async function launchAgent(kind, args) {
546
558
  const supportsWriteScope = kind !== "claude";
547
- const { prompt, throughline_source_session, throughline_supplement_file, model, reasoning_effort, env_vars, cwd, session_name, launch_operation_id, write_scope } = args;
559
+ const { prompt, image, throughline_source_session, throughline_supplement_file, model, reasoning_effort, env_vars, cwd, session_name, launch_operation_id, write_scope } = args;
548
560
  try {
549
561
  if (!supportsWriteScope && write_scope !== undefined) {
550
562
  throw new core.AitermError("claude-code harnessはwrite_scopeに対応していません。指定を外してください", 2);
551
563
  }
564
+ const initialPrompt = image && image.length > 0 ? core.attachImages(prompt ?? "", image) : prompt ?? undefined;
552
565
  const [sid, hint, eventCursor, submitResidue] = await core.openAgentWithInitialPrompt(kind, {
553
- prompt: prompt ?? undefined,
566
+ prompt: initialPrompt,
554
567
  throughline_source_session,
555
568
  throughline_supplement_file,
556
569
  model: model ?? undefined,
@@ -660,6 +673,7 @@ server.registerTool("agent_launch", {
660
673
  inputSchema: {
661
674
  harness: z.enum(["claude-code", "codex-cli", "grok-cli", "cursor-cli"]).describe("agent loop・session・hook・transcript・認証を所有する実行基盤"),
662
675
  prompt: z.string().nullish().describe("起動時に渡す初手プロンプト(任意)。送信後は待たずに即返る"),
676
+ image: z.array(z.string()).optional().describe("初手プロンプトへ添付する画像ファイルの絶対パス(png/jpg/jpeg/gif/webp)"),
663
677
  throughline_source_session: z.string().min(1).optional().describe("同一端末のThroughline sessionから読み取り専用contextを初手へ注入する"),
664
678
  throughline_supplement_file: z.string().min(1).optional().describe("Throughline 0.10.8以降へそのまま渡すproject束縛済み長期記憶・知識の補足JSON path"),
665
679
  model: z.string().nullish().describe("harnessが選ぶモデル。provider名ではなくlive catalog上のmodel ID"),
package/docs/RELEASE.md CHANGED
@@ -1,53 +1,43 @@
1
1
  # Release
2
2
 
3
- Aitermのreleaseはこのrepositoryが所有する。`.github/workflows/product-full-ci.yml`が変更影響選択、3環境runnerと
3
+ Aitermのreleaseはこのrepositoryが所有する。`.github/workflows/product-full-ci.yml`が変更影響選択、runnerと
4
4
  製品gateの正本であり、dotagentsの工場CIは横断受入のconsumerであってreleaseを制御しない。
5
5
 
6
- ## Version同期
6
+ ## CIの範囲
7
7
 
8
- 新versionでは次を同じ値へ更新する。
8
+ - push/pull request: `linux-workstation` 1環境。変更した実装の依存graphから試験を選び、依存を確定できない変更は
9
+ Linuxの全テストへ広げる。Windows固有ファイル(`src/windows-powershell.ts`、`src/psmux-send-worker.ts`、
10
+ `test/windows-*.test.mjs`)を触った変更だけ`windows-native`を加える。
11
+ - 週1回の定期実行(月曜 03:00 JST)と手動実行だけが、`macos-native`、`linux-workstation`、`windows-native`の
12
+ 3環境で全テストを回す。
13
+ - tag push: 所有確認と、tagged commitが`origin/main`の祖先であることの確認だけを行い、npmへprovenance付きで
14
+ publishする。同じcommitのmain CIの結果は待たない。
9
15
 
10
- - `package.json`と`package-lock.json`
11
- - `server.json`
12
- - `mcpb/manifest.json`
13
- - `README.md`と`README.ja.md`の現行公開版
14
- - `CHANGELOG.md`の新version見出し、`Unreleased`比較先、新version比較link
16
+ 実測(2026-09-02): Linux 2分、macOS 2分、Windows 6分。全環境展開ではWindowsが常にcritical pathになる。
15
17
 
16
- 同じversionのtagやnpm packageを移動・上書きしない。失敗版はそのまま残し、修正版を次のversionで出す。
17
-
18
- ## Local gate
18
+ ## Release手順
19
19
 
20
- 変更に直結するfocused testを通した後、最終確認を一度だけ行う。
20
+ 1. 変更に直結するfocused testを手元で通す。full regressionは手元で回さず、CIに任せる。
21
+ 2. `CHANGELOG.md`の`## [Unreleased]`へ内容を書き、mainへcommitしてpushする。
22
+ 3. 一回で公開する。
21
23
 
22
- ```bash
23
- npm ci
24
- npm test
25
- npm pack --dry-run
26
- npm run mcpb:build
27
- ```
24
+ ```bash
25
+ npm run release -- <version>
26
+ ```
28
27
 
29
- MCPBのstaged serverでversion、16 tools、stderr 0、必要なruntime JavaScriptの同梱を確認する。
28
+ scriptはversion同期(`package.json`、`package-lock.json`、`server.json`、`mcpb/manifest.json`、`README.md`と`README.ja.md`の現行公開版)、
29
+ CHANGELOG見出しと比較link、metadata検査、release commit、main push、`v<version>` tag push、
30
+ MCPB build、GitHub Release作成までを行う。tag pushがnpm publish、Release作成がOfficial MCP Registry登録を起動する。
31
+ scriptはその完了を待たない。
30
32
 
31
- ## Mainと公開
33
+ 4. 後で確認する: `npm view aiterm-mcp@<version> version`、Official Registryの`io.github.kitepon/aiterm-mcp`。
32
34
 
33
- 1. release commitを`main`へpushする。version・manifest・workflowを含むrelease変更は未分類変更として`macos-native`、`linux-workstation`、`windows-native`の全テストへ広がり、3環境をgreenにする。
34
- 2. `npm run verify:release-commit`で対象commitが`origin/main`の祖先かつworktree cleanであることを確認する。
35
- 3. 同じcommitへ`v<version>` tagを付けてpushする。tag CIは同じcommitのmain CI成功を確認し、3環境試験を再実行せずnpmへprovenance付きでpublishする。
36
- 4. build済みMCPBを添付したGitHub Releaseを公開する。release eventがOfficial MCP Registry登録を起動する。
37
- 5. npm、GitHub Release、Official Registryが同じversionを返すまで確認する。
38
-
39
- CI callerは同じrepositoryの`./.github/workflows/product-full-ci.yml`だけを呼ぶ。製品側の変更影響選択、
40
- 全テストへの明示的な拡張、3環境runner、release gateを外部repositoryへ移さず、dotagentsの変更や停止からAitermの受入を独立させる。
35
+ 同じversionのtagやnpm packageを移動・上書きしない。失敗版はそのまま残し、修正版を次のversionで出す。
41
36
 
42
37
  ## 公開後smoke
43
38
 
44
- 公式npm packageを隔離またはglobal installし、次を確認する。
45
-
46
- - `aiterm-mcp`、`aiterm-wait`、`aiterm-runtime-errors`の3 bins。
47
- - MCP initializeのversion、16 tools、stderr 0。
48
- - POSIXはtmux、Windows nativeはpsmux 3.3.8以上とPowerShell 7。
49
- - 変更に触れたharnessの起動、non-blocking dispatch、wait outcome、transcript回収、`pty_close`後の残骸ゼロ。
50
- - Official Registryが`io.github.kitepon/aiterm-mcp`の同じversionをactive/latestとして返す。
39
+ 公式npm packageを隔離またはglobal installし、変更に触れたharnessの起動、non-blocking dispatch、wait outcome、
40
+ transcript回収、`pty_close`後の残骸ゼロを確認する。
51
41
 
52
42
  ## 利用者の更新と巻き戻し
53
43
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiterm-mcp",
3
- "version": "0.29.30",
3
+ "version": "0.30.0",
4
4
  "mcpName": "io.github.kitepon/aiterm-mcp",
5
5
  "description": "Persistent terminal MCP with one harness-based launcher for Claude Code, Codex CLI, Grok CLI, and Cursor Agent CLI, plus durable PTYs for SSH, containers, and REPLs.",
6
6
  "keywords": [
@@ -60,6 +60,7 @@
60
60
  "build": "node scripts/clean-build.mjs && tsc",
61
61
  "mcpb:build": "npm run build && node scripts/build-mcpb.mjs && npm ci --omit=dev --ignore-scripts --no-audit --no-fund --prefix dist/mcpb-stage/server && npx --yes @anthropic-ai/mcpb@2.1.2 validate dist/mcpb-stage/manifest.json && npx --yes @anthropic-ai/mcpb@2.1.2 pack dist/mcpb-stage dist/aiterm-mcp.mcpb",
62
62
  "verify:release-commit": "node scripts/verify-release-commit.mjs",
63
+ "release": "node scripts/release.mjs",
63
64
  "test:docs": "node --test test/repository-contract.test.mjs",
64
65
  "prepublishOnly": "npm run verify:release-commit && npm run build",
65
66
  "start": "node dist/index.js",