aiterm-mcp 0.38.2 → 0.39.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 +22 -1
- package/README.ja.md +33 -6
- package/README.md +33 -5
- package/dist/agent-resolver.js +2 -2
- package/dist/codex-desktop-binary.js +27 -0
- package/dist/codex-parent-hooks.js +2 -1
- package/dist/codex-parent-receiver.js +2 -1
- package/dist/core.js +156 -20
- package/dist/harnesses/claude.js +5 -4
- package/dist/harnesses/codex.js +29 -13
- package/dist/harnesses/cursor.js +53 -7
- package/dist/harnesses/grok.js +4 -0
- package/dist/index.js +161 -22
- package/dist/parent-delivery.js +35 -23
- package/dist/process-runtime.js +12 -1
- package/dist/remote.js +276 -0
- package/dist/setup-codex-hooks.js +2 -1
- package/dist/setup-codex-relay.js +17 -5
- package/dist/tmux-runtime.js +103 -1
- package/dist/update-cli.js +69 -0
- package/dist/update.js +220 -0
- package/docs/DESIGN.md +26 -1
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.39.0] - 2026-09-26
|
|
11
|
+
|
|
12
|
+
### 追加
|
|
13
|
+
|
|
14
|
+
- `agent_launch`と既存のPTY/agent操作toolに`remote`を追加する。SSHで入った別端末のAitermへ同じtoolを中継し、端末へ入る操作と現地のagent起動を1回で行う。接続先・鍵・パスフレーズは呼び出しごとに受け取り、保存・管理しない。Codex/Claude Code/Cursor親への回答自動配送は別端末の子にも働き、記録は旧版が読まない`remote-`付きの保存場所へ分ける。
|
|
15
|
+
- `pty_read(agent_transcript:true)`が`raw:true`を受け付け、削減前の回答本文を返す。
|
|
16
|
+
- `agent_steer`がClaude CodeとCursorの実行中turnにも差し込めるようにする。これまではCodex/Grok専用で、BellTeamのように処理中のBotへ待ち行列のメッセージを差し込む呼び出し側では、Claude Code/CursorのBot宛てがエラーになっていた。
|
|
17
|
+
- `aiterm-update`を追加する。この端末と`--host`で指定したSSH接続先のAitermを同じ版へ入れ替え、各端末で新しい版の`aiterm-setup --json`を実行し直す。版の解決は呼んだ端末で1回だけ行い、`aiterm-update`を持たない旧版の端末にはnpmで入れてから渡す。更新前から動いている`aiterm-mcp`の数を`running_servers`で返す。
|
|
18
|
+
|
|
19
|
+
### 修正
|
|
20
|
+
|
|
21
|
+
- Claude Code 2.1.282の番号付きログイン方式選択を、初回案内の未完了として見分ける。ログイン済みでも初回案内が済んでいないと起動のたびにこの画面で止まるため、ready timeoutを待たずに`vendor_onboarding_required`と直し方を返す。初回テーマ選択もtimeoutを待たずに確定する。
|
|
22
|
+
- Cursor Agentが利用上限に達した画面を見分ける。Cursorは上限時にtranscriptへ完了を書かず入力欄も戻さないため、Aitermは完了を待ち続けていた。現在の画面に上限の説明が出ていれば`rate_limited`(aiterm-waitはexit 6)として上限の説明を返し、`pty_observe`は`blocked`/`rate_limited`を返す。後ろに入力欄や実行中表示がある古い上限表示は数えない。
|
|
23
|
+
- Codex CLI 0.157の利用上限接近の画面(「Approaching rate limits」)を見分ける。0.157では切替先modelが`gpt-6-luna`になり、画面下の案内も`enter select · esc back`へ変わったため、Aitermはこの画面を見分けられず、次の送信が入力待ちにならないまま止まっていた。切替先modelを固定せず、新しい案内行もmodalのfooterとして扱い、従来どおり現在のmodelのまま続ける「2」だけを選ぶ。
|
|
24
|
+
- Claude Codeの起動時の確認画面(フォルダの信頼、Bypass Permissionsの確認)で、選択が「Yes」の行へ移ったのを画面で確かめてからEnterを送る。どちらも既定の選択が「No, exit」なので、起動直後のCLIが「↓」を取り落とすとEnterでCLIが終了していた(2.1.283で再現)。選択が動かなければ「↓」を一度だけ送り直し、それでも動かなければEnterを送らず`startup_dialog`で止める。
|
|
25
|
+
- `agent_steer`でGrokへ差し込んだ文が現在のturnに入らず、turnの完了後に別turnとして動いていた。Aitermは最初のturnの終わりを完了として届けるため、差し込んだ指示への回答は行き場を失っていた。Grokが待ち行列へ入れたのを確かめてから標準の「send now」で現在turnへ移し、この時に書かれる`turn_ended`(`cancelled`、`trigger=send_now`)は完了と数えない。Cursorも同じく「follow-ups」枠へ入った文を「enter steer」で現在turnへ移す。
|
|
26
|
+
- `agent_steer`の結果の型がCodex/Grokのvendorとharnessだけを許していたため、Claude Code/Cursorへの差し込みは本文が届いても呼び出し側にはエラーとして返っていた。
|
|
27
|
+
- WindowsのCursor Agentを、Git BashではなくPowerShell 7のpaneから起動する。Git Bash配下で起動したCursorはhookへ渡すJSONの先頭にUTF-8 BOMを付け、BOMを読めない利用者のhook(Throughline、caveat等)が送信を止めていた。この時Cursorは`prompt_history.json`だけを残して会話を作らず、画面にも理由を出さないため、Aitermは完了を待ち続けていた。
|
|
28
|
+
- Codex DesktopのSteerが、Desktopの更新後に動かなくなっていた。setup時に保存した同梱Codex CLIの場所(Windowsは版ごとのcache directory、macOSはbundle内)がDesktopの更新で無くなり、回答の配送も親hookも`CODEX_RECEIVER_TRANSPORT_FAILED`(ENOENT)で失敗していた。保存した場所が無ければ使う時点で公式Desktopから探し直して設定を更新し、探せなければ`CODEX_DESKTOP_BINARY_MOVED`と直し方を返す。macOSでは`ChatGPT.app/Contents/Resources/codex-cli/CodexCLI.app/Contents/MacOS/codex`の新しい同梱場所も見つける。
|
|
29
|
+
|
|
10
30
|
## [0.38.2] - 2026-09-25
|
|
11
31
|
|
|
12
32
|
### 修正
|
|
@@ -1751,7 +1771,8 @@ prototype (preserved under `prototype/python/` as the porting source and referen
|
|
|
1751
1771
|
`ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
|
|
1752
1772
|
provenance.
|
|
1753
1773
|
|
|
1754
|
-
[Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.
|
|
1774
|
+
[Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.39.0...HEAD
|
|
1775
|
+
[0.39.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.38.2...v0.39.0
|
|
1755
1776
|
[0.38.2]: https://github.com/kitepon/aiterm-mcp/compare/v0.38.1...v0.38.2
|
|
1756
1777
|
[0.38.1]: https://github.com/kitepon/aiterm-mcp/compare/v0.38.0...v0.38.1
|
|
1757
1778
|
[0.38.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.37.10...v0.38.0
|
package/README.ja.md
CHANGED
|
@@ -74,7 +74,7 @@ hookはAiterm自身の配送記録と本文が一致する回答だけを取り
|
|
|
74
74
|
|
|
75
75
|
解除・hook未対応の旧版への巻き戻し前は`aiterm-setup --codex-steer disable`を実行してCodexを再起動してください。
|
|
76
76
|
macOS・Windowsの公式Codex Desktopと、公式キュー・hookに対応する同梱CLIを対象にします。
|
|
77
|
-
|
|
77
|
+
Desktopの更新で同梱Codex CLIの場所が変わった時は、Aitermが使う時点で探し直して設定を更新します。探せない時は`CODEX_DESKTOP_BINARY_MOVED`を返すので、Desktopを起動してからsetupを再実行してください。LinuxのSteer付き導入は理由付き`unsupported`を返します。
|
|
78
78
|
Aiterm単品の公式キュー配送は従来どおり利用できます。
|
|
79
79
|
|
|
80
80
|
cloneもビルドも不要。どのクライアントでも公開パッケージを次のコマンドで起動する:
|
|
@@ -143,7 +143,7 @@ diagnostics、recovery、update、releaseを所有します。このREADMEと[
|
|
|
143
143
|
|
|
144
144
|
**言葉でなく実測で:** 記録済み203テストのベンチマークでは、`pty_read` はコンテキストに載るトークンを生ログの **約 7.1 分の 1** に減らす。しかも pass/fail の判定は畳んでも残る。→ [組み込みシェルツールとの使い分け](#組み込みシェルツールとの使い分け)
|
|
145
145
|
|
|
146
|
-
18ツール: 7つのPTYツール、正規のagent起動入口`agent_launch`、実行中のCodex/Grokを誘導する`agent_steer`、移行用の旧4alias、`agent_configure`、`agent_approval`、`claude_turn`、`claude_approval`、`diagnostics`。backendはPOSIXのtmux/Windows nativeのpsmuxなので、MCPサーバやAIクライアントが再起動してもsessionは生き残る。
|
|
146
|
+
18ツール: 7つのPTYツール、正規のagent起動入口`agent_launch`、実行中のClaude/Codex/Grok/Cursorを誘導する`agent_steer`、移行用の旧4alias、`agent_configure`、`agent_approval`、`claude_turn`、`claude_approval`、`diagnostics`。backendはPOSIXのtmux/Windows nativeのpsmuxなので、MCPサーバやAIクライアントが再起動してもsessionは生き残る。
|
|
147
147
|
|
|
148
148
|
**v0.28.0では実行基盤harnessとmodelを分離した。** harnessはagent loop・認証・hook・session・transcriptを所有し、modelはその上で選ぶ。Cursor Agent CLIでGPT/Claude/Grokを選んでも完了契約はCursor方式のまま。Composerは別harnessではなく、`harness:"grok-cli", model:"grok-composer-2.5-fast"`で表す。旧4起動ツールは同じ実装へ流れる互換alias。
|
|
149
149
|
|
|
@@ -202,12 +202,24 @@ runtime-error store は canonical dotagents config の `collection.enabled: true
|
|
|
202
202
|
場合だけ収集し、既定OFF、network送信は行いません。tag起点CIのnpm provenance(OIDC Trusted
|
|
203
203
|
Publishing)で公開し、GitHub Release が Official MCP Registry を再登録します。
|
|
204
204
|
|
|
205
|
-
**状態:** 開発継続中 · 現行公開版 **v0.
|
|
205
|
+
**状態:** 開発継続中 · 現行公開版 **v0.39.0** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
|
|
206
206
|
|
|
207
207
|
### 更新と巻き戻し
|
|
208
208
|
|
|
209
|
-
npm packageが単独配布の正本であり、dotagentsは介在しません。global install
|
|
210
|
-
|
|
209
|
+
npm packageが単独配布の正本であり、dotagentsは介在しません。global installは`aiterm-update`で更新します。
|
|
210
|
+
npmで同じ導入先を指定の版へ入れ替え、新しい版の`aiterm-setup --json`で登録と実動作を確かめ直します。
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
aiterm-update # この端末をlatestへ
|
|
214
|
+
aiterm-update --host rabbit --host win-test # この端末とSSH接続先を同じ版へ
|
|
215
|
+
aiterm-update --version 0.39.0 --check # 入れ替えずに現在の版と更新先を確かめる
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
`--host`は`~/.ssh/config`の接続名かホスト名で、接続先は保存しません。版の解決は呼んだ端末で1回だけ行い、
|
|
219
|
+
全端末を同じ版へ揃えます。更新機能より古い版の端末では、npmで入れてから新しい`aiterm-update`へ渡します。
|
|
220
|
+
導入先へ書けない時は`permission_required`と管理者権限での手順を返します。更新前から動いている`aiterm-mcp`は
|
|
221
|
+
MCP clientが起動し直すまで旧版のcodeで動き(`running_servers`)、tmux/psmuxのsessionは残ります。
|
|
222
|
+
`aiterm-update`を持たない版では`npm install -g aiterm-mcp@latest`の後に`aiterm-setup --json`を再実行します。巻き戻す時は
|
|
211
223
|
`npm install -g "aiterm-mcp@<known-good-version>"`のように既知の正常versionを明示します。setupを持つ版では同じ入口を再実行し、MCP clientを再起動します。
|
|
212
224
|
`npx`設定では`aiterm-mcp@latest`へ変えると更新でき、`aiterm-mcp@<version>`へ変えると固定・巻き戻し
|
|
213
225
|
できます。downgrade前に[変更履歴](CHANGELOG.md)でstate/schema互換を確認してください。maintainer向けの
|
|
@@ -508,7 +520,7 @@ Claudeの相関済み承認は既存の`claude_approval`を使う。
|
|
|
508
520
|
| `pty_observe` | pane/harnessの生存、native process identity、状態と活動 | `session_id`, `cursor?` |
|
|
509
521
|
| `agent_launch` | harnessとmodelを別軸で選ぶ正規agent起動入口 | `harness`, `prompt?`, `model?`, `reasoning_effort?`, `cwd?`, `write_scope?`, `trust_project?`, `env_vars?`, `throughline_source_session?`, `throughline_supplement_file?` |
|
|
510
522
|
| `agent_approval` | Codexの現在の承認を検査し、単発許可・拒否を送る | `action`, `session_id`, `approval_choice?`, `observed_prompt_digest?` |
|
|
511
|
-
| `agent_steer` | 実行中のCodex/Grok turn
|
|
523
|
+
| `agent_steer` | 実行中のClaude/Codex/Grok/Cursor turnへ、各harness標準の操作でtextを差し込む。差し込み後の作業の完了は1回だけ届く。idleなら送信せず`idle`を返す。Grokが待ち行列へ入れない時とCursorの入力欄に残った時は`steered`を返さず失敗する | `session_id`, `text` |
|
|
512
524
|
| `claude_agent` / `codex_agent` / `grok_agent` / `composer_agent` | deprecated互換alias | 旧launcher引数 |
|
|
513
525
|
| `agent_configure` | 起動中のClaude/Codex/Grok/Composer/Cursorを再起動せずmodel/effort変更 | `session_id`, `model?`, `reasoning_effort?` |
|
|
514
526
|
| `claude_turn` | 相関済みClaude operationをdispatch(issue)または回収(recover) | `action`, `session_id`, `operation_id`, `text?` |
|
|
@@ -571,6 +583,21 @@ Cursor親(`clientInfo.name`が`cursor-vscode`)も同じ完了観測と本文
|
|
|
571
583
|
Claudeをリンク経由の`cwd`から起動した場合も、実体パスに対応する会話記録を参照する。
|
|
572
584
|
|
|
573
585
|
|
|
586
|
+
### 別端末のagentを1回で起動する(`remote`)
|
|
587
|
+
|
|
588
|
+
`agent_launch`に`remote`を付けると、SSHで入った別端末のAitermで同じ起動を行う。端末へ入る操作と現地のagentを起動する操作が1回で済み、親から見た完了の届き方はこの端末の子と同じになる。Linux・macOS・Windowsの端末へ同じ依頼を並行して投げる用途を想定している。
|
|
589
|
+
|
|
590
|
+
```jsonc
|
|
591
|
+
agent_launch({ "harness": "codex-cli", "remote": { "host": "rabbit" }, "cwd": "/home/kite/project", "prompt": "..." })
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
- `remote`は`host`、`user`、`port`、`identity_file`、`passphrase`または`passphrase_env`、`ssh_options`(`Key=Value`)を受け取る。`host`だけなら`~/.ssh/config`の接続名として使う。Aitermは接続先を保存・管理しない。どこへどの鍵で入るかは呼び出し側が持つ。
|
|
595
|
+
- 平文の`passphrase`は呼び出したAIの会話記録に残る。ssh-agentか`passphrase_env`(環境変数名)を推奨する。受け取ったパスフレーズはMCP processのメモリにだけ置き、`SSH_ASKPASS`でsshへ渡す。状態ファイルやログには書かない。
|
|
596
|
+
- 現地には`aiterm-mcp`、tmux(Windowsはpsmux)、使うharnessのCLIが要る。接続先のshell(POSIX系、PowerShell、cmd)は最初の接続で見分ける。POSIX系ではログインshellからPATHだけを受け取るので、`~/.local/bin`、Homebrew、nvmなどに置いたCLIも使える。WindowsはユーザーのPATHのまま`aiterm-mcp`を起動する。
|
|
597
|
+
- 以後の`pty_send`、`pty_read`、`pty_close`、`pty_observe`、`agent_steer`などにも同じ`remote`を付ける。session名は現地のもので、この端末の同名sessionとは別に扱う。
|
|
598
|
+
- Codex/Claude Code/Cursor親には、この端末の子と同じく回答本文が自動で届く。完了は`ssh <host> aiterm-wait`で観測し、SSHが切れても同じcursorでつなぎ直す。それ以外の親には、sshを使う`wait_process`を返す。
|
|
599
|
+
- 同じ接続先への呼び出しはControlMasterで1本のSSHに相乗りする。`remote`付きの`image`添付と`claude_turn issue`は未対応。
|
|
600
|
+
|
|
574
601
|
### トークン削減
|
|
575
602
|
|
|
576
603
|
- `pty_read` は既定で制御文字除去・連続重複圧縮・head+tail 折りたたみ(+復元ヒント・メタ併記)をかける。
|
package/README.md
CHANGED
|
@@ -74,7 +74,7 @@ hookはAiterm自身の配送記録と本文が一致する回答だけを取り
|
|
|
74
74
|
|
|
75
75
|
解除・hook未対応の旧版への巻き戻し前は`aiterm-setup --codex-steer disable`を実行してCodexを再起動してください。
|
|
76
76
|
macOS・Windowsの公式Codex Desktopと、公式キュー・hookに対応する同梱CLIを対象にします。
|
|
77
|
-
|
|
77
|
+
When a Desktop update moves the bundled Codex CLI, Aiterm finds it again at use time and updates its configuration. If it cannot, it returns `CODEX_DESKTOP_BINARY_MOVED`; start Desktop and rerun setup.LinuxのSteer付き導入は理由付き`unsupported`を返します。
|
|
78
78
|
Aiterm単品の公式キュー配送は従来どおり利用できます。
|
|
79
79
|
|
|
80
80
|
No clone or build is required. Each client launches the published package with:
|
|
@@ -145,7 +145,7 @@ Aiterm and is not a runtime dependency.
|
|
|
145
145
|
|
|
146
146
|
**Measured, not claimed:** in the recorded 203-test benchmark, a `pty_read` puts **~7.1× fewer tokens** in your context than the raw log — and the pass/fail verdict survives the fold. → [When to reach for it vs. the built-in shell](#when-to-reach-for-it-vs-the-built-in-shell)
|
|
147
147
|
|
|
148
|
-
Eighteen tools: seven **PTY tools** — `pty_open` / `pty_send` / `pty_read` / `pty_key` / `pty_close` / `pty_list` / `pty_observe` — to open, drive, read, and observe one persistent terminal; one canonical **agent launcher**, `agent_launch`, which selects `claude-code`, `codex-cli`, `grok-cli`, or `cursor-cli` as the execution harness; `agent_steer` for an active Codex or
|
|
148
|
+
Eighteen tools: seven **PTY tools** — `pty_open` / `pty_send` / `pty_read` / `pty_key` / `pty_close` / `pty_list` / `pty_observe` — to open, drive, read, and observe one persistent terminal; one canonical **agent launcher**, `agent_launch`, which selects `claude-code`, `codex-cli`, `grok-cli`, or `cursor-cli` as the execution harness; `agent_steer` for an active Claude, Codex, Grok, or Cursor turn; four deprecated launcher aliases kept for migration; `agent_configure`; `agent_approval`; `claude_turn`; `claude_approval`; and `diagnostics`. The backend is **tmux on POSIX and psmux on native Windows**, so sessions survive even if the MCP server or the AI client restarts.
|
|
149
149
|
|
|
150
150
|
**v0.28.0 separates the execution harness from the model.** The harness owns the agent loop, authentication, hooks, session, and transcript; `model` is what that harness runs. Cursor Agent CLI can therefore select GPT, Claude, or Grok without changing the completion contract from Cursor hooks to another harness's. Grok Composer is a Grok CLI model preset, not another harness: use `harness: "grok-cli", model: "grok-composer-2.5-fast"`. The old four launcher tools are thin compatibility aliases over the same implementation.
|
|
151
151
|
|
|
@@ -216,12 +216,25 @@ collection is off by default and performs no network I/O. It ships via
|
|
|
216
216
|
tag-triggered CI with npm provenance (OIDC Trusted Publishing); the GitHub
|
|
217
217
|
Release re-registers the Official MCP Registry entry.
|
|
218
218
|
|
|
219
|
-
**Status:** actively maintained · current public release **v0.
|
|
219
|
+
**Status:** actively maintained · current public release **v0.39.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).
|
|
220
220
|
|
|
221
221
|
### Update and rollback
|
|
222
222
|
|
|
223
223
|
The npm package is the standalone distribution; dotagents is not involved. For a global install,
|
|
224
|
-
update with `
|
|
224
|
+
update with `aiterm-update`. It reinstalls the requested version into the same npm prefix, then reruns the new version's
|
|
225
|
+
`aiterm-setup --json` to re-register and re-verify.
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
aiterm-update # this machine to latest
|
|
229
|
+
aiterm-update --host rabbit --host win-test # this machine and SSH hosts to the same version
|
|
230
|
+
aiterm-update --version 0.39.0 --check # report current and target versions without changing anything
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
`--host` takes an `~/.ssh/config` alias or host name; hosts are not stored. The version is resolved once on the calling
|
|
234
|
+
machine so every host lands on the same version. Hosts older than `aiterm-update` get it through npm first. When the
|
|
235
|
+
npm prefix is not writable, the result is `permission_required` with the command to run as an administrator. `aiterm-mcp`
|
|
236
|
+
servers that were already running keep the old code until their MCP client restarts (`running_servers`); tmux/psmux
|
|
237
|
+
sessions survive. Versions without `aiterm-update` use `npm install -g aiterm-mcp@latest` and `aiterm-setup --json`. To roll back, install a known-good immutable version,
|
|
225
238
|
for example `npm install -g "aiterm-mcp@<known-good-version>"`, then restart the MCP client. setupを持つ版では再起動前に`aiterm-setup --json`を再実行する。For an `npx` configuration,
|
|
226
239
|
use `aiterm-mcp@latest` to update or replace it with `aiterm-mcp@<version>` to pin or roll back.
|
|
227
240
|
Check the [CHANGELOG](CHANGELOG.md) for state/schema compatibility before downgrading. Maintainer
|
|
@@ -540,7 +553,7 @@ continue to use `claude_approval`.
|
|
|
540
553
|
| `pty_observe` | Pane/harness liveness, native process identity, state, and activity | `session_id`, `cursor?` |
|
|
541
554
|
| `agent_launch` | Canonical agent launch; harness and model are independent | `harness`, `prompt?`, `model?`, `reasoning_effort?`, `cwd?`, `write_scope?`, `trust_project?`, `env_vars?`, `throughline_source_session?`, `throughline_supplement_file?` |
|
|
542
555
|
| `agent_approval` | Inspect a Codex approval and submit a one-time approval or denial | `action`, `session_id`, `approval_choice?`, `observed_prompt_digest?` |
|
|
543
|
-
| `agent_steer` | Inject text into the active Codex or
|
|
556
|
+
| `agent_steer` | Inject text into the active Claude, Codex, Grok, or Cursor turn with each harness's own steering control, so the steered work still ends in one completion; return `idle` without sending when no turn is active, and fail instead of returning `steered` when Grok does not queue the text or Cursor leaves it in the composer | `session_id`, `text` |
|
|
544
557
|
| `claude_agent` / `codex_agent` / `grok_agent` / `composer_agent` | Deprecated compatibility aliases | legacy launcher arguments |
|
|
545
558
|
| `agent_configure` | Change model/effort in a running Claude, Codex, Grok, Composer, or Cursor session without restarting it | `session_id`, `model?`, `reasoning_effort?` |
|
|
546
559
|
| `claude_turn` | Issue (dispatch-only) or recover one correlated Claude operation | `action`, `session_id`, `operation_id`, `text?` |
|
|
@@ -616,6 +629,21 @@ Claudeをリンク経由の`cwd`から起動した場合も、実体パスに対
|
|
|
616
629
|
|
|
617
630
|
`aiterm-wait` takes no locks, never writes session state, and never dispatches — any number can run beside the MCP server and each other, and `pty_close`/concurrent sends are unaffected.
|
|
618
631
|
|
|
632
|
+
### Launch an agent on another machine in one call (`remote`)
|
|
633
|
+
|
|
634
|
+
Add `remote` to `agent_launch` and the same launch runs in the Aiterm of another machine reached over SSH. Entering the machine and starting its agent become one call, and completion reaches the parent exactly as it does for a local child. The intended use is sending the same task to Linux, macOS, and Windows machines in parallel.
|
|
635
|
+
|
|
636
|
+
```jsonc
|
|
637
|
+
agent_launch({ "harness": "codex-cli", "remote": { "host": "rabbit" }, "cwd": "/home/kite/project", "prompt": "..." })
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
- `remote` takes `host`, `user`, `port`, `identity_file`, `passphrase` or `passphrase_env`, and `ssh_options` (`Key=Value`). A bare `host` is used as an `~/.ssh/config` alias. Aiterm does not store or manage destinations; the caller owns where to connect and with which key.
|
|
641
|
+
- A plain `passphrase` stays in the calling AI's conversation log. Prefer ssh-agent or `passphrase_env` (an environment variable name). A received passphrase lives only in the MCP process memory and reaches ssh through `SSH_ASKPASS`; it is never written to state files or logs.
|
|
642
|
+
- The remote machine needs `aiterm-mcp`, tmux (psmux on Windows), and the harness CLI. The remote shell family (POSIX, PowerShell, or cmd) is detected on first contact. On POSIX machines Aiterm takes only PATH from the login shell, so CLIs under `~/.local/bin`, Homebrew, or nvm are found; on Windows it starts `aiterm-mcp` with the user's PATH as is.
|
|
643
|
+
- Pass the same `remote` to later `pty_send`, `pty_read`, `pty_close`, `pty_observe`, `agent_steer`, and so on. Session names belong to the remote machine and never collide with local sessions of the same name.
|
|
644
|
+
- Codex, Claude Code, and Cursor parents receive the answer automatically, as with a local child. Completion is observed with `ssh <host> aiterm-wait`, reconnecting at the same cursor if SSH drops. Other parents get an ssh-based `wait_process`.
|
|
645
|
+
- Calls to the same destination share one SSH connection through ControlMaster. `image` attachments and `claude_turn issue` are not yet supported with `remote`.
|
|
646
|
+
|
|
619
647
|
### Token reduction
|
|
620
648
|
|
|
621
649
|
- `pty_read` by default strips control characters, collapses repeated lines, and folds long output into head+tail (with a restore hint and a meta line).
|
package/dist/agent-resolver.js
CHANGED
|
@@ -6,7 +6,7 @@ import * as path from "node:path";
|
|
|
6
6
|
import * as os from "node:os";
|
|
7
7
|
import { AitermError } from "./errors.js";
|
|
8
8
|
import { resolveWindowsPowerShell7 } from "./windows-powershell.js";
|
|
9
|
-
import { isWin } from "./tmux-runtime.js";
|
|
9
|
+
import { isWin, spawnInMacGuiWhenOutsideAqua } from "./tmux-runtime.js";
|
|
10
10
|
export function isUsableExecutableFile(candidate) {
|
|
11
11
|
try {
|
|
12
12
|
if (!fs.statSync(candidate).isFile())
|
|
@@ -82,7 +82,7 @@ export function spawnAgentControlCommand(bin, args, _cwd, options) {
|
|
|
82
82
|
// これを直接 spawn すると常に失敗し、「受入が通した bin で起動が必ず失敗する」矛盾になる。
|
|
83
83
|
return spawnSync(resolveWinPaneShell("bash"), [bin, ...args], options);
|
|
84
84
|
}
|
|
85
|
-
return spawnSync(bin, args, options);
|
|
85
|
+
return spawnInMacGuiWhenOutsideAqua(bin, args, options) ?? spawnSync(bin, args, options);
|
|
86
86
|
}
|
|
87
87
|
export function resolveWinPaneShell(shell) {
|
|
88
88
|
if (!isWin)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Codex DesktopのSteerで使う同梱Codex CLIの場所。Desktopは更新のたびに同梱物の場所を変えることがある
|
|
2
|
+
// (Windowsは版ごとのcache directory、macOSはbundle内の配置)。setup時に保存した場所が消えていたら、
|
|
3
|
+
// 使う時点で公式Desktopから探し直して設定を更新する。探せなければ理由付きで失敗し、別のCodexへは切り替えない。
|
|
4
|
+
import * as fs from "node:fs";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { CodexDeliveryError } from "./codex-delivery-error.js";
|
|
7
|
+
import { codexHookDirectory, writeHookJson } from "./codex-hook-state.js";
|
|
8
|
+
async function platformFinder() {
|
|
9
|
+
if (process.platform === "win32")
|
|
10
|
+
return (await import("./windows-codex-setup.js")).findWindowsCodexBinary;
|
|
11
|
+
return (await import("./setup-codex-relay.js")).findDesktopBinary;
|
|
12
|
+
}
|
|
13
|
+
export async function currentCodexDesktopBinary(config, options = {}) {
|
|
14
|
+
const exists = options.exists ?? fs.existsSync;
|
|
15
|
+
if (exists(config.binary))
|
|
16
|
+
return config.binary;
|
|
17
|
+
let binary;
|
|
18
|
+
try {
|
|
19
|
+
binary = (options.find ?? await platformFinder())();
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
throw new CodexDeliveryError("CODEX_DESKTOP_BINARY_MOVED", `Codex Desktopの更新で ${config.binary} が無くなり、新しい場所も特定できません(${error instanceof Error ? error.message : String(error)})。` +
|
|
23
|
+
"Codex Desktopを起動してから aiterm-setup --codex-steer enable を実行してください");
|
|
24
|
+
}
|
|
25
|
+
writeHookJson(path.join(options.directory ?? codexHookDirectory(), "config.json"), { ...config, binary });
|
|
26
|
+
return binary;
|
|
27
|
+
}
|
|
@@ -6,6 +6,7 @@ import { realCodexHome } from "./harnesses/codex.js";
|
|
|
6
6
|
import { withCodexReceiver } from "./codex-parent-receiver.js";
|
|
7
7
|
import { CodexDeliveryError } from "./codex-delivery-error.js";
|
|
8
8
|
import { readRuntimeProcesses } from "./process-runtime.js";
|
|
9
|
+
import { currentCodexDesktopBinary } from "./codex-desktop-binary.js";
|
|
9
10
|
import { answerDigest, codexHookDirectory, codexInputDirectory, hookInputSchema, readCodexHookConfig, writeHookJson } from "./codex-hook-state.js";
|
|
10
11
|
export async function runCodexResultHook(input, emit, options = {}) {
|
|
11
12
|
const event = z.object({ session_id: z.uuid(), turn_id: z.string().min(1),
|
|
@@ -96,7 +97,7 @@ export async function runCodexResultHook(input, emit, options = {}) {
|
|
|
96
97
|
taken.pop();
|
|
97
98
|
}
|
|
98
99
|
}
|
|
99
|
-
}, options.runtime ?? { executable: config
|
|
100
|
+
}, options.runtime ?? { executable: await currentCodexDesktopBinary(config), timeout_ms: 5_000 });
|
|
100
101
|
const text = taken.map(item => item.text).join("\n\n");
|
|
101
102
|
await emit(!taken.length ? {} : event.hook_event_name === "Stop"
|
|
102
103
|
? { decision: "block", reason: text }
|
|
@@ -7,6 +7,7 @@ import { realCodexHome } from "./harnesses/codex.js";
|
|
|
7
7
|
import { CodexDeliveryError } from "./codex-delivery-error.js";
|
|
8
8
|
import { readRelayConfig, parentRelaySocket } from "./codex-relay-config.js";
|
|
9
9
|
import { withCodexRelay, verifyLoadedParent } from "./codex-relay-client.js";
|
|
10
|
+
import { currentCodexDesktopBinary } from "./codex-desktop-binary.js";
|
|
10
11
|
import { finishCodexHookSubmission, assertCodexHookParentCurrent, assertCodexHooksReady, codexHookDirectory, readCodexHookConfig, registerCodexHookInput } from "./codex-hook-state.js";
|
|
11
12
|
export { CodexDeliveryError } from "./codex-delivery-error.js";
|
|
12
13
|
/** modelの引数ではなく、CodexがMCP要求へ付けるmetadataだけを宛先にする。 */
|
|
@@ -29,7 +30,7 @@ function relaySocket(runtime) {
|
|
|
29
30
|
}
|
|
30
31
|
export async function withCodexReceiver(parent, action, runtime = {}) {
|
|
31
32
|
const config = runtime.executable ? null : readCodexHookConfig();
|
|
32
|
-
const executable = runtime.executable ?? (config?.enabled ? config
|
|
33
|
+
const executable = runtime.executable ?? (config?.enabled ? await currentCodexDesktopBinary(config) : null) ?? resolveAgentBin("codex");
|
|
33
34
|
if (!executable)
|
|
34
35
|
throw new CodexDeliveryError("CODEX_RECEIVER_UNAVAILABLE", "Codexの実行ファイルを確認できません");
|
|
35
36
|
const child = spawn(executable, runtime.args ?? ["app-server", "--listen", "stdio://"], {
|
package/dist/core.js
CHANGED
|
@@ -14,14 +14,14 @@ import { createHash, randomBytes } from "node:crypto";
|
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
15
|
import * as rtk from "./rtk.js";
|
|
16
16
|
import { paneTokenHint } from "./harnesses/pane-tokens.js";
|
|
17
|
-
import { readRuntimeProcesses, processSubtree, processIdentity, backgroundProcesses } from "./process-runtime.js";
|
|
17
|
+
import { readRuntimeProcesses, processSubtree, parentProcess, processIdentity, backgroundProcesses } from "./process-runtime.js";
|
|
18
18
|
import { AitermError, telemetryOwnedFailure, ownTelemetryFailure } from "./errors.js";
|
|
19
19
|
import { isWin, SOCKDIR, tmuxCommand, sendPsmuxPayload, loadPtyBufferChunk, pasteBufferBaseArgs, TMUX_EMPTY_CONFIG, attachCommand, normalizePaneCommand, atomicShellMultiline, appendMarkSentinel, markShellCommand, settlePaneLog, paneCwdArgument, sessionEnvironmentLaunch, } from "./tmux-runtime.js";
|
|
20
20
|
import { sleep, currentUid, runtimeStateBase, safeStatSize, readFileRange, writeJson0600, createEmpty0600, shq, LAUNCH_ID_RE, AGENT_DONE_POLL_MS, AGENT_EVENT_MAX_BYTES, assertSessionName, agentsDir, agentEventPath, agentMetadataPath, writeAgentMetadata, AGENT_EVENT_TAIL_BYTES, agentLabel, agentHarness, subagentInstruction, agentLineageFields, } from "./agent-shared.js";
|
|
21
21
|
import { GROK_MODEL_DEFAULTS, realGrokHome, resolveAndValidateGrokAuth, assertGrokModelAvailable, grokEventsTranscript, latestGrokCompletion, observeGrokDone, buildGrokAgentCmd, grokLaunchNote, grokEnvTokens, grokTuiReady, grokTuiBusy, grokPaneObservation, grokRateLimitDialog, grokStartupAction, grokLaunchBlockingDialog, assertGrokSandboxNotRejected, GROK_COMPOSER_MARKER_RE, grokFooterHasConfiguration, grokTranscriptText, createGrokAgentMetadata, } from "./harnesses/grok.js";
|
|
22
22
|
import { bindCodexTranscriptSession, latestCodexCompletion, observeCodexDone, buildCodexAgentCmd, codexLaunchNote, codexTuiReady, codexPaneObservation, codexRateLimitModelSwitchDialog, codexApprovalDialog, codexStartupAction, CODEX_COMPOSER_MARKER_RE, codexModelChoice, codexEffortChoice, codexMoreReasoningChoice, codexTranscriptText, createCodexAgentMetadata, } from "./harnesses/codex.js";
|
|
23
|
-
import { OPERATION_ID_RE, CLAUDE_RESULT_MAX_BYTES, CLAUDE_EFFORTS, agentManagedClaudeSettingsPath, agentClaudeResultPath, agentClaudeOperationPath, agentClaudeApprovalReceiptPath, agentClaudeDispatchReceiptPath, validateOperationId, readClaudeResultText, assertClaudeAuthenticationReady, buildClaudeAgentCmd, claudeLaunchNote, claudeTuiReady, claudePaneObservation, claudeStartupAction, CLAUDE_COMPOSER_MARKER_RE, createClaudeAgentMetadata, claudeSessionTranscriptPath, claudeApiErrorFromLine, } from "./harnesses/claude.js";
|
|
24
|
-
import { bindCursorTranscriptSession, cursorTurnBoundary, latestCursorCompletion, observeCursorDone, cursorTranscriptText, assertCursorAuthenticationReady, assertCursorModelAvailable, buildCursorAgentCmd, cursorPromptWithLineage, createCursorAgentMetadata, cursorLaunchNote, cursorEffortNavigation, cursorTuiReady, cursorPaneObservation, CURSOR_SUBMIT_SEQUENCE, CURSOR_COMPOSER_CONTENT_MARKER_RE, validateCursorModelEffort, } from "./harnesses/cursor.js";
|
|
23
|
+
import { OPERATION_ID_RE, CLAUDE_RESULT_MAX_BYTES, CLAUDE_EFFORTS, agentManagedClaudeSettingsPath, agentClaudeResultPath, agentClaudeOperationPath, agentClaudeApprovalReceiptPath, agentClaudeDispatchReceiptPath, validateOperationId, readClaudeResultText, assertClaudeAuthenticationReady, buildClaudeAgentCmd, claudeLaunchNote, claudeTuiReady, claudePaneObservation, claudeStartupAction, claudeLoginMethodMenu, CLAUDE_COMPOSER_MARKER_RE, createClaudeAgentMetadata, claudeSessionTranscriptPath, claudeApiErrorFromLine, } from "./harnesses/claude.js";
|
|
24
|
+
import { bindCursorTranscriptSession, cursorTurnBoundary, latestCursorCompletion, observeCursorDone, cursorTranscriptText, assertCursorAuthenticationReady, assertCursorModelAvailable, buildCursorAgentCmd, cursorAgentArgv, cursorPwshLaunchLine, cursorPromptWithLineage, createCursorAgentMetadata, cursorLaunchNote, cursorEffortNavigation, cursorTuiReady, cursorPaneObservation, cursorUsageLimit, CURSOR_SUBMIT_SEQUENCE, CURSOR_COMPOSER_CONTENT_MARKER_RE, validateCursorModelEffort, } from "./harnesses/cursor.js";
|
|
25
25
|
import { resolveAgentBin, resolveThroughlineBin, runThroughlineHandoffContext, isWindowsNativeExecutable, agentBinForPaneShell, resolveWinPaneShell } from "./agent-resolver.js";
|
|
26
26
|
export { AitermError } from "./errors.js";
|
|
27
27
|
export { tmuxSpawnEnv } from "./tmux-runtime.js";
|
|
@@ -1040,8 +1040,12 @@ export function selectHarnessProcesses(meta, rows, subtree) {
|
|
|
1040
1040
|
.replace(/\.exe$/i, "").replace(/^-/, "").toLowerCase();
|
|
1041
1041
|
const command = row.command.replace(/\\/g, "/").replace(/\.exe(?=["\s]|$)/gi, "").replace(/"/g, "");
|
|
1042
1042
|
const tokens = row.command.match(/"[^"]*"|'[^']*'|\S+/g) ?? [];
|
|
1043
|
-
|
|
1044
|
-
|
|
1043
|
+
// `sh <script>` だけでなく、Windowsの `cmd.exe /c <launcher>.cmd` のように起動した実行ファイルが
|
|
1044
|
+
// 2番目以降に来る形もある(Cursor公式installerは .cmd → .ps1 → node と中継する)。
|
|
1045
|
+
const launched = meta.agent_executable?.replace(/\\/g, "/");
|
|
1046
|
+
const sameFile = (value) => /^[A-Za-z]:\//.test(launched) ? value.toLowerCase() === launched.toLowerCase() : value === launched;
|
|
1047
|
+
const launchedScript = launched !== undefined
|
|
1048
|
+
&& tokens.slice(1).some(token => sameFile(token.replace(/^["']|["']$/g, "").replace(/\\/g, "/")));
|
|
1045
1049
|
return launchedScript || (!SHELLS.has(executable) && executable !== "pwsh" && AGENT_COMMAND_PATTERNS[meta.kind].test(command));
|
|
1046
1050
|
};
|
|
1047
1051
|
// WindowsのMSYS execでnative親子関係が切れる場合も、launch固有引数で相関する。
|
|
@@ -1052,12 +1056,12 @@ export function selectHarnessProcesses(meta, rows, subtree) {
|
|
|
1052
1056
|
// npm shimとnative本体の間にNodeなどの非候補processがいても、同じ起動の祖先を辿る。
|
|
1053
1057
|
const roots = candidates.filter(row => {
|
|
1054
1058
|
const seen = new Set();
|
|
1055
|
-
let parent =
|
|
1059
|
+
let parent = parentProcess(row, byPid);
|
|
1056
1060
|
while (parent && !seen.has(parent.pid)) {
|
|
1057
1061
|
if (candidatePids.has(parent.pid))
|
|
1058
1062
|
return false;
|
|
1059
1063
|
seen.add(parent.pid);
|
|
1060
|
-
parent =
|
|
1064
|
+
parent = parentProcess(parent, byPid);
|
|
1061
1065
|
}
|
|
1062
1066
|
return true;
|
|
1063
1067
|
});
|
|
@@ -2603,7 +2607,7 @@ export function agentWaitGuide(session) {
|
|
|
2603
2607
|
const cmd = `aiterm-wait --session ${session ?? "<session_id>"} --cursor 0`;
|
|
2604
2608
|
return `完了通知は ${agentWaitLaunchForm(cmd)} で受ける(親はここで待たない・polling 不要)。receipt の outcome=done を確認してから再取得する。`;
|
|
2605
2609
|
}
|
|
2606
|
-
// harness別の利用上限観測。Grok/Composer
|
|
2610
|
+
// harness別の利用上限観測。Grok/Composerは現在の質問カード、Cursorは現在の画面、他harnessは既存logを使う。
|
|
2607
2611
|
// 出典(2026-08-22): grok は live 実バナーで検証、codex/claude はインストール済み実バイナリの
|
|
2608
2612
|
// 埋込文字列から抽出(codex: "You've hit your usage limit for" / claude: "Usage limit reached ·
|
|
2609
2613
|
// continuing automatically when it resets"。Claude Code はリセット時に自動継続する設計なので、
|
|
@@ -2618,6 +2622,8 @@ export function detectAgentRateLimit(kind, aitermSession) {
|
|
|
2618
2622
|
if (kind === "grok" || kind === "composer") {
|
|
2619
2623
|
return grokRateLimitDialog(captureScreen(aitermSession, 0))?.message ?? null;
|
|
2620
2624
|
}
|
|
2625
|
+
if (kind === "cursor")
|
|
2626
|
+
return cursorUsageLimit(captureScreen(aitermSession, 0))?.message ?? null;
|
|
2621
2627
|
const patterns = AGENT_RATE_LIMIT_PATTERNS[kind];
|
|
2622
2628
|
if (!patterns)
|
|
2623
2629
|
return null;
|
|
@@ -2792,6 +2798,8 @@ function isAgentTuiActionRequired(kind, screen) {
|
|
|
2792
2798
|
}
|
|
2793
2799
|
if (kind === "claude") {
|
|
2794
2800
|
return /new MCP servers? found in this project/iu.test(screen)
|
|
2801
|
+
|| claudeLoginMethodMenu(screen)
|
|
2802
|
+
|| claudeStartupAction(screen, false)?.kind === "initial_theme_selected"
|
|
2795
2803
|
|| screen.includes("Is this a project you created or one you trust")
|
|
2796
2804
|
|| screen.includes("trust this folder")
|
|
2797
2805
|
|| (screen.includes("Claude Code running in Bypass Permissions mode") && screen.includes("Yes, I accept"));
|
|
@@ -3077,11 +3085,32 @@ async function sendAgentPromptText(name, text, kind) {
|
|
|
3077
3085
|
}
|
|
3078
3086
|
sendKey(name, "Enter", { preserveAgentOperation: true });
|
|
3079
3087
|
}
|
|
3088
|
+
// 利用者が端末で済ませるしかない起動前の状態。reasonだけでは直し方が分からないので案内を添える。
|
|
3089
|
+
function startupBlockedHint(reason) {
|
|
3090
|
+
if (reason === "vendor_onboarding_required") {
|
|
3091
|
+
return "\nClaude Codeの初回案内(テーマ・ログイン方法の選択)が済んでいません。`claude auth status` がログイン済みでも、" +
|
|
3092
|
+
"初回案内が未完了なら起動のたびにこの画面で止まります。対象端末で一度 `claude` を起動し、案内を最後まで進めてください。";
|
|
3093
|
+
}
|
|
3094
|
+
return "";
|
|
3095
|
+
}
|
|
3080
3096
|
function setInitialDelivery(meta, value, cursor) {
|
|
3081
3097
|
meta.initial_prompt_delivery = value;
|
|
3082
3098
|
meta.initial_prompt_cursor = cursor;
|
|
3083
3099
|
writeAgentMetadata(meta);
|
|
3084
3100
|
}
|
|
3101
|
+
const STARTUP_SELECTION_POLL_MS = 100;
|
|
3102
|
+
const STARTUP_SELECTION_MAX_SAMPLES = 30;
|
|
3103
|
+
/** 起動時の確認画面で、選択中(❯)の行が目的の選択肢へ移ったことを確かめる。 */
|
|
3104
|
+
async function waitStartupSelection(name, selected) {
|
|
3105
|
+
for (let i = 0; i < STARTUP_SELECTION_MAX_SAMPLES; i++) {
|
|
3106
|
+
const marker = captureScreen(name, AGENT_TUI_READY_LINES).split("\n").slice(-32)
|
|
3107
|
+
.filter(line => /^\s*❯/u.test(line)).at(-1)?.trim() ?? "";
|
|
3108
|
+
if (selected.test(marker))
|
|
3109
|
+
return true;
|
|
3110
|
+
await sleep(STARTUP_SELECTION_POLL_MS);
|
|
3111
|
+
}
|
|
3112
|
+
return false;
|
|
3113
|
+
}
|
|
3085
3114
|
async function prepareAgentInput(name, meta, options) {
|
|
3086
3115
|
await ensureAgentOwnsPaneInput(name, meta.kind);
|
|
3087
3116
|
let ready = await waitAgentTuiReady(name, meta, options.ready_timeout ?? AGENT_TUI_READY_TIMEOUT_MS);
|
|
@@ -3093,10 +3122,26 @@ async function prepareAgentInput(name, meta, options) {
|
|
|
3093
3122
|
if (!action || handled.has(action.kind))
|
|
3094
3123
|
break;
|
|
3095
3124
|
handled.add(action.kind);
|
|
3096
|
-
|
|
3125
|
+
const keys = action.selected ? action.keys.slice(0, -1) : action.keys;
|
|
3126
|
+
for (const key of keys) {
|
|
3097
3127
|
sendKey(name, key, { preserveAgentOperation: true });
|
|
3098
3128
|
await sleep(AGENT_SUBMIT_DELAY_MS);
|
|
3099
3129
|
}
|
|
3130
|
+
if (action.selected) {
|
|
3131
|
+
// 起動直後のTUIは最初のキーを取り落とすことがある。選択が動いていなければ移動キーを一度だけ送り直す。
|
|
3132
|
+
let moved = await waitStartupSelection(name, action.selected);
|
|
3133
|
+
if (!moved && keys.length > 0) {
|
|
3134
|
+
for (const key of keys) {
|
|
3135
|
+
sendKey(name, key, { preserveAgentOperation: true });
|
|
3136
|
+
await sleep(AGENT_SUBMIT_DELAY_MS);
|
|
3137
|
+
}
|
|
3138
|
+
moved = await waitStartupSelection(name, action.selected);
|
|
3139
|
+
}
|
|
3140
|
+
if (!moved)
|
|
3141
|
+
break;
|
|
3142
|
+
sendKey(name, action.keys.at(-1), { preserveAgentOperation: true });
|
|
3143
|
+
await sleep(AGENT_SUBMIT_DELAY_MS);
|
|
3144
|
+
}
|
|
3100
3145
|
ready = await waitAgentTuiReady(name, meta, options.ready_timeout ?? AGENT_TUI_READY_TIMEOUT_MS);
|
|
3101
3146
|
}
|
|
3102
3147
|
if (!ready.ready) {
|
|
@@ -3125,7 +3170,8 @@ export async function sendInitialAgentPrompt(name, text, o = {}) {
|
|
|
3125
3170
|
if (startup.status !== "ready") {
|
|
3126
3171
|
setInitialDelivery(meta, { status: "not_sent", reason: startup.reason, turn_started: false }, null);
|
|
3127
3172
|
throw new AitermError(`initial_prompt=not_sent vendor=${meta.kind} ready=false harness=${agentHarness(meta.kind)}\n` +
|
|
3128
|
-
`agent session '${name}' の起動準備が完了していないため、promptは送信していません。reason=${startup.reason}
|
|
3173
|
+
`agent session '${name}' の起動準備が完了していないため、promptは送信していません。reason=${startup.reason}` +
|
|
3174
|
+
startupBlockedHint(startup.reason), 2);
|
|
3129
3175
|
}
|
|
3130
3176
|
const promptText = meta.kind === "cursor" ? cursorPromptWithLineage(meta, text) : text;
|
|
3131
3177
|
const startOffset = agentCompletionCursor(meta);
|
|
@@ -3574,12 +3620,37 @@ export async function dispatchAgentTurn(name, text, o = {}) {
|
|
|
3574
3620
|
pane_input_recovery: paneInputRecovery,
|
|
3575
3621
|
};
|
|
3576
3622
|
}
|
|
3623
|
+
// Grok CLIは実行中の送信を次turn用の待ち行列へ入れ、「Enter to send now」で現在turnへ差し込む。
|
|
3624
|
+
const GROK_QUEUED_SEND_NOW_RE = /Queued · Enter to send now|Enter:send now/;
|
|
3625
|
+
// 待ち行列の表示は入力欄の直上と操作案内に出る。scrollbackへ流れた古い表示を数えないよう、画面末尾だけを見る。
|
|
3626
|
+
const grokSteerQueued = (screen) => GROK_QUEUED_SEND_NOW_RE.test(screen.split("\n").filter(line => line.trim()).slice(-10).join("\n"));
|
|
3627
|
+
// Cursor Agentは実行中の送信を「follow-ups」枠へ入れ、「enter steer」で現在turnへ差し込む。
|
|
3628
|
+
const CURSOR_FOLLOW_UP_STEER_RE = /enter steer · /;
|
|
3629
|
+
const cursorSteerQueued = (screen) => CURSOR_FOLLOW_UP_STEER_RE.test(screen.split("\n").filter(line => line.trim()).slice(-30).join("\n"));
|
|
3630
|
+
const STEER_SCREEN_POLL_MS = 100;
|
|
3631
|
+
const STEER_SCREEN_MAX_SAMPLES = 50;
|
|
3632
|
+
async function waitSteerScreen(name, predicate) {
|
|
3633
|
+
for (let i = 0; i < STEER_SCREEN_MAX_SAMPLES; i++) {
|
|
3634
|
+
if (predicate(captureScreen(name, AGENT_TUI_READY_LINES)))
|
|
3635
|
+
return true;
|
|
3636
|
+
await sleep(STEER_SCREEN_POLL_MS);
|
|
3637
|
+
}
|
|
3638
|
+
return false;
|
|
3639
|
+
}
|
|
3640
|
+
export function __testSteerQueued(kind, screen) {
|
|
3641
|
+
return kind === "cursor" ? cursorSteerQueued(screen) : grokSteerQueued(screen);
|
|
3642
|
+
}
|
|
3643
|
+
/**
|
|
3644
|
+
* 実行中turnへ追加の指示を渡す。各harnessの標準操作だけを使い、完了境界は1つに保つ。
|
|
3645
|
+
* - Codex: 次のtool呼出し後に同じturnへ入る(rolloutのturn_idが同じ)。
|
|
3646
|
+
* - Claude Code: 次のtool境界で同じturnへ入り、Stopは1回。
|
|
3647
|
+
* - Cursor: 「follow-ups」枠へ入れた後に「enter steer」で現在turnへ移す。turn_endedは最後に1回。
|
|
3648
|
+
* - Grok: 待ち行列へ入れた後に「send now」を押す。旧turnは`cancelled`(trigger=send_now)で閉じ、
|
|
3649
|
+
* 新turnが作業を継ぐ。完了判定はこの継ぎ目を完了と数えない(grokCompletionEvent)。
|
|
3650
|
+
*/
|
|
3577
3651
|
export async function steerAgentTurn(name, text) {
|
|
3578
3652
|
assertSessionName(name);
|
|
3579
3653
|
const meta = loadAgentMetadata(name);
|
|
3580
|
-
if (!["codex", "grok", "composer"].includes(meta.kind)) {
|
|
3581
|
-
throw new AitermError("agent_steer はCodex/Grok agent sessionだけで使用できます", 2);
|
|
3582
|
-
}
|
|
3583
3654
|
const receipt = {
|
|
3584
3655
|
schema: "aiterm.agent-steer.v1",
|
|
3585
3656
|
session_id: meta.aiterm_session,
|
|
@@ -3592,16 +3663,54 @@ export async function steerAgentTurn(name, text) {
|
|
|
3592
3663
|
if (!isAgentTuiBusy(meta.kind, captureScreen(name, AGENT_TUI_READY_LINES))) {
|
|
3593
3664
|
return { ...receipt, delivery: "idle", pane_input_recovery: paneInputRecovery };
|
|
3594
3665
|
}
|
|
3666
|
+
prepareSendText(text, { raw: false });
|
|
3667
|
+
// Claudeのoperation相関は変えない。差し込みは実行中turnの一部であり、新しいturnを予約しない。
|
|
3668
|
+
const preserveAgentOperation = meta.kind === "claude";
|
|
3595
3669
|
send(name, text, {
|
|
3596
3670
|
enter: false,
|
|
3597
3671
|
force: true,
|
|
3598
3672
|
raw: false,
|
|
3599
3673
|
mark: false,
|
|
3600
3674
|
rtk: false,
|
|
3675
|
+
preserveAgentOperation,
|
|
3601
3676
|
bracketedPaste: true,
|
|
3602
3677
|
});
|
|
3603
|
-
|
|
3604
|
-
|
|
3678
|
+
if (meta.kind === "cursor") {
|
|
3679
|
+
const visible = await waitCursorPromptVisible(name, text);
|
|
3680
|
+
if (!visible.visible) {
|
|
3681
|
+
throw new AitermError(`vendor=cursor session=${name}\n差し込む文がCursorの入力欄へ反映されたことを確認できないため、Enterは送信していません。`, 2);
|
|
3682
|
+
}
|
|
3683
|
+
}
|
|
3684
|
+
else {
|
|
3685
|
+
await sleep(AGENT_SUBMIT_DELAY_MS);
|
|
3686
|
+
}
|
|
3687
|
+
sendKey(name, "Enter", { preserveAgentOperation });
|
|
3688
|
+
// GrokとCursorは実行中の送信を待ち行列へ入れる。そのままだと現在turnの完了後に別turnとして動き、
|
|
3689
|
+
// 完了通知が差し込み前の回答で届いてしまう。待ち行列へ入ったことを確かめ、標準の「今すぐ送る」で現在turnへ移す。
|
|
3690
|
+
const queued = meta.kind === "grok" || meta.kind === "composer" ? grokSteerQueued
|
|
3691
|
+
: meta.kind === "cursor" ? cursorSteerQueued : null;
|
|
3692
|
+
if (queued) {
|
|
3693
|
+
const label = meta.kind === "cursor" ? "Cursor" : "Grok";
|
|
3694
|
+
if (!await waitSteerScreen(name, queued)) {
|
|
3695
|
+
throw new AitermError(`STEER_NOT_QUEUED vendor=${meta.kind} session=${name}\n` +
|
|
3696
|
+
`差し込む文が${label}の待ち行列へ入ったことを確認できません。pty_read(screen:true)で入力欄を確かめてください。`, 2);
|
|
3697
|
+
}
|
|
3698
|
+
sendKey(name, "Enter");
|
|
3699
|
+
if (!await waitSteerScreen(name, screen => !queued(screen))) {
|
|
3700
|
+
throw new AitermError(`STEER_STILL_QUEUED vendor=${meta.kind} session=${name}\n` +
|
|
3701
|
+
`差し込む文が${label}の待ち行列に残っています。現在のturnが終わると別turnとして実行されます。`, 2);
|
|
3702
|
+
}
|
|
3703
|
+
}
|
|
3704
|
+
// Cursorだけは実行中でも入力欄の残留を確実に見分けられる(dispatchと同じ扱い)。Codex/Claudeは
|
|
3705
|
+
// 待ち行列に入った文が入力欄の近くへ表示されるため、残留判定で失敗にしない。
|
|
3706
|
+
if (meta.kind === "cursor") {
|
|
3707
|
+
let residue = await detectAgentSubmitResidue(name, meta.kind, text);
|
|
3708
|
+
residue = await retryCursorSubmitIfResidue(name, meta.kind, text, residue);
|
|
3709
|
+
if (residue.residue === true) {
|
|
3710
|
+
throw new AitermError(`submit_residue=true vendor=${meta.kind} session=${name}\n` +
|
|
3711
|
+
"差し込む文がCursorの入力欄に残っており、実行中のturnへ渡せませんでした。", 2);
|
|
3712
|
+
}
|
|
3713
|
+
}
|
|
3605
3714
|
return { ...receipt, delivery: "steered", pane_input_recovery: paneInputRecovery };
|
|
3606
3715
|
}
|
|
3607
3716
|
export async function runClaudeOperation({ session_id: name, action, operation_id: operationIdInput, text, before_send, }) {
|
|
@@ -3716,6 +3825,25 @@ function buildAgentCmd(kind, bin, model, effort, prompt, meta = null) {
|
|
|
3716
3825
|
return buildCursorAgentCmd(bin, model, effort, prompt, meta);
|
|
3717
3826
|
return buildGrokAgentCmd(kind, bin, model, effort, prompt, meta);
|
|
3718
3827
|
}
|
|
3828
|
+
/** Cursor用のagent環境変数(名前と値)。Windowsでは引用規則の違うPowerShell paneへ渡す。 */
|
|
3829
|
+
function cursorAgentEnvEntries(meta, sid, envVars = []) {
|
|
3830
|
+
const inherited = envVars.flatMap((name) => {
|
|
3831
|
+
const value = process.env[name];
|
|
3832
|
+
return value === undefined ? [] : [[name, value]];
|
|
3833
|
+
});
|
|
3834
|
+
return [
|
|
3835
|
+
...inherited,
|
|
3836
|
+
["AITERM_AGENT_KIND", meta.kind],
|
|
3837
|
+
["AITERM_SESSION_ID", sid],
|
|
3838
|
+
["AITERM_AGENT_SESSION_ID", sid],
|
|
3839
|
+
["AITERM_AGENT_LAUNCH_ID", meta.launch_id],
|
|
3840
|
+
["AITERM_AGENT_ROLE", meta.agent_role ?? "subagent"],
|
|
3841
|
+
["AITERM_AGENT_PARENT_SESSION_ID", meta.parent_session_id ?? "host-root"],
|
|
3842
|
+
["AITERM_AGENT_DEPTH", String(meta.delegation_depth ?? 1)],
|
|
3843
|
+
["AITERM_AGENT_LINEAGE", meta.lineage ?? `host-root>${meta.kind}:${sid}`],
|
|
3844
|
+
["AITERM_AGENT_DELEGATION_ALLOWED", meta.delegation_allowed === true ? "true" : "false"],
|
|
3845
|
+
];
|
|
3846
|
+
}
|
|
3719
3847
|
function agentEnvPrefix(meta, sid, envVars = []) {
|
|
3720
3848
|
const inherited = envVars.flatMap((name) => {
|
|
3721
3849
|
const value = process.env[name];
|
|
@@ -3906,8 +4034,10 @@ export function openAgent(kind, opts = {}) {
|
|
|
3906
4034
|
}
|
|
3907
4035
|
let sid;
|
|
3908
4036
|
let hint;
|
|
4037
|
+
// WindowsのCursorだけはPowerShell 7 paneから起動する(理由はcursorPwshLaunchLine)。
|
|
4038
|
+
const paneShell = isWin && kind === "cursor" && agentDone ? "pwsh" : "bash";
|
|
3909
4039
|
try {
|
|
3910
|
-
[sid, hint] = openSession(opts.session_name ?? null,
|
|
4040
|
+
[sid, hint] = openSession(opts.session_name ?? null, paneShell, envVars);
|
|
3911
4041
|
}
|
|
3912
4042
|
catch (error) {
|
|
3913
4043
|
if (launchOperationId !== null && sessionExists(opts.session_name)) {
|
|
@@ -3934,9 +4064,15 @@ export function openAgent(kind, opts = {}) {
|
|
|
3934
4064
|
agentMetadataNegativeCache.delete(sid);
|
|
3935
4065
|
}
|
|
3936
4066
|
launchNote = buildAgentLaunchNote(kind, model, effort, meta);
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
4067
|
+
let full;
|
|
4068
|
+
if (paneShell === "pwsh" && meta) {
|
|
4069
|
+
full = cursorPwshLaunchLine(cwd, cursorAgentEnvEntries(meta, sid, envVars), cursorAgentArgv(bin, model, effort, opts.prompt ?? null, meta));
|
|
4070
|
+
}
|
|
4071
|
+
else {
|
|
4072
|
+
const cmd = buildAgentCmd(kind, binForCmd, model, effort, opts.prompt ?? null, meta);
|
|
4073
|
+
const envPrefix = agentEnvPrefix(meta, sid, envVars);
|
|
4074
|
+
full = cwdForCmd ? `cd ${shq(cwdForCmd)} && ${envPrefix}${cmd}` : `${envPrefix}${cmd}`;
|
|
4075
|
+
}
|
|
3940
4076
|
// force:true はagent sessionへの手動介入を表す。起動コマンド自体はAitermが組み立てて素送信する。
|
|
3941
4077
|
send(sid, full, {
|
|
3942
4078
|
enter: true,
|
|
@@ -4039,7 +4175,7 @@ export async function openAgentWithInitialPrompt(kind, opts = {}) {
|
|
|
4039
4175
|
throw new AgentLaunchPromptError(`session_id: ${sid}\n起動準備中に失敗しました。${error instanceof Error ? error.message : String(error)}`, error instanceof AitermError ? error.code : 1, sid, delivery, null, { status: "blocked", reason: "startup_failed" });
|
|
4040
4176
|
}
|
|
4041
4177
|
if (startup.status !== "ready")
|
|
4042
|
-
throw new AgentLaunchPromptError(`session_id: ${sid}\n起動準備を完了できませんでした。reason=${startup.reason}`, 2, sid, delivery, null, startup);
|
|
4178
|
+
throw new AgentLaunchPromptError(`session_id: ${sid}\n起動準備を完了できませんでした。reason=${startup.reason}${startupBlockedHint(startup.reason)}`, 2, sid, delivery, null, startup);
|
|
4043
4179
|
}
|
|
4044
4180
|
return [sid, hint, null, null, delivery, startup];
|
|
4045
4181
|
}
|