aiterm-mcp 0.8.0 → 0.9.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 +13 -12
- package/README.md +13 -12
- package/dist/codex-stop-hook.js +124 -0
- package/dist/core.js +927 -4
- package/dist/grok-stop-hook.js +120 -0
- package/dist/index.js +28 -3
- package/package.json +1 -1
package/README.ja.md
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
|
|
23
23
|
9 ツール: 6 つの **PTY ツール**(`pty_open` / `pty_send` / `pty_read` / `pty_key` / `pty_close` / `pty_list`)で 1 本の永続端末を開き・操作し・読む。加えて 3 つの **エージェント起動ツール**(`codex_agent` / `grok_agent` / `composer_agent`)が、別のコーディングエージェントの TUI を新しい端末の中に起動する。バックエンドは **tmux** なので、MCP サーバや AI クライアントが再起動してもセッションは生き残る。
|
|
24
24
|
|
|
25
|
-
**状態:** 開発継続中 · この分野では新参で、別の形に賭けている([既存手段との比較](#既存手段との比較)参照)· 動作対象は Linux · WSL2 · macOS · Windows
|
|
25
|
+
**状態:** 開発継続中 · この分野では新参で、別の形に賭けている([既存手段との比較](#既存手段との比較)参照)· 動作対象は Linux · WSL2 · macOS · Windows ネイティブ(core PTY ツール。`agent_done` は現時点では POSIX/WSL/macOS のみ)· MIT · [変更履歴](CHANGELOG.md)。
|
|
26
26
|
|
|
27
27
|
## なぜ今
|
|
28
28
|
|
|
@@ -45,25 +45,26 @@ pty_read(id, { wait: true }) → 削減済みの出力を読む(完了
|
|
|
45
45
|
|
|
46
46
|
### 2. その端末の中に他のコーディングエージェントを起動する — オーケストレーションの旗艦
|
|
47
47
|
|
|
48
|
-
同じ primitive が別エージェントの TUI を宿す。3 つの起動ツールが、各ベンダーの対話型コーディングエージェント TUI を新しい永続端末の中に起動し、`session_id` を返す。以後はどのシェルとも同じ `pty_read` / `pty_send` で操作する——出力をトークン削減して読み、次の一手を送る。(TUI は全画面アプリなので `pty_read({ screen: true })`
|
|
48
|
+
同じ primitive が別エージェントの TUI を宿す。3 つの起動ツールが、各ベンダーの対話型コーディングエージェント TUI を新しい永続端末の中に起動し、`session_id` を返す。以後はどのシェルとも同じ `pty_read` / `pty_send` で操作する——出力をトークン削減して読み、次の一手を送る。(TUI は全画面アプリなので `pty_read({ screen: true })` で描画済みの画面が得られる。)`agent_done: true` を付けると hook ベースのターン完了待ちが有効になり、`pty_send({ wait: "agent_done" })` が agent のターン終了後に返る。Codex/Grok/Composer は、各ベンダー CLI が認証済みなら実 smoke 済み。これはベンダー自身の CLI が導入・認証済みであることが必要——[要件](#要件)参照。
|
|
49
49
|
|
|
50
50
|
```text
|
|
51
|
-
codex_agent({ session_name: "codex1", cwd: "/repo",
|
|
51
|
+
codex_agent({ session_name: "codex1", cwd: "/repo", agent_done: true,
|
|
52
52
|
prompt: "port test/legacy.py to vitest" })
|
|
53
53
|
→ { session_id: "codex1", … } # Codex が永続端末で稼働開始
|
|
54
54
|
pty_read("codex1", { screen: true }) → 何をしているか読む(トークン削減)
|
|
55
|
-
pty_send("codex1", "also fix the imports it broke"
|
|
55
|
+
pty_send("codex1", "also fix the imports it broke", { wait: "agent_done" })
|
|
56
|
+
→ 操舵し、Codex の次の入力境界で返る
|
|
56
57
|
```
|
|
57
58
|
|
|
58
59
|
モデルごとに 1 ツール=ツール名を見ればどのモデルか分かる:
|
|
59
60
|
|
|
60
61
|
| ツール | 起動するもの | 主な引数 |
|
|
61
62
|
| --- | --- | --- |
|
|
62
|
-
| `codex_agent` | Codex CLI(OpenAI・モデルは CLI の既定) | `prompt?`, `reasoning_effort?`, `cwd?`, `session_name?` |
|
|
63
|
-
| `grok_agent` | Grok Build の `grok-build` モデル(xAI) | `prompt?`, `reasoning_effort?`(`low`/`medium`/`high`/`xhigh`/`max`), `cwd?`, `session_name?` |
|
|
63
|
+
| `codex_agent` | Codex CLI(OpenAI・モデルは CLI の既定) | `prompt?`, `reasoning_effort?`, `cwd?`, `session_name?`, `agent_done?` |
|
|
64
|
+
| `grok_agent` | Grok Build の `grok-build` モデル(xAI) | `prompt?`, `reasoning_effort?`(`low`/`medium`/`high`/`xhigh`/`max`), `cwd?`, `session_name?`, `agent_done?` |
|
|
64
65
|
| `composer_agent` | Grok Build の `grok-composer-2.5-fast` モデル(xAI) | `grok_agent` と同じ |
|
|
65
66
|
|
|
66
|
-
各ベンダーの CLI が導入・認証済みであること(`codex_agent` は `codex`、Grok 系 2 つは `grok`)。バイナリは `CODEX_BIN` / `GROK_BIN` → `~/.local/bin/codex` / `~/.grok/bin/grok` → `PATH` の順で解決する。前提は **session を作る前に**検証する——`grok_agent` / `composer_agent` は範囲外の `reasoning_effort` を先に弾き(値集合は固定=`low`/`medium`/`high`/`xhigh`/`max`)、CLI バイナリ不在・実在しない `cwd` は 3 つとも失敗する。前提(`reasoning_effort`・バイナリ・`cwd`)で弾かれた起動は **session の残骸をゼロ**にして返し、起動キーストロークを送れなければ session を畳む。(その先でベンダー CLI が実際に立ち上がったか・認証できたかは aiterm は検証しない——`openAgent` は起動コマンドを送った時点で返る。起動できたかは session を読んで確認する。)(`codex_agent` は Codex の受理値が版で変わるため、`reasoning_effort` を検証せず config override〔`-c model_reasoning_effort=…`〕として Codex CLI へ渡す。)`cwd` は絶対パスで渡す——`~`
|
|
67
|
+
各ベンダーの CLI が導入・認証済みであること(`codex_agent` は `codex`、Grok 系 2 つは `grok`)。バイナリは `CODEX_BIN` / `GROK_BIN` → `~/.local/bin/codex` / `~/.grok/bin/grok` → `PATH` の順で解決する。前提は **session を作る前に**検証する——`grok_agent` / `composer_agent` は範囲外の `reasoning_effort` を先に弾き(値集合は固定=`low`/`medium`/`high`/`xhigh`/`max`)、CLI バイナリ不在・実在しない `cwd` は 3 つとも失敗する。前提(`reasoning_effort`・バイナリ・`cwd`)で弾かれた起動は **session の残骸をゼロ**にして返し、起動キーストロークを送れなければ session を畳む。(その先でベンダー CLI が実際に立ち上がったか・認証できたかは aiterm は検証しない——`openAgent` は起動コマンドを送った時点で返る。起動できたかは session を読んで確認する。)(`codex_agent` は Codex の受理値が版で変わるため、`reasoning_effort` を検証せず config override〔`-c model_reasoning_effort=…`〕として Codex CLI へ渡す。)`cwd` は絶対パスで渡す——`~` は展開されない。`agent_done` は launch ごとの managed vendor home を使い、通常の hook file は書き換えない。Grok/Composer はさらに `GROK_HOME` と `HOME` を隔離し、compat hook/plugin 混入を抑える。一方で OAuth は通常 Grok home の `auth.json` と `auth.json.lock` をセットで共有し、複数セッション間で refresh lock が分裂しないようにする。初回の未 bind `pty_send({ wait: "agent_done" })` は、vendor TUI の入力欄が出るまで待ち、入力受付状態でなければ文字列を送らず送信前エラーにする。`agent_done` は `getuid`・安全な一時ファイル・hard link 検査など POSIX ファイルシステム前提を使うため、対応は Linux / WSL2 / macOS。Windows ネイティブでは core PTY と agent launcher は使えるが、`agent_done` はまだ未対応。
|
|
67
68
|
|
|
68
69
|
**Claude launcher はあえて無い**し、エージェント間のプロトコルも無い: aiterm の役目は、あなたの Claude(や任意の MCP クライアント)が*他の*エージェントに手を伸ばして端末を操作できるようにすること。起動したエージェントは単なるもう 1 本の永続セッションなので、この README の他のすべて(トークン削減読取・完了検出・人の `attach` での監視/介入)がそのまま効く。
|
|
69
70
|
|
|
@@ -206,7 +207,7 @@ aiterm は同じ核心の洞察——端末を出会いの場にする——を
|
|
|
206
207
|
| ツール | 役割 | 主な引数 |
|
|
207
208
|
| --- | --- | --- |
|
|
208
209
|
| `pty_open` | 端末を 1 個握り `session_id` を返す | `name?`, `shell="bash"` |
|
|
209
|
-
| `pty_send` | テキスト(コマンド)を送る | `session_id`, `text`, `enter=true`, `mark`, `force`, `rtk`, `raw` |
|
|
210
|
+
| `pty_send` | テキスト(コマンド)を送る | `session_id`, `text`, `enter=true`, `wait`, `timeout`, `screen`, `lines`, `mark`, `force`, `rtk`, `raw` |
|
|
210
211
|
| `pty_read` | 出力を削減して読む(既定は増分) | `session_id`, `wait`, `until`, `until_regex`, `timeout`, `screen`, `full`, `lines`, `line_range`, `raw`, `rtk` |
|
|
211
212
|
| `pty_key` | 制御キーを送る | `session_id`, `key`(`C-c`/`Enter`/`Up`…) |
|
|
212
213
|
| `pty_close` | セッションを閉じる | `session_id` |
|
|
@@ -218,15 +219,15 @@ aiterm は同じ核心の洞察——端末を出会いの場にする——を
|
|
|
218
219
|
|
|
219
220
|
| ツール | 起動するもの | 主な引数 |
|
|
220
221
|
| --- | --- | --- |
|
|
221
|
-
| `codex_agent` | Codex CLI(OpenAI・モデルは CLI の既定) | `prompt?`, `reasoning_effort?`, `cwd?`, `session_name?` |
|
|
222
|
-
| `grok_agent` | Grok Build の `grok-build` モデル(xAI) | `prompt?`, `reasoning_effort?`(`low`/`medium`/`high`/`xhigh`/`max`), `cwd?`, `session_name?` |
|
|
222
|
+
| `codex_agent` | Codex CLI(OpenAI・モデルは CLI の既定) | `prompt?`, `reasoning_effort?`, `cwd?`, `session_name?`, `agent_done?` |
|
|
223
|
+
| `grok_agent` | Grok Build の `grok-build` モデル(xAI) | `prompt?`, `reasoning_effort?`(`low`/`medium`/`high`/`xhigh`/`max`), `cwd?`, `session_name?`, `agent_done?` |
|
|
223
224
|
| `composer_agent` | Grok Build の `grok-composer-2.5-fast` モデル(xAI) | `grok_agent` と同じ |
|
|
224
225
|
|
|
225
|
-
各ベンダーの CLI が導入・認証済みであること(`codex_agent` は `codex`、Grok 系 2 つは `grok`)。バイナリは `CODEX_BIN` / `GROK_BIN` → `~/.local/bin/codex` / `~/.grok/bin/grok` → `PATH` の順で解決する。前提はセッション作成前に検証し(grok/composer は範囲外の `reasoning_effort` を弾く。CLI 不在・実在しない `cwd` は 3 つとも失敗)、起動に失敗してもセッションの残骸は残さない。詳細は [その端末の中に他のコーディングエージェントを起動する](#2-その端末の中に他のコーディングエージェントを起動する--オーケストレーションの旗艦)。`cwd` は絶対パスで渡す——`~`
|
|
226
|
+
各ベンダーの CLI が導入・認証済みであること(`codex_agent` は `codex`、Grok 系 2 つは `grok`)。バイナリは `CODEX_BIN` / `GROK_BIN` → `~/.local/bin/codex` / `~/.grok/bin/grok` → `PATH` の順で解決する。前提はセッション作成前に検証し(grok/composer は範囲外の `reasoning_effort` を弾く。CLI 不在・実在しない `cwd` は 3 つとも失敗)、起動に失敗してもセッションの残骸は残さない。詳細は [その端末の中に他のコーディングエージェントを起動する](#2-その端末の中に他のコーディングエージェントを起動する--オーケストレーションの旗艦)。`cwd` は絶対パスで渡す——`~` は展開されない。`agent_done` は hook-backed で、Codex/Grok/Composer は Linux/WSL2/macOS で実 smoke 済み。Windows ネイティブでは agent launcher は使えるが `agent_done` はまだ未対応。初回の未 bind agent send では `pty_send({ wait:"agent_done" })` が vendor TUI の入力欄を待ち、未 ready なら送信前エラーにする。Grok OAuth mode では hook/config 隔離は launch ごとに維持しつつ、通常 Grok home の `auth.json` と `auth.json.lock` をセットで共有する。OAuth auth 不在ならセッション残骸なしで失敗する。
|
|
226
227
|
|
|
227
228
|
### 完了検出(5 層)
|
|
228
229
|
|
|
229
|
-
`pty_read({ wait: true })` は、プロセス終了 / `mark:true` sentinel の自動検出(後述)/ `until` 一致(**既定はリテラル部分一致**、`until_regex: true` で正規表現)/ 出力静止 ∧ シェル復帰(quiescence)/ timeout の 5 層で「コマンドが終わったか」を判定する。ネスト中(SSH・コンテナ・REPL・起動したエージェントの TUI の中)はシェル復帰判定が効かないので、`until` で内側プロンプトを指定するか、`mark: true` で送れば `pty_read({ wait: true })` が sentinel を自動検出する(until 不要・ネストでも効く)——全画面のエージェント TUI なら、出力が落ち着いた時点で `{ screen: true }`
|
|
230
|
+
`pty_read({ wait: true })` は、プロセス終了 / `mark:true` sentinel の自動検出(後述)/ `until` 一致(**既定はリテラル部分一致**、`until_regex: true` で正規表現)/ 出力静止 ∧ シェル復帰(quiescence)/ timeout の 5 層で「コマンドが終わったか」を判定する。ネスト中(SSH・コンテナ・REPL・起動したエージェントの TUI の中)はシェル復帰判定が効かないので、`until` で内側プロンプトを指定するか、`mark: true` で送れば `pty_read({ wait: true })` が sentinel を自動検出する(until 不要・ネストでも効く)——全画面のエージェント TUI なら、出力が落ち着いた時点で `{ screen: true }` を読む。`agent_done:true` で起動したセッションは代わりに `pty_send({ wait:"agent_done" })` を使える。必要な場合はまず agent TUI の入力欄を待ち、その後 vendor Stop hook を待ってターン境界後の画面を返す。送信前 ready 失敗は MCP エラー、送信後 timeout は `is_complete=False via agent_timeout` で返り、成功扱いしない。完結した hook JSONL 行が壊れていた場合は、切り分け用に timeout suffix へ `malformed_events=N` が付く。ターンは完了したが端末 screen/log が flush 窓内で安定しなかった場合は、`agent_done_but_screen_unstable` が付く。
|
|
230
231
|
|
|
231
232
|
### トークン削減
|
|
232
233
|
|
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
|
|
23
23
|
Nine tools: six **PTY tools** — `pty_open` / `pty_send` / `pty_read` / `pty_key` / `pty_close` / `pty_list` — to open, drive, and read one persistent terminal, plus three **agent launchers** — `codex_agent` / `grok_agent` / `composer_agent` — that each start another coding agent's TUI inside a fresh one. The backend is **tmux**, so sessions survive even if the MCP server or the AI client restarts.
|
|
24
24
|
|
|
25
|
-
**Status:** actively maintained · the newcomer here, betting on a different shape (see [vs. the alternatives](#vs-the-alternatives)) · runs on Linux · WSL2 · macOS · native Windows · MIT · see the [CHANGELOG](CHANGELOG.md).
|
|
25
|
+
**Status:** actively maintained · the newcomer here, betting on a different shape (see [vs. the alternatives](#vs-the-alternatives)) · runs on Linux · WSL2 · macOS · native Windows for the core PTY tools (`agent_done` is POSIX/WSL/macOS only for now) · MIT · see the [CHANGELOG](CHANGELOG.md).
|
|
26
26
|
|
|
27
27
|
## Why now
|
|
28
28
|
|
|
@@ -45,25 +45,26 @@ pty_read(id, { wait: true }) → read the token-reduced output, completion
|
|
|
45
45
|
|
|
46
46
|
### 2. Launch other coding agents into that terminal — the orchestration flagship
|
|
47
47
|
|
|
48
|
-
The same primitive hosts another agent's TUI. Three launchers each start one vendor's interactive coding-agent TUI inside a fresh persistent terminal and return a `session_id`. From there you drive it with the same `pty_read` / `pty_send` you'd use on any shell: read its output token-reduced, send it the next step. (The TUIs are full-screen apps, so `pty_read({ screen: true })` gives you the rendered view.) This
|
|
48
|
+
The same primitive hosts another agent's TUI. Three launchers each start one vendor's interactive coding-agent TUI inside a fresh persistent terminal and return a `session_id`. From there you drive it with the same `pty_read` / `pty_send` you'd use on any shell: read its output token-reduced, send it the next step. (The TUIs are full-screen apps, so `pty_read({ screen: true })` gives you the rendered view.) Agent launchers can also opt into hook-backed turn completion with `agent_done: true`, letting `pty_send({ wait: "agent_done" })` return after the agent turn ends. Codex/Grok/Composer all have passing live smokes when their vendor CLI is authenticated. This needs the vendor's own CLI installed and authenticated — see [Requirements](#requirements).
|
|
49
49
|
|
|
50
50
|
```text
|
|
51
|
-
codex_agent({ session_name: "codex1", cwd: "/repo",
|
|
51
|
+
codex_agent({ session_name: "codex1", cwd: "/repo", agent_done: true,
|
|
52
52
|
prompt: "port test/legacy.py to vitest" })
|
|
53
53
|
→ { session_id: "codex1", … } # Codex now live in a persistent terminal
|
|
54
54
|
pty_read("codex1", { screen: true }) → read what it's doing (token-reduced)
|
|
55
|
-
pty_send("codex1", "also fix the imports it broke"
|
|
55
|
+
pty_send("codex1", "also fix the imports it broke", { wait: "agent_done" })
|
|
56
|
+
→ steer it, then return once Codex reaches its next turn boundary
|
|
56
57
|
```
|
|
57
58
|
|
|
58
59
|
One call per model, so the tool name itself tells you which model you get:
|
|
59
60
|
|
|
60
61
|
| Tool | Launches | Key args |
|
|
61
62
|
| --- | --- | --- |
|
|
62
|
-
| `codex_agent` | Codex CLI (OpenAI; the CLI's default model) | `prompt?`, `reasoning_effort?`, `cwd?`, `session_name?` |
|
|
63
|
-
| `grok_agent` | Grok Build, model `grok-build` (xAI) | `prompt?`, `reasoning_effort?` (`low`/`medium`/`high`/`xhigh`/`max`), `cwd?`, `session_name?` |
|
|
63
|
+
| `codex_agent` | Codex CLI (OpenAI; the CLI's default model) | `prompt?`, `reasoning_effort?`, `cwd?`, `session_name?`, `agent_done?` |
|
|
64
|
+
| `grok_agent` | Grok Build, model `grok-build` (xAI) | `prompt?`, `reasoning_effort?` (`low`/`medium`/`high`/`xhigh`/`max`), `cwd?`, `session_name?`, `agent_done?` |
|
|
64
65
|
| `composer_agent` | Grok Build, model `grok-composer-2.5-fast` (xAI) | same as `grok_agent` |
|
|
65
66
|
|
|
66
|
-
The vendor CLI must be installed and authenticated (`codex` for `codex_agent`; `grok` for both Grok tools). aiterm resolves the binary via `CODEX_BIN` / `GROK_BIN`, then `~/.local/bin/codex` / `~/.grok/bin/grok`, then `PATH`. Prerequisites are checked **before** a session exists: for `grok_agent` / `composer_agent` an out-of-range `reasoning_effort` is rejected up front (the value set is fixed — `low`/`medium`/`high`/`xhigh`/`max`); a missing CLI binary or a nonexistent `cwd` fails for all three. A rejected launch — bad `reasoning_effort`, an unresolvable binary, or a nonexistent `cwd` — leaves **zero leftover session** behind, and if the launch keystroke itself can't be delivered aiterm tears the session down. (aiterm does *not* then verify the vendor CLI actually started or authenticated — `openAgent` returns once the launch command is sent; read the session to confirm the agent came up.) (`codex_agent` forwards `reasoning_effort` to the Codex CLI as a config override — `-c model_reasoning_effort=…` — rather than validating it, since Codex's accepted values vary by version.) Pass an absolute path for `cwd` — `~` is not expanded.
|
|
67
|
+
The vendor CLI must be installed and authenticated (`codex` for `codex_agent`; `grok` for both Grok tools). aiterm resolves the binary via `CODEX_BIN` / `GROK_BIN`, then `~/.local/bin/codex` / `~/.grok/bin/grok`, then `PATH`. Prerequisites are checked **before** a session exists: for `grok_agent` / `composer_agent` an out-of-range `reasoning_effort` is rejected up front (the value set is fixed — `low`/`medium`/`high`/`xhigh`/`max`); a missing CLI binary or a nonexistent `cwd` fails for all three. A rejected launch — bad `reasoning_effort`, an unresolvable binary, or a nonexistent `cwd` — leaves **zero leftover session** behind, and if the launch keystroke itself can't be delivered aiterm tears the session down. (aiterm does *not* then verify the vendor CLI actually started or authenticated — `openAgent` returns once the launch command is sent; read the session to confirm the agent came up.) (`codex_agent` forwards `reasoning_effort` to the Codex CLI as a config override — `-c model_reasoning_effort=…` — rather than validating it, since Codex's accepted values vary by version.) Pass an absolute path for `cwd` — `~` is not expanded. `agent_done` uses launch-local managed vendor homes and does not edit your normal hook files; Grok/Composer additionally isolate `GROK_HOME` and `HOME` to suppress compat hook/plugin contamination, while OAuth uses the normal Grok home's `auth.json` and `auth.json.lock` as a pair so refresh locking does not split across sessions. Before the first unbound `pty_send({ wait: "agent_done" })`, aiterm waits for the vendor TUI's input prompt and fails before sending if the prompt is not ready, avoiding dropped startup input. `agent_done` currently requires POSIX filesystem semantics (`getuid`, secure temp files, hard-link checks), so it is supported on Linux, WSL2, and macOS; native Windows can still use the core PTY tools and agent launchers without `agent_done`.
|
|
67
68
|
|
|
68
69
|
There is deliberately **no Claude launcher**, and no protocol between the agents: aiterm's job is to let your Claude (or any MCP client) reach for *other* agents and drive their terminals. Because a launched agent is just another persistent session, everything else in this README applies to it: token-reduced reads, completion detection, and a human `attach` to watch or take over.
|
|
69
70
|
|
|
@@ -206,7 +207,7 @@ On top of that sits a productized layer a raw tmux bridge doesn't have: **token-
|
|
|
206
207
|
| Tool | Role | Key args |
|
|
207
208
|
| --- | --- | --- |
|
|
208
209
|
| `pty_open` | Grab one terminal, return a `session_id` | `name?`, `shell="bash"` |
|
|
209
|
-
| `pty_send` | Send text (a command) | `session_id`, `text`, `enter=true`, `mark`, `force`, `rtk`, `raw` |
|
|
210
|
+
| `pty_send` | Send text (a command) | `session_id`, `text`, `enter=true`, `wait`, `timeout`, `screen`, `lines`, `mark`, `force`, `rtk`, `raw` |
|
|
210
211
|
| `pty_read` | Read output, token-reduced (incremental by default) | `session_id`, `wait`, `until`, `until_regex`, `timeout`, `screen`, `full`, `lines`, `line_range`, `raw`, `rtk` |
|
|
211
212
|
| `pty_key` | Send a control key | `session_id`, `key` (`C-c`/`Enter`/`Up`…) |
|
|
212
213
|
| `pty_close` | Close a session | `session_id` |
|
|
@@ -218,15 +219,15 @@ Each launcher starts a specific vendor's interactive coding-agent TUI inside a f
|
|
|
218
219
|
|
|
219
220
|
| Tool | Launches | Key args |
|
|
220
221
|
| --- | --- | --- |
|
|
221
|
-
| `codex_agent` | Codex CLI (OpenAI; the CLI's default model) | `prompt?`, `reasoning_effort?`, `cwd?`, `session_name?` |
|
|
222
|
-
| `grok_agent` | Grok Build, model `grok-build` (xAI) | `prompt?`, `reasoning_effort?` (`low`/`medium`/`high`/`xhigh`/`max`), `cwd?`, `session_name?` |
|
|
222
|
+
| `codex_agent` | Codex CLI (OpenAI; the CLI's default model) | `prompt?`, `reasoning_effort?`, `cwd?`, `session_name?`, `agent_done?` |
|
|
223
|
+
| `grok_agent` | Grok Build, model `grok-build` (xAI) | `prompt?`, `reasoning_effort?` (`low`/`medium`/`high`/`xhigh`/`max`), `cwd?`, `session_name?`, `agent_done?` |
|
|
223
224
|
| `composer_agent` | Grok Build, model `grok-composer-2.5-fast` (xAI) | same as `grok_agent` |
|
|
224
225
|
|
|
225
|
-
The vendor CLI must be installed and authenticated (`codex` for `codex_agent`; `grok` for both Grok tools). aiterm resolves the binary via `CODEX_BIN` / `GROK_BIN`, then `~/.local/bin/codex` / `~/.grok/bin/grok`, then `PATH`. Prerequisites are validated before a session is created (grok/composer reject an out-of-range `reasoning_effort`; a missing CLI or a nonexistent `cwd` fails for all three), and a failed launch leaves no session behind. Full details under [Launch other coding agents into that terminal](#2-launch-other-coding-agents-into-that-terminal--the-orchestration-flagship). Pass an absolute path for `cwd` — `~` is not expanded.
|
|
226
|
+
The vendor CLI must be installed and authenticated (`codex` for `codex_agent`; `grok` for both Grok tools). aiterm resolves the binary via `CODEX_BIN` / `GROK_BIN`, then `~/.local/bin/codex` / `~/.grok/bin/grok`, then `PATH`. Prerequisites are validated before a session is created (grok/composer reject an out-of-range `reasoning_effort`; a missing CLI or a nonexistent `cwd` fails for all three), and a failed launch leaves no session behind. Full details under [Launch other coding agents into that terminal](#2-launch-other-coding-agents-into-that-terminal--the-orchestration-flagship). Pass an absolute path for `cwd` — `~` is not expanded. `agent_done` is hook-backed for agent launchers; Codex/Grok/Composer have passing live smokes on Linux/WSL2/macOS; native Windows can launch agents but `agent_done` is not supported yet. Before the first unbound agent send, `pty_send({ wait:"agent_done" })` waits for the vendor TUI input prompt and fails before sending if it is not ready. In Grok OAuth mode, aiterm keeps hook/config isolation per launch but shares both `auth.json` and `auth.json.lock` with the normal Grok home; missing OAuth auth fails without leaving a session behind.
|
|
226
227
|
|
|
227
228
|
### Completion detection (5 layers)
|
|
228
229
|
|
|
229
|
-
`pty_read({ wait: true })` decides "is the command done?" via five layers: process exit / a `mark:true` sentinel (auto-detected — see below) / an `until` match (a literal substring by default; pass `until_regex: true` for a regex) / output is quiescent ∧ the shell is back (quiescence) / timeout. While nested (inside SSH, a container, a REPL, or a launched agent's TUI), the "shell is back" check cannot fire, so pass `until` with the inner prompt — or send with `mark: true` and `pty_read({ wait: true })` auto-detects the completion sentinel (no `until` needed, works nested too) — or, for a full-screen agent TUI, read `{ screen: true }` once its output settles.
|
|
230
|
+
`pty_read({ wait: true })` decides "is the command done?" via five layers: process exit / a `mark:true` sentinel (auto-detected — see below) / an `until` match (a literal substring by default; pass `until_regex: true` for a regex) / output is quiescent ∧ the shell is back (quiescence) / timeout. While nested (inside SSH, a container, a REPL, or a launched agent's TUI), the "shell is back" check cannot fire, so pass `until` with the inner prompt — or send with `mark: true` and `pty_read({ wait: true })` auto-detects the completion sentinel (no `until` needed, works nested too) — or, for a full-screen agent TUI, read `{ screen: true }` once its output settles. Sessions launched with `agent_done:true` can instead use `pty_send({ wait:"agent_done" })`, which first waits for the agent TUI input prompt when needed, then waits for the vendor Stop hook and returns the screen after the turn boundary; pre-send readiness failures are MCP errors and timeouts after sending are reported as `is_complete=False via agent_timeout`, not as success. If a complete hook JSONL line is malformed, the timeout suffix includes `malformed_events=N` for diagnosis. If the turn is done but the terminal screen/log does not settle within the flush window, aiterm appends `agent_done_but_screen_unstable`.
|
|
230
231
|
|
|
231
232
|
### Token reduction
|
|
232
233
|
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
const LAUNCH_ID_RE = /^[0-9a-f]{32}$/;
|
|
6
|
+
const SESSION_RE = /^[A-Za-z0-9_-]{1,64}$/;
|
|
7
|
+
const MAX_STDIN_BYTES = 1024 * 1024;
|
|
8
|
+
function fail(message) {
|
|
9
|
+
process.stderr.write(`aiterm codex-stop-hook: ${message}\n`);
|
|
10
|
+
process.stdout.write(JSON.stringify({ continue: false }) + "\n");
|
|
11
|
+
process.exit(0);
|
|
12
|
+
}
|
|
13
|
+
function noop() {
|
|
14
|
+
process.stdout.write(JSON.stringify({ continue: false }) + "\n");
|
|
15
|
+
process.exit(0);
|
|
16
|
+
}
|
|
17
|
+
function hasAitermEnv() {
|
|
18
|
+
return !!(process.env.AITERM_AGENT_KIND ||
|
|
19
|
+
process.env.AITERM_SESSION_ID ||
|
|
20
|
+
process.env.AITERM_AGENT_SESSION_ID ||
|
|
21
|
+
process.env.AITERM_AGENT_LAUNCH_ID);
|
|
22
|
+
}
|
|
23
|
+
function uid() {
|
|
24
|
+
if (typeof process.getuid !== "function")
|
|
25
|
+
fail("POSIX getuid が使えません");
|
|
26
|
+
return process.getuid();
|
|
27
|
+
}
|
|
28
|
+
function runtimeStateBase() {
|
|
29
|
+
const xdg = process.env.XDG_RUNTIME_DIR;
|
|
30
|
+
if (xdg) {
|
|
31
|
+
try {
|
|
32
|
+
if (fs.statSync(xdg).isDirectory())
|
|
33
|
+
return xdg;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
/* XDG_RUNTIME_DIR が壊れている CI/非 login 環境では os.tmpdir() に戻す */
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return os.tmpdir();
|
|
40
|
+
}
|
|
41
|
+
function secureAgentsDir() {
|
|
42
|
+
const root = path.join(runtimeStateBase(), `aiterm-mcp-${uid()}`);
|
|
43
|
+
const agents = path.join(root, "agents");
|
|
44
|
+
const rst = fs.lstatSync(root);
|
|
45
|
+
if (!rst.isDirectory() || rst.isSymbolicLink() || rst.uid !== uid() || (rst.mode & 0o077) !== 0) {
|
|
46
|
+
fail(`agent state root が安全ではありません: ${root}`);
|
|
47
|
+
}
|
|
48
|
+
const ast = fs.lstatSync(agents);
|
|
49
|
+
if (!ast.isDirectory() || ast.isSymbolicLink() || ast.uid !== uid() || (ast.mode & 0o077) !== 0) {
|
|
50
|
+
fail(`agent state dir が安全ではありません: ${agents}`);
|
|
51
|
+
}
|
|
52
|
+
return agents;
|
|
53
|
+
}
|
|
54
|
+
function str(v) {
|
|
55
|
+
return typeof v === "string" ? v : null;
|
|
56
|
+
}
|
|
57
|
+
async function readStdin() {
|
|
58
|
+
const chunks = [];
|
|
59
|
+
let total = 0;
|
|
60
|
+
for await (const chunk of process.stdin) {
|
|
61
|
+
const b = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
|
|
62
|
+
total += b.length;
|
|
63
|
+
if (total > MAX_STDIN_BYTES)
|
|
64
|
+
fail("payload が大きすぎます");
|
|
65
|
+
chunks.push(b);
|
|
66
|
+
}
|
|
67
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
68
|
+
}
|
|
69
|
+
function appendEvent(file, event) {
|
|
70
|
+
const nofollow = fs.constants.O_NOFOLLOW ?? 0;
|
|
71
|
+
const line = JSON.stringify(event) + "\n";
|
|
72
|
+
if (Buffer.byteLength(line, "utf8") > 64 * 1024)
|
|
73
|
+
fail("event line が大きすぎます");
|
|
74
|
+
const fd = fs.openSync(file, fs.constants.O_CREAT | fs.constants.O_APPEND | fs.constants.O_WRONLY | nofollow, 0o600);
|
|
75
|
+
try {
|
|
76
|
+
const st = fs.fstatSync(fd);
|
|
77
|
+
if (!st.isFile() || st.uid !== uid() || st.nlink !== 1 || (st.mode & 0o077) !== 0) {
|
|
78
|
+
fail(`event file が安全ではありません: ${file}`);
|
|
79
|
+
}
|
|
80
|
+
fs.writeSync(fd, line, undefined, "utf8");
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
fs.closeSync(fd);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async function main() {
|
|
87
|
+
if (!hasAitermEnv())
|
|
88
|
+
noop();
|
|
89
|
+
const kind = process.env.AITERM_AGENT_KIND;
|
|
90
|
+
const session = process.env.AITERM_SESSION_ID || process.env.AITERM_AGENT_SESSION_ID || "";
|
|
91
|
+
const launchId = process.env.AITERM_AGENT_LAUNCH_ID || "";
|
|
92
|
+
if (kind !== "codex")
|
|
93
|
+
fail(`AITERM_AGENT_KIND が codex ではありません: ${kind ?? ""}`);
|
|
94
|
+
if (!SESSION_RE.test(session))
|
|
95
|
+
fail(`session id が不正です: ${session}`);
|
|
96
|
+
if (!LAUNCH_ID_RE.test(launchId))
|
|
97
|
+
fail(`launch id が不正です: ${launchId}`);
|
|
98
|
+
let payload = {};
|
|
99
|
+
const input = await readStdin();
|
|
100
|
+
if (input.trim()) {
|
|
101
|
+
try {
|
|
102
|
+
payload = JSON.parse(input);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
payload = {};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const agents = secureAgentsDir();
|
|
109
|
+
const eventFile = path.join(agents, `${session}.${launchId}.events.jsonl`);
|
|
110
|
+
appendEvent(eventFile, {
|
|
111
|
+
type: "agent_done",
|
|
112
|
+
vendor: "codex",
|
|
113
|
+
aiterm_session: session,
|
|
114
|
+
launch_id: launchId,
|
|
115
|
+
vendor_session_id: str(payload.session_id),
|
|
116
|
+
turn_id: str(payload.turn_id),
|
|
117
|
+
reason: str(payload.hook_event_name) ?? "Stop",
|
|
118
|
+
done_status: "turn_done",
|
|
119
|
+
stop_hook_active: !!payload.stop_hook_active,
|
|
120
|
+
at: new Date().toISOString(),
|
|
121
|
+
});
|
|
122
|
+
process.stdout.write(JSON.stringify({ continue: false }) + "\n");
|
|
123
|
+
}
|
|
124
|
+
main().catch((e) => fail(e instanceof Error ? e.message : String(e)));
|