aiterm-mcp 0.37.4 → 0.37.6
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 +17 -1
- package/README.ja.md +21 -19
- package/README.md +22 -20
- package/dist/codex-hook-state.js +126 -0
- package/dist/codex-parent-hook.js +15 -0
- package/dist/codex-parent-hooks.js +112 -0
- package/dist/codex-parent-receiver.js +33 -14
- package/dist/core.js +3 -21
- package/dist/parent-delivery.js +6 -2
- package/dist/psmux-send-worker.js +2 -1
- package/dist/setup-cli.js +3 -4
- package/dist/setup-codex-hooks.js +142 -0
- package/dist/setup-codex-relay.js +1 -1
- package/dist/setup.js +2 -3
- package/dist/tmux-runtime.js +16 -0
- package/docs/DESIGN.md +39 -43
- package/docs/RELEASE.md +15 -16
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.37.6] - 2026-09-19
|
|
11
|
+
|
|
12
|
+
### 修正
|
|
13
|
+
|
|
14
|
+
- WindowsのPowerShellへ複数行を送ると、改行が`Ctrl+Enter`として解釈されて行順が反転し、完了印が処理より先に出る問題を修正した。POSIX shellと同じくscript全体を一括で渡し、現在のshellの変数・作業場所とEnterによる実行開始を保持する。
|
|
15
|
+
|
|
16
|
+
## [0.37.5] - 2026-09-19
|
|
17
|
+
|
|
18
|
+
### 修正
|
|
19
|
+
|
|
20
|
+
- Codex親へのSteer相当の配送を、公式キューと同期hookへ変更する。ツール実行後または回答終了直前に同じターンへ追加し、終了後は公式キューで同じ会話を再開する。終了後の再開には約10秒かかる場合がある。
|
|
21
|
+
- 選択導入は専用hookだけを公式APIで承認・読戻しし、その後に旧中継の起動差し替えを解除する。Codexの通常起動はAitermのNodeとmoduleに依存しない。
|
|
22
|
+
- 利用者自身のキュー入力は保持する。並行hookの取得を一つに制限し、取り出し中断・出力失敗は本文を保存して`unknown`として照会できる。
|
|
23
|
+
|
|
10
24
|
## [0.37.4] - 2026-09-19
|
|
11
25
|
|
|
12
26
|
### 修正
|
|
@@ -1692,7 +1706,9 @@ prototype (preserved under `prototype/python/` as the porting source and referen
|
|
|
1692
1706
|
`ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
|
|
1693
1707
|
provenance.
|
|
1694
1708
|
|
|
1695
|
-
[Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.37.
|
|
1709
|
+
[Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.37.6...HEAD
|
|
1710
|
+
[0.37.6]: https://github.com/kitepon/aiterm-mcp/compare/v0.37.5...v0.37.6
|
|
1711
|
+
[0.37.5]: https://github.com/kitepon/aiterm-mcp/compare/v0.37.4...v0.37.5
|
|
1696
1712
|
[0.37.4]: https://github.com/kitepon/aiterm-mcp/compare/v0.37.3...v0.37.4
|
|
1697
1713
|
[0.37.3]: https://github.com/kitepon/aiterm-mcp/compare/v0.37.2...v0.37.3
|
|
1698
1714
|
[0.37.2]: https://github.com/kitepon/aiterm-mcp/compare/v0.37.1...v0.37.2
|
package/README.ja.md
CHANGED
|
@@ -42,7 +42,7 @@ Ubuntu/Debianはsudoとaptでtmuxを準備する。必要な公式package mana
|
|
|
42
42
|
既存設定の他サーバーを保持し、JSON設定は変更前の`.aiterm-backup`を残す。
|
|
43
43
|
結果の`status`は`ready`/`unsupported`/`failed`/`restart_required`。未検出のAIは`not_detected`とし、全AI未検出は成功にしない。
|
|
44
44
|
登録先はglobal packageのNodeとMCP入口の絶対パスで、npm一時cacheやsource checkoutは登録しない。
|
|
45
|
-
HomebrewのNodeは更新後も有効な`opt`のパスをMCP登録とCodex
|
|
45
|
+
HomebrewのNodeは更新後も有効な`opt`のパスをMCP登録とCodexのhookに使う。旧版の登録でNode更新後に起動できなくなった場合も、更新後の`aiterm-setup --json`で修復できる。
|
|
46
46
|
更新後も同じ入口を実行し、MCP clientを再起動する。npm install自体はユーザー設定を変更しない。
|
|
47
47
|
公開JSONは`schema: "aiterm.setup-result.v1"`、全体の`status`、端末の`backend`、
|
|
48
48
|
AI別の`integrations`と選択機能の`codex_steer`を持つ。失敗時は`reason_code`を付け、終了コードはreadyなら0、再起動待ちは3、それ以外は2となる。
|
|
@@ -56,22 +56,24 @@ AI別の`integrations`と選択機能の`codex_steer`を持つ。失敗時は`re
|
|
|
56
56
|
aiterm-setup --json --codex-steer enable
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
59
|
+
公式キューと公式hookを使い、実行中の親には同じターンの次の推論へ回答を渡し、終了後は同じ会話を自動再開します。
|
|
60
|
+
Codexの起動プログラムと通常のstdio通信は変更しません。hookの実行ファイルが失われてもCodexの起動・応答は継続します。
|
|
61
|
+
終了後の再開は公式キューの監視周期に従い、約10秒かかる場合があります。
|
|
62
|
+
|
|
63
|
+
`aiterm-setup`は`CODEX_HOME/hooks.json`へ専用の`PostToolUse`と`Stop`を追加し、公式APIでその2件だけを承認・読戻しします。
|
|
64
|
+
他のhookや承認は保持します。選択と配送の所有記録は`~/.config/aiterm-mcp/codex-parent-hooks/`へ保存します。
|
|
65
|
+
既存の中継は新しいhookの確認後に解除し、保存していた`CODEX_CLI_PATH`を復元します。macOSの専用LaunchAgentも解除します。
|
|
66
|
+
移行前から動いているCodexがあれば`restart_required`(終了コード3)を返します。完全終了・再起動後に
|
|
67
|
+
`aiterm-setup --codex-steer status`で`ready`を確認してください。旧設定は移行を実行するまで維持します。
|
|
68
|
+
|
|
69
|
+
hookはAiterm自身の配送記録と本文が一致する回答だけを取り出し、利用者がキューに入れた入力は保持します。
|
|
70
|
+
取り出し中断や出力失敗は`parent_deliveries`に`unknown`と`CODEX_HOOK_DELIVERY_UNCONFIRMED`で現れ、本文を保存します。
|
|
71
|
+
自動再送はしません。長い回答はCodexの公式hook処理で抜粋と全文ファイルへの参照になる場合があります。
|
|
72
|
+
|
|
73
|
+
解除・hook未対応の旧版への巻き戻し前は`aiterm-setup --codex-steer disable`を実行してCodexを再起動してください。
|
|
74
|
+
macOS・Windowsの公式Codex Desktopと、公式キュー・hookに対応する同梱CLIを対象にします。
|
|
75
|
+
WindowsのDesktop更新後はsetupを再実行してください。LinuxのSteer付き導入は理由付き`unsupported`を返します。
|
|
76
|
+
Aiterm単品の公式キュー配送は従来どおり利用できます。
|
|
75
77
|
|
|
76
78
|
cloneもビルドも不要。どのクライアントでも公開パッケージを次のコマンドで起動する:
|
|
77
79
|
|
|
@@ -198,7 +200,7 @@ runtime-error store は canonical dotagents config の `collection.enabled: true
|
|
|
198
200
|
場合だけ収集し、既定OFF、network送信は行いません。tag起点CIのnpm provenance(OIDC Trusted
|
|
199
201
|
Publishing)で公開し、GitHub Release が Official MCP Registry を再登録します。
|
|
200
202
|
|
|
201
|
-
**状態:** 開発継続中 · 現行公開版 **v0.37.
|
|
203
|
+
**状態:** 開発継続中 · 現行公開版 **v0.37.6** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
|
|
202
204
|
|
|
203
205
|
### 更新と巻き戻し
|
|
204
206
|
|
|
@@ -569,7 +571,7 @@ Claudeをリンク経由の`cwd`から起動した場合も、実体パスに対
|
|
|
569
571
|
|
|
570
572
|
`pty_send` はコマンドやpromptの意味を判定せず、指定された本文を端末へ送る。既定ではESC・ブラケットペースト終端などをサニタイズし、`pty_read`も制御文字を無害化して返す(`raw: true`はそのまま扱う)。コマンドの許可・拒否はshell、接続先、起動したharnessが所有する。
|
|
571
573
|
|
|
572
|
-
1回の `pty_send` が受理する本文はUTF-8で最大64KiB。同一sessionへの送信はaiterm processをまたいで直列化し、chunk同士の混線を防ぐ。全OSで長いPTY入力の欠落を避けるためplatformのmultiplexerへUTF-8境界を壊さない256-byte単位でpasteし、chunk間に10msのdrain間隔を置く。POSIX shellが前面にいる時のsanitize
|
|
574
|
+
1回の `pty_send` が受理する本文はUTF-8で最大64KiB。同一sessionへの送信はaiterm processをまたいで直列化し、chunk同士の混線を防ぐ。全OSで長いPTY入力の欠落を避けるためplatformのmultiplexerへUTF-8境界を壊さない256-byte単位でpasteし、chunk間に10msのdrain間隔を置く。POSIX shellまたはPowerShellが前面にいる時のsanitize済み複数行は、改行を含まない単一入力へ符号化する。POSIXは`eval`、PowerShellはUTF-8のBase64を復元したscriptblockのdot-sourceを使い、変数と作業場所を現在のshellに保つ。shellがscript全体を所有してから先頭行を実行するため、途中で起動したpager/REPLが後続行を対話キーとして奪わない。単一行、`raw:true`、非shell前面は従来どおり直接PTYへpasteする。agent dispatchはtmux互換のbracketed paste操作(`paste-buffer -p`)を使う: bracketed paste modeを要求しているpaneへは各chunkを`ESC[200~/201~`で包んで届け、chunk投入中のキー解釈による語中文字化け・submit取り落としを抑える。途中chunkが失敗した場合は部分送信済みであることを明示し、自動でEnterを押さない。送信processの異常終了でlockが残った場合は送信前にfail-closedする。`pty_list`で対象sessionを確認し、`pty_close`で閉じてから同じsession IDを作り直す。公開の一括停止toolは存在しない。
|
|
573
575
|
|
|
574
576
|
## 人が覗く
|
|
575
577
|
|
package/README.md
CHANGED
|
@@ -42,7 +42,7 @@ Ubuntu/Debianはsudoとaptでtmuxを準備する。必要な公式package mana
|
|
|
42
42
|
既存設定の他サーバーを保持し、JSON設定は変更前の`.aiterm-backup`を残す。
|
|
43
43
|
結果の`status`は`ready`/`unsupported`/`failed`/`restart_required`。未検出のAIは`not_detected`とし、全AI未検出は成功にしない。
|
|
44
44
|
登録先はglobal packageのNodeとMCP入口の絶対パスで、npm一時cacheやsource checkoutは登録しない。
|
|
45
|
-
HomebrewのNodeは更新後も有効な`opt`のパスをMCP登録とCodex
|
|
45
|
+
HomebrewのNodeは更新後も有効な`opt`のパスをMCP登録とCodexのhookに使う。旧版の登録でNode更新後に起動できなくなった場合も、更新後の`aiterm-setup --json`で修復できる。
|
|
46
46
|
更新後も同じ入口を実行し、MCP clientを再起動する。npm install自体はユーザー設定を変更しない。
|
|
47
47
|
公開JSONは`schema: "aiterm.setup-result.v1"`、全体の`status`、端末の`backend`、
|
|
48
48
|
AI別の`integrations`と選択機能の`codex_steer`を持つ。失敗時は`reason_code`を付け、終了コードはreadyなら0、再起動待ちは3、それ以外は2となる。
|
|
@@ -56,22 +56,24 @@ AI別の`integrations`と選択機能の`codex_steer`を持つ。失敗時は`re
|
|
|
56
56
|
aiterm-setup --json --codex-steer enable
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
59
|
+
公式キューと公式hookを使い、実行中の親には同じターンの次の推論へ回答を渡し、終了後は同じ会話を自動再開します。
|
|
60
|
+
Codexの起動プログラムと通常のstdio通信は変更しません。hookの実行ファイルが失われてもCodexの起動・応答は継続します。
|
|
61
|
+
終了後の再開は公式キューの監視周期に従い、約10秒かかる場合があります。
|
|
62
|
+
|
|
63
|
+
`aiterm-setup`は`CODEX_HOME/hooks.json`へ専用の`PostToolUse`と`Stop`を追加し、公式APIでその2件だけを承認・読戻しします。
|
|
64
|
+
他のhookや承認は保持します。選択と配送の所有記録は`~/.config/aiterm-mcp/codex-parent-hooks/`へ保存します。
|
|
65
|
+
既存の中継は新しいhookの確認後に解除し、保存していた`CODEX_CLI_PATH`を復元します。macOSの専用LaunchAgentも解除します。
|
|
66
|
+
移行前から動いているCodexがあれば`restart_required`(終了コード3)を返します。完全終了・再起動後に
|
|
67
|
+
`aiterm-setup --codex-steer status`で`ready`を確認してください。旧設定は移行を実行するまで維持します。
|
|
68
|
+
|
|
69
|
+
hookはAiterm自身の配送記録と本文が一致する回答だけを取り出し、利用者がキューに入れた入力は保持します。
|
|
70
|
+
取り出し中断や出力失敗は`parent_deliveries`に`unknown`と`CODEX_HOOK_DELIVERY_UNCONFIRMED`で現れ、本文を保存します。
|
|
71
|
+
自動再送はしません。長い回答はCodexの公式hook処理で抜粋と全文ファイルへの参照になる場合があります。
|
|
72
|
+
|
|
73
|
+
解除・hook未対応の旧版への巻き戻し前は`aiterm-setup --codex-steer disable`を実行してCodexを再起動してください。
|
|
74
|
+
macOS・Windowsの公式Codex Desktopと、公式キュー・hookに対応する同梱CLIを対象にします。
|
|
75
|
+
WindowsのDesktop更新後はsetupを再実行してください。LinuxのSteer付き導入は理由付き`unsupported`を返します。
|
|
76
|
+
Aiterm単品の公式キュー配送は従来どおり利用できます。
|
|
75
77
|
|
|
76
78
|
No clone or build is required. Each client launches the published package with:
|
|
77
79
|
|
|
@@ -212,7 +214,7 @@ collection is off by default and performs no network I/O. It ships via
|
|
|
212
214
|
tag-triggered CI with npm provenance (OIDC Trusted Publishing); the GitHub
|
|
213
215
|
Release re-registers the Official MCP Registry entry.
|
|
214
216
|
|
|
215
|
-
**Status:** actively maintained · current public release **v0.37.
|
|
217
|
+
**Status:** actively maintained · current public release **v0.37.6** · 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).
|
|
216
218
|
|
|
217
219
|
### Update and rollback
|
|
218
220
|
|
|
@@ -583,7 +585,7 @@ For PowerShell over SSH, `mark:true` recognizes the current standard `PS ...>` p
|
|
|
583
585
|
|
|
584
586
|
自動配送のreceiptには`parent_delivery`が付き、`wait_process`/`wait_command`はnullになる。`pty_observe`の`parent_deliveries`で`waiting`、`ready`、`sending`、`submitted`、`failed`、`unknown`を確認できる。`submitted`はCodexの公式受信口での受付またはClaudeのhookへの本文出力を示し、modelの読了ではない。MCP再接続後は未送信の記録を再開し、出力中断で結果が分からない場合は本文を保持して`unknown`とする。自動再送はしない。
|
|
585
587
|
|
|
586
|
-
For queue delivery, use a Codex runtime that supplies MCP `_meta.threadId` and the official `thread/queue` API (verified with Codex CLI 0.154.0). `aiterm-setup` checks the installed queue entry point; Aiterm verifies the requesting thread before each dispatch. Codex native sub-agents reject external queue input and cannot be automatic-delivery parents.
|
|
588
|
+
For queue delivery, use a Codex runtime that supplies MCP `_meta.threadId` and the official `thread/queue` API (verified with Codex CLI 0.154.0). `aiterm-setup` checks the installed queue entry point; Aiterm verifies the requesting thread before each dispatch. Codex native sub-agents reject external queue input and cannot be automatic-delivery parents. Steer相当の選択時も公式キューへ投入し、専用hookが同一ターンへ取り込みます。
|
|
587
589
|
|
|
588
590
|
Claude Codeは2.1.259以上の対話sessionに対応する。`aiterm-setup`が専用の`PreToolUse`、`PostToolUse`、`SessionEnd`を登録するため、Channelsの起動flagは不要。公式`asyncRewake` hookだけが裏で待ち、親はその間も次のturnへ進める。回答は`Stop hook feedback`として届く。hookのexit 2は親の再開信号であり、子の成功・失敗は本文の`outcome`で区別する。
|
|
589
591
|
|
|
@@ -614,7 +616,7 @@ Claudeをリンク経由の`cwd`から起動した場合も、実体パスに対
|
|
|
614
616
|
|
|
615
617
|
`pty_send` does not interpret command or prompt meaning; it delivers the requested text to the terminal. By default it sanitizes ESC and bracketed-paste terminators, while `pty_read` neutralizes control characters in returned output (`raw: true` keeps them unchanged). The shell, remote endpoint, or launched harness owns command authorization.
|
|
616
618
|
|
|
617
|
-
Each `pty_send` accepts at most 64 KiB of UTF-8 text. Sends to the same session are serialized across aiterm processes so chunks cannot interleave. Every OS pastes through its multiplexer in UTF-8-safe 256-byte chunks with a 10 ms drain interval; macOS, Linux, and WSL2 have all demonstrated silent middle/trailing loss when a long input is pushed without that boundary. Sanitized multiline text sent while a POSIX shell is in the foreground is encoded as one newline-free `eval`
|
|
619
|
+
Each `pty_send` accepts at most 64 KiB of UTF-8 text. Sends to the same session are serialized across aiterm processes so chunks cannot interleave. Every OS pastes through its multiplexer in UTF-8-safe 256-byte chunks with a 10 ms drain interval; macOS, Linux, and WSL2 have all demonstrated silent middle/trailing loss when a long input is pushed without that boundary. Sanitized multiline text sent while a POSIX shell or PowerShell is in the foreground is encoded as one newline-free input (`eval` for POSIX shells; a dot-sourced, UTF-8 Base64-decoded scriptblock for PowerShell): the shell receives the complete script before it runs the first line, so a pager or REPL started mid-script cannot consume later lines as interactive keystrokes. Single-line input, `raw:true`, and non-shell frontends remain direct PTY pastes. Agent dispatches additionally use the tmux-compatible bracketed-paste operation (`paste-buffer -p`): panes that requested bracketed-paste mode receive each chunk wrapped in `ESC[200~/201~`, hardening prompt injection against mid-word key-interpretation corruption and dropped submits. If a later chunk fails, aiterm reports the partial-send state and does not press Enter automatically. A lock left by a terminated sender fails closed before sending; use `pty_list` to confirm the affected session, close it with `pty_close`, then recreate the same session ID. There is no public kill-all tool.
|
|
618
620
|
|
|
619
621
|
## A human can watch
|
|
620
622
|
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// Codexの設定領域にはhook登録だけを置き、配送の所有情報はAitermが保持する。
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { relayConfigDirectory } from "./codex-relay-config.js";
|
|
7
|
+
import { CodexDeliveryError } from "./codex-delivery-error.js";
|
|
8
|
+
import { readRuntimeProcesses } from "./process-runtime.js";
|
|
9
|
+
const processSchema = z.object({ pid: z.number().int(), started_identity: z.string() });
|
|
10
|
+
export const codexHookConfigSchema = z.object({
|
|
11
|
+
schema: z.literal("aiterm.codex-parent-hooks.v1"), enabled: z.boolean(),
|
|
12
|
+
codex_home: z.string(), binary: z.string(), command: z.string(), node: z.string(), hook: z.string(),
|
|
13
|
+
stale_processes: z.array(processSchema),
|
|
14
|
+
}).strict();
|
|
15
|
+
export function codexHookDirectory() { return path.join(path.dirname(relayConfigDirectory()), "codex-parent-hooks"); }
|
|
16
|
+
export function writeHookJson(file, value) {
|
|
17
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
18
|
+
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
19
|
+
try {
|
|
20
|
+
fs.writeFileSync(temporary, JSON.stringify(value) + "\n", { flag: "wx", mode: 0o600 });
|
|
21
|
+
fs.renameSync(temporary, file);
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
if (fs.existsSync(temporary))
|
|
25
|
+
fs.unlinkSync(temporary);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function readCodexHookConfig(directory = codexHookDirectory()) {
|
|
29
|
+
try {
|
|
30
|
+
return codexHookConfigSchema.parse(JSON.parse(fs.readFileSync(path.join(directory, "config.json"), "utf8")));
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (error.code === "ENOENT")
|
|
34
|
+
return null;
|
|
35
|
+
throw new CodexDeliveryError("CODEX_HOOK_CONFIG_INVALID", "AitermのCodex hook設定を読めません");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function codexInputDirectory(root, home, thread) {
|
|
39
|
+
z.uuid().parse(thread);
|
|
40
|
+
const hash = createHash("sha256").update(fs.realpathSync(home)).digest("hex");
|
|
41
|
+
return path.join(root, "inputs", hash, thread);
|
|
42
|
+
}
|
|
43
|
+
export const hookInputSchema = z.object({
|
|
44
|
+
delivery_id: z.uuid(), thread_id: z.uuid(), text_sha256: z.string(),
|
|
45
|
+
}).strict();
|
|
46
|
+
export function answerDigest(text) { return createHash("sha256").update(text).digest("hex"); }
|
|
47
|
+
export function registerCodexHookInput(home, thread, id, text, root = codexHookDirectory()) {
|
|
48
|
+
const input = hookInputSchema.parse({ delivery_id: id, thread_id: thread, text_sha256: answerDigest(text) });
|
|
49
|
+
const directory = codexInputDirectory(root, home, thread);
|
|
50
|
+
fs.mkdirSync(path.join(directory, "pending"), { recursive: true, mode: 0o700 });
|
|
51
|
+
// 一つの配送IDを別の本文で再使用しない。送信前の所有記録は再送の指示にはしない。
|
|
52
|
+
fs.writeFileSync(path.join(directory, "pending", `${id}.json`), JSON.stringify(input) + "\n", { flag: "wx", mode: 0o600 });
|
|
53
|
+
}
|
|
54
|
+
export function finishCodexHookSubmission(home, thread, id, root) {
|
|
55
|
+
const directory = codexInputDirectory(root, home, thread);
|
|
56
|
+
const file = path.join(directory, "settled", `${id}.json`);
|
|
57
|
+
writeHookJson(file, { delivery_id: id });
|
|
58
|
+
if (!fs.existsSync(path.join(directory, "pending", `${id}.json`)))
|
|
59
|
+
fs.rmSync(file, { force: true });
|
|
60
|
+
}
|
|
61
|
+
export function codexHookDeliveryState(home, thread, id, root = codexHookDirectory()) {
|
|
62
|
+
if (!fs.existsSync(path.join(root, "inputs")))
|
|
63
|
+
return null;
|
|
64
|
+
let value;
|
|
65
|
+
try {
|
|
66
|
+
value = JSON.parse(fs.readFileSync(path.join(codexInputDirectory(root, home, thread), "claims", `${z.uuid().parse(id)}.json`), "utf8"));
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (error.code === "ENOENT")
|
|
70
|
+
return null;
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
// 所有権のlinkだけを作った段階では、公式キューからまだ削除していない。
|
|
74
|
+
if (hookInputSchema.safeParse(value).success)
|
|
75
|
+
return null;
|
|
76
|
+
if (value.state === "unknown")
|
|
77
|
+
return "unknown";
|
|
78
|
+
if (value.state === "deleting")
|
|
79
|
+
return readRuntimeProcesses().some(row => row.pid === value.pid && row.started_identity === value.started_identity) ? "sending" : "unknown";
|
|
80
|
+
if (["emitted", "not_in_queue"].includes(value.state))
|
|
81
|
+
return null;
|
|
82
|
+
throw new CodexDeliveryError("CODEX_HOOK_STATE_INVALID", "hookの配送状態を読めません");
|
|
83
|
+
}
|
|
84
|
+
export function ownedCodexHooks(response, command, file) {
|
|
85
|
+
const rows = response?.data;
|
|
86
|
+
if (!Array.isArray(rows) || rows.length !== 1 || !Array.isArray(rows[0]?.hooks) || rows[0]?.errors?.length) {
|
|
87
|
+
throw new CodexDeliveryError("CODEX_HOOK_UNAVAILABLE", "公式APIでCodexのhook設定を読めません");
|
|
88
|
+
}
|
|
89
|
+
const source = fs.realpathSync(file);
|
|
90
|
+
const owned = rows[0].hooks.filter((hook) => hook.command === command && hook.sourcePath === source);
|
|
91
|
+
for (const event of ["postToolUse", "stop"]) {
|
|
92
|
+
const matches = owned.filter((hook) => hook.eventName === event);
|
|
93
|
+
if (matches.length !== 1 || matches[0].async || matches[0].handlerType !== "command") {
|
|
94
|
+
throw new CodexDeliveryError("CODEX_HOOK_UNAVAILABLE", "Aitermの同期hook登録を確認できません");
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (owned.length !== 2)
|
|
98
|
+
throw new CodexDeliveryError("CODEX_HOOK_UNAVAILABLE", "Aitermのhook登録が重複しています");
|
|
99
|
+
return owned;
|
|
100
|
+
}
|
|
101
|
+
export function assertCodexHooksReady(response, command, file) {
|
|
102
|
+
if (ownedCodexHooks(response, command, file).some(hook => !hook.enabled || !["trusted", "managed"].includes(hook.trustStatus))) {
|
|
103
|
+
throw new CodexDeliveryError("CODEX_HOOK_UNTRUSTED", "AitermのCodex hookが無効または未承認です。aiterm-setupを実行してください");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
export function assertCodexHookParentCurrent(config, rows, pid = process.pid) {
|
|
107
|
+
try {
|
|
108
|
+
fs.accessSync(config.node, fs.constants.X_OK);
|
|
109
|
+
fs.accessSync(config.hook, fs.constants.R_OK);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
throw new CodexDeliveryError("CODEX_HOOK_RUNTIME_UNAVAILABLE", "Codex親hookの実行ファイルを確認できません。aiterm-setupを実行してください");
|
|
113
|
+
}
|
|
114
|
+
if (!config.stale_processes.length)
|
|
115
|
+
return;
|
|
116
|
+
const processes = new Map((rows ?? readRuntimeProcesses()).map(row => [row.pid, row]));
|
|
117
|
+
const seen = new Set();
|
|
118
|
+
let current = processes.get(pid);
|
|
119
|
+
while (current && !seen.has(current.pid)) {
|
|
120
|
+
seen.add(current.pid);
|
|
121
|
+
if (config.stale_processes.some(stale => stale.pid === current.pid && stale.started_identity === current.started_identity)) {
|
|
122
|
+
throw new CodexDeliveryError("CODEX_STEER_RESTART_REQUIRED", "親のCodexはhookの導入前から動いています。完全終了して再起動してください");
|
|
123
|
+
}
|
|
124
|
+
current = processes.get(current.parent_pid);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// stdoutはCodexの公式hook出力だけに使う。失敗をStop継続のexit 2に変換しない。
|
|
3
|
+
import { runCodexResultHook } from "./codex-parent-hooks.js";
|
|
4
|
+
let input = "";
|
|
5
|
+
try {
|
|
6
|
+
for await (const chunk of process.stdin)
|
|
7
|
+
input += chunk;
|
|
8
|
+
await runCodexResultHook(JSON.parse(input), value => new Promise((resolve, reject) => {
|
|
9
|
+
process.stdout.write(JSON.stringify(value) + "\n", error => error ? reject(error) : resolve());
|
|
10
|
+
}), { directory: process.argv[2] });
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
process.stderr.write(`CODEX_PARENT_HOOK_FAILED: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
14
|
+
process.exitCode = 1;
|
|
15
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// 親の同期hookだけが、公式キューのAiterm回答を同じターンの入力へ移す。
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { realCodexHome } from "./harnesses/codex.js";
|
|
6
|
+
import { withCodexReceiver } from "./codex-parent-receiver.js";
|
|
7
|
+
import { CodexDeliveryError } from "./codex-delivery-error.js";
|
|
8
|
+
import { readRuntimeProcesses } from "./process-runtime.js";
|
|
9
|
+
import { answerDigest, codexHookDirectory, codexInputDirectory, hookInputSchema, readCodexHookConfig, writeHookJson } from "./codex-hook-state.js";
|
|
10
|
+
export async function runCodexResultHook(input, emit, options = {}) {
|
|
11
|
+
const event = z.object({ session_id: z.uuid(), turn_id: z.string().min(1),
|
|
12
|
+
hook_event_name: z.enum(["PostToolUse", "Stop"]) }).parse(input);
|
|
13
|
+
const root = options.directory ?? codexHookDirectory();
|
|
14
|
+
const config = readCodexHookConfig(root);
|
|
15
|
+
if (!config?.enabled) {
|
|
16
|
+
await emit({});
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const home = options.codex_home ?? realCodexHome();
|
|
20
|
+
if (fs.realpathSync(home) !== fs.realpathSync(config.codex_home)) {
|
|
21
|
+
throw new CodexDeliveryError("CODEX_HOOK_HOME_MISMATCH", "hookと配送設定のCodex環境が一致しません");
|
|
22
|
+
}
|
|
23
|
+
const directory = codexInputDirectory(root, home, event.session_id);
|
|
24
|
+
const pending = path.join(directory, "pending");
|
|
25
|
+
if (!fs.existsSync(pending) || !fs.readdirSync(pending).length) {
|
|
26
|
+
await emit({});
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const claims = path.join(directory, "claims");
|
|
30
|
+
fs.mkdirSync(claims, { recursive: true, mode: 0o700 });
|
|
31
|
+
const taken = [];
|
|
32
|
+
const identity = readRuntimeProcesses().find(row => row.pid === process.pid);
|
|
33
|
+
if (!identity)
|
|
34
|
+
throw new CodexDeliveryError("CODEX_HOOK_PROCESS_UNAVAILABLE", "受信hookのprocessを確認できません");
|
|
35
|
+
try {
|
|
36
|
+
await withCodexReceiver({ thread_id: event.session_id, codex_home: home }, async (request) => {
|
|
37
|
+
// 削除でoffsetが動くため、全ページを読んでから取り出す。
|
|
38
|
+
const entries = [];
|
|
39
|
+
let cursor = null;
|
|
40
|
+
do {
|
|
41
|
+
const page = await request("thread/queue/list", { threadId: event.session_id, cursor, limit: 100 });
|
|
42
|
+
if (!Array.isArray(page?.data) || !(page.nextCursor === null || typeof page.nextCursor === "string")) {
|
|
43
|
+
throw new CodexDeliveryError("CODEX_HOOK_QUEUE_INVALID", "公式キューの一覧応答を認識できません");
|
|
44
|
+
}
|
|
45
|
+
entries.push(...page.data);
|
|
46
|
+
cursor = page.nextCursor;
|
|
47
|
+
} while (cursor);
|
|
48
|
+
const queued = new Set(entries.map(entry => entry.clientUserMessageId));
|
|
49
|
+
// 送信processが終了し、キューに残っていない所有記録を消す。投入中は触らない。
|
|
50
|
+
for (const name of fs.readdirSync(pending)) {
|
|
51
|
+
const ack = path.join(directory, "settled", name);
|
|
52
|
+
if (name.endsWith(".json") && !queued.has(name.slice(0, -5)) && fs.existsSync(ack)) {
|
|
53
|
+
fs.rmSync(path.join(pending, name), { force: true });
|
|
54
|
+
fs.rmSync(ack, { force: true });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
for (const entry of entries) {
|
|
58
|
+
const id = z.uuid().safeParse(entry.clientUserMessageId);
|
|
59
|
+
if (!id.success)
|
|
60
|
+
continue;
|
|
61
|
+
const source = path.join(pending, `${id.data}.json`);
|
|
62
|
+
let owner;
|
|
63
|
+
try {
|
|
64
|
+
owner = hookInputSchema.parse(JSON.parse(fs.readFileSync(source, "utf8")));
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (error.code === "ENOENT")
|
|
68
|
+
continue;
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
const text = entry.input?.length === 1 && entry.input[0].type === "text" ? entry.input[0].text : undefined;
|
|
72
|
+
if (owner.delivery_id !== id.data || owner.thread_id !== event.session_id || typeof text !== "string" || answerDigest(text) !== owner.text_sha256) {
|
|
73
|
+
throw new CodexDeliveryError("CODEX_HOOK_INPUT_CHANGED", "Aitermの配送記録とキュー本文が一致しません。取り出していません");
|
|
74
|
+
}
|
|
75
|
+
const file = path.join(claims, `${id.data}.json`);
|
|
76
|
+
// 同時hookが同じ回答を取り出すことを防ぎ、中断したclaimは自動再送しない。
|
|
77
|
+
try {
|
|
78
|
+
fs.linkSync(source, file);
|
|
79
|
+
fs.unlinkSync(source);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
if (["ENOENT", "EEXIST"].includes(error.code ?? ""))
|
|
83
|
+
continue;
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
fs.rmSync(path.join(directory, "settled", `${id.data}.json`), { force: true });
|
|
87
|
+
const item = { file, delivery_id: id.data, text };
|
|
88
|
+
taken.push(item);
|
|
89
|
+
writeHookJson(file, { ...owner, text, state: "deleting", turn_id: event.turn_id, queued_submission_id: entry.id,
|
|
90
|
+
pid: identity.pid, started_identity: identity.started_identity });
|
|
91
|
+
const result = await request("thread/queue/delete", { threadId: event.session_id, queuedSubmissionId: entry.id });
|
|
92
|
+
if (typeof result?.deleted !== "boolean")
|
|
93
|
+
throw new CodexDeliveryError("CODEX_HOOK_QUEUE_INVALID", "キュー削除の結果を確認できません", true);
|
|
94
|
+
if (!result.deleted) {
|
|
95
|
+
writeHookJson(file, { ...owner, state: "not_in_queue", turn_id: event.turn_id });
|
|
96
|
+
taken.pop();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}, options.runtime ?? { executable: config.binary, timeout_ms: 5_000 });
|
|
100
|
+
const text = taken.map(item => item.text).join("\n\n");
|
|
101
|
+
await emit(!taken.length ? {} : event.hook_event_name === "Stop"
|
|
102
|
+
? { decision: "block", reason: text }
|
|
103
|
+
: { hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext: text } });
|
|
104
|
+
for (const item of taken)
|
|
105
|
+
writeHookJson(item.file, { delivery_id: item.delivery_id, state: "emitted", turn_id: event.turn_id, text: item.text });
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
for (const item of taken)
|
|
109
|
+
writeHookJson(item.file, { delivery_id: item.delivery_id, state: "unknown", turn_id: event.turn_id, text: item.text });
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Codex
|
|
1
|
+
// Codex親の配送は公式queueを使う。旧中継の選択はsetupで移行するまで維持する。
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { createInterface } from "node:readline";
|
|
4
4
|
import * as path from "node:path";
|
|
@@ -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 { finishCodexHookSubmission, assertCodexHookParentCurrent, assertCodexHooksReady, codexHookDirectory, readCodexHookConfig, registerCodexHookInput } from "./codex-hook-state.js";
|
|
10
11
|
export { CodexDeliveryError } from "./codex-delivery-error.js";
|
|
11
12
|
/** modelの引数ではなく、CodexがMCP要求へ付けるmetadataだけを宛先にする。 */
|
|
12
13
|
export function codexParentFromRequest(clientName, metadata) {
|
|
@@ -21,11 +22,14 @@ export function codexParentFromRequest(clientName, metadata) {
|
|
|
21
22
|
function relaySocket(runtime) {
|
|
22
23
|
if (runtime)
|
|
23
24
|
return runtime.socket_path ?? null;
|
|
25
|
+
if (readCodexHookConfig()?.enabled)
|
|
26
|
+
return null;
|
|
24
27
|
const config = readRelayConfig();
|
|
25
28
|
return config?.enabled ? parentRelaySocket(config) : null;
|
|
26
29
|
}
|
|
27
|
-
async function withCodexReceiver(parent, action, runtime = {}) {
|
|
28
|
-
const
|
|
30
|
+
export async function withCodexReceiver(parent, action, runtime = {}) {
|
|
31
|
+
const config = runtime.executable ? null : readCodexHookConfig();
|
|
32
|
+
const executable = runtime.executable ?? (config?.enabled ? config.binary : null) ?? resolveAgentBin("codex");
|
|
29
33
|
if (!executable)
|
|
30
34
|
throw new CodexDeliveryError("CODEX_RECEIVER_UNAVAILABLE", "Codexの実行ファイルを確認できません");
|
|
31
35
|
const child = spawn(executable, runtime.args ?? ["app-server", "--listen", "stdio://"], {
|
|
@@ -123,6 +127,11 @@ export async function verifyCodexParent(parent, runtime) {
|
|
|
123
127
|
throw new CodexDeliveryError("CODEX_PARENT_UNSUPPORTED", "Codexのnative sub-agentは外部processからのキュー入力を受け付けません");
|
|
124
128
|
}
|
|
125
129
|
await request("thread/queue/list", { threadId: parent.thread_id, limit: 1 });
|
|
130
|
+
const config = runtime ? (runtime.hook_directory ? readCodexHookConfig(runtime.hook_directory) : null) : readCodexHookConfig();
|
|
131
|
+
if (config?.enabled) {
|
|
132
|
+
assertCodexHookParentCurrent(config);
|
|
133
|
+
assertCodexHooksReady(await request("hooks/list", { cwds: [response.thread.cwd] }), config.command, path.join(parent.codex_home, "hooks.json"));
|
|
134
|
+
}
|
|
126
135
|
}, runtime);
|
|
127
136
|
}
|
|
128
137
|
export async function submitCodexParentAnswer(parent, deliveryId, text, runtime) {
|
|
@@ -139,15 +148,25 @@ export async function submitCodexParentAnswer(parent, deliveryId, text, runtime)
|
|
|
139
148
|
return { queued_submission_id: null };
|
|
140
149
|
}, runtime?.timeout_ms);
|
|
141
150
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
151
|
+
const root = runtime ? runtime.hook_directory : codexHookDirectory();
|
|
152
|
+
const hook = root && readCodexHookConfig(root)?.enabled;
|
|
153
|
+
if (hook)
|
|
154
|
+
registerCodexHookInput(parent.codex_home, parent.thread_id, deliveryId, text, root);
|
|
155
|
+
try {
|
|
156
|
+
return await withCodexReceiver(parent, async (request) => {
|
|
157
|
+
const result = await request("thread/queue/add", {
|
|
158
|
+
threadId: parent.thread_id,
|
|
159
|
+
input: [{ type: "text", text, text_elements: [] }],
|
|
160
|
+
clientUserMessageId: deliveryId,
|
|
161
|
+
});
|
|
162
|
+
if (typeof result?.queuedSubmission?.id !== "string") {
|
|
163
|
+
throw new CodexDeliveryError("CODEX_RECEIVER_INVALID_RESPONSE", "キューの受付IDを確認できません", true);
|
|
164
|
+
}
|
|
165
|
+
return { queued_submission_id: result.queuedSubmission.id };
|
|
166
|
+
}, runtime);
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
if (hook)
|
|
170
|
+
finishCodexHookSubmission(parent.codex_home, parent.thread_id, deliveryId, root);
|
|
171
|
+
}
|
|
153
172
|
}
|
package/dist/core.js
CHANGED
|
@@ -16,7 +16,7 @@ import * as rtk from "./rtk.js";
|
|
|
16
16
|
import { paneTokenHint } from "./harnesses/pane-tokens.js";
|
|
17
17
|
import { readRuntimeProcesses, processSubtree, processIdentity, backgroundProcesses } from "./process-runtime.js";
|
|
18
18
|
import { AitermError, telemetryOwnedFailure, ownTelemetryFailure } from "./errors.js";
|
|
19
|
-
import { isWin, SOCKDIR, tmuxCommand, sendPsmuxPayload, loadPtyBufferChunk, pasteBufferBaseArgs, TMUX_EMPTY_CONFIG, attachCommand, normalizePaneCommand, appendMarkSentinel, markShellCommand, settlePaneLog, paneCwdArgument, sessionEnvironmentLaunch, } from "./tmux-runtime.js";
|
|
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, grokStartupAction, grokLaunchBlockingDialog, assertGrokSandboxNotRejected, GROK_COMPOSER_MARKER_RE, grokFooterHasConfiguration, grokTranscriptText, createGrokAgentMetadata, } from "./harnesses/grok.js";
|
|
22
22
|
import { bindCodexTranscriptSession, latestCodexCompletion, observeCodexDone, buildCodexAgentCmd, codexLaunchNote, codexTuiReady, codexPaneObservation, codexApprovalDialog, codexStartupAction, CODEX_COMPOSER_MARKER_RE, codexModelChoice, codexEffortChoice, codexMoreReasoningChoice, codexTranscriptText, createCodexAgentMetadata, } from "./harnesses/codex.js";
|
|
@@ -31,11 +31,6 @@ export const DEFAULT_TIMEOUT = 10.0;
|
|
|
31
31
|
const POLL = 0.25;
|
|
32
32
|
const STABLE_POLLS = 2; // 連続でログサイズ不変ならば静止とみなす回数
|
|
33
33
|
const SHELLS = new Set(["bash", "sh", "zsh", "fish", "dash"]);
|
|
34
|
-
// 通常PTYへ複数行をそのままpasteすると、途中で起動したpager/REPLが後続行を
|
|
35
|
-
// キー入力として消費する。POSIX shell前面では改行を持たないeval 1行へ包み、
|
|
36
|
-
// script全体をshell内部へ取り込んでから実行する。fish等の非POSIX shellや
|
|
37
|
-
// ssh/REPL前面は従来の生pasteを維持する。
|
|
38
|
-
const ATOMIC_MULTILINE_SHELLS = new Set(["bash", "sh", "zsh", "dash"]);
|
|
39
34
|
// mark の sentinel は POSIX シェル構文(; と "$?")に依存する。これらの非 POSIX 対話シェルが
|
|
40
35
|
// 前面のときは "$?" が正しく展開されず sentinel が壊れるので mark を拒否する(B8)。ssh/docker で
|
|
41
36
|
// リモート POSIX シェルに入っている場合は前面が "ssh"/"docker" 等で本集合に含まれず=許可される。
|
|
@@ -270,19 +265,6 @@ function splitPtyText(text) {
|
|
|
270
265
|
chunks.push(chunk);
|
|
271
266
|
return chunks;
|
|
272
267
|
}
|
|
273
|
-
function quoteForPrintfB(text) {
|
|
274
|
-
return text
|
|
275
|
-
.replace(/\\/g, "\\\\")
|
|
276
|
-
.replace(/\n/g, "\\n")
|
|
277
|
-
.replace(/\r/g, "\\r")
|
|
278
|
-
.replace(/\t/g, "\\t")
|
|
279
|
-
.replace(/'/g, `'"'"'`);
|
|
280
|
-
}
|
|
281
|
-
function atomicShellMultiline(text) {
|
|
282
|
-
// POSIX printf %bで元の改行・backslashを復元し、evalは現在shell内で実行する。
|
|
283
|
-
// command substitutionが末尾LFを落とす点は、pty_sendのsubmitを担うEnterと同値。
|
|
284
|
-
return `eval "$(command printf '%b' '${quoteForPrintfB(text)}')"`;
|
|
285
|
-
}
|
|
286
268
|
function assertSendTextSize(text, context = "送信文字列") {
|
|
287
269
|
const bytes = Buffer.byteLength(text, "utf8");
|
|
288
270
|
if (bytes > MAX_SEND_BYTES) {
|
|
@@ -792,8 +774,8 @@ export function send(name, text, o = {}) {
|
|
|
792
774
|
text = appendMarkSentinel(text, fg);
|
|
793
775
|
assertSendTextSize(text, o.rtk || o.mark ? "変換後の送信文字列" : "送信文字列");
|
|
794
776
|
const reportedText = text;
|
|
795
|
-
if (!o.raw && text.includes("\n")
|
|
796
|
-
text = atomicShellMultiline(text);
|
|
777
|
+
if (!o.raw && text.includes("\n")) {
|
|
778
|
+
text = atomicShellMultiline(text, fg || paneCurrentCommand(name));
|
|
797
779
|
}
|
|
798
780
|
if (o.mark) {
|
|
799
781
|
try {
|
package/dist/parent-delivery.js
CHANGED
|
@@ -9,6 +9,7 @@ import { readRuntimeProcesses } from "./process-runtime.js";
|
|
|
9
9
|
import { CodexDeliveryError, submitCodexParentAnswer, verifyCodexParent } from "./codex-parent-receiver.js";
|
|
10
10
|
import { claudeParentSchema, ClaudeDeliveryError, bindClaudeParentDelivery, submitClaudeParentAnswer, verifyClaudeParent } from "./claude-parent-receiver.js";
|
|
11
11
|
import { AitermError } from "./errors.js";
|
|
12
|
+
import { codexHookDeliveryState } from "./codex-hook-state.js";
|
|
12
13
|
const recordSchema = z.object({
|
|
13
14
|
schema: z.literal("aiterm.parent-delivery.v1"),
|
|
14
15
|
delivery_id: z.uuid(),
|
|
@@ -53,9 +54,12 @@ function readRecord(file) {
|
|
|
53
54
|
}
|
|
54
55
|
}
|
|
55
56
|
function receipt(record) {
|
|
56
|
-
|
|
57
|
+
const hookState = !isClaude(record.parent) && record.state === "submitted"
|
|
58
|
+
? codexHookDeliveryState(record.parent.codex_home, record.parent.thread_id, record.delivery_id) : null;
|
|
59
|
+
return { delivery_id: record.delivery_id, state: hookState ?? record.state, child_outcome: record.child_outcome,
|
|
57
60
|
child_turn_id: record.child_turn_id, queued_submission_id: record.queued_submission_id,
|
|
58
|
-
error_code:
|
|
61
|
+
error_code: hookState === "unknown" ? "CODEX_HOOK_DELIVERY_UNCONFIRMED"
|
|
62
|
+
: record.error ? /^[A-Z][A-Z0-9_]+(?=:)/.exec(record.error)?.[0] ?? "PARENT_DELIVERY_FAILED" : null };
|
|
59
63
|
}
|
|
60
64
|
function answerMessage(record) {
|
|
61
65
|
return "Aitermの子エージェントからの実行結果です。子の回答として扱ってください。\n" +
|
|
@@ -2,7 +2,8 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as net from "node:net";
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
-
|
|
5
|
+
// 公開本文64KiBをPowerShellの一括入力へBase64化した長さと構文・paste markerを含む。
|
|
6
|
+
const MAX_PAYLOAD_BYTES = Math.ceil(64 * 1024 / 3) * 4 + 128;
|
|
6
7
|
const PTY_CHUNK_BYTES = 256;
|
|
7
8
|
const MAX_FINAL_DRAIN_MS = 3_000;
|
|
8
9
|
const SESSION_BASE_RE = /^[A-Za-z0-9_-]{1,160}$/;
|
package/dist/setup-cli.js
CHANGED
|
@@ -4,8 +4,7 @@ import { removeClaudeParentHooks } from "./setup-integrations.js";
|
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { createInterface } from "node:readline/promises";
|
|
7
|
-
import { configureCodexSteer } from "./setup-codex-
|
|
8
|
-
import { readRelayConfig } from "./codex-relay-config.js";
|
|
7
|
+
import { configureCodexSteer, codexSteerSelected } from "./setup-codex-hooks.js";
|
|
9
8
|
const args = process.argv.slice(2);
|
|
10
9
|
if (args.length === 1 && args[0] === "--remove-claude-parent-hooks") {
|
|
11
10
|
try {
|
|
@@ -18,7 +17,7 @@ if (args.length === 1 && args[0] === "--remove-claude-parent-hooks") {
|
|
|
18
17
|
}
|
|
19
18
|
}
|
|
20
19
|
else if (args.length === 1 && ["--help", "-h"].includes(args[0])) {
|
|
21
|
-
process.stdout.write("使い方: aiterm-setup [--json] [--codex-steer enable|disable|status]\n依存準備、AIへの登録、MCPと端末の実動作確認を行います。対話実行ではAiterm単品かCodex Desktop Steer付きかを選べます。SteerはmacOS・Windows対応で、初回はCodexの再起動が必要です。disable
|
|
20
|
+
process.stdout.write("使い方: aiterm-setup [--json] [--codex-steer enable|disable|status]\n依存準備、AIへの登録、MCPと端末の実動作確認を行います。対話実行ではAiterm単品かCodex Desktop Steer付きかを選べます。SteerはmacOS・Windows対応で、初回はCodexの再起動が必要です。disableは専用hookを解除し、statusは公式hookの登録・承認と再起動の必要性を確認します。旧中継はhook導入後に解除します。--jsonは対話せず、Steerの選択を維持します。\n旧版へ戻す前のClaude専用hook解除: --remove-claude-parent-hooks\n");
|
|
22
21
|
}
|
|
23
22
|
else {
|
|
24
23
|
try {
|
|
@@ -34,7 +33,7 @@ else {
|
|
|
34
33
|
}
|
|
35
34
|
const onlySteer = action === "disable" || action === "status";
|
|
36
35
|
if (!action && !json && process.stdin.isTTY && process.stderr.isTTY && ["darwin", "win32"].includes(process.platform)) {
|
|
37
|
-
const enabled =
|
|
36
|
+
const enabled = codexSteerSelected();
|
|
38
37
|
const prompt = createInterface({ input: process.stdin, output: process.stderr });
|
|
39
38
|
try {
|
|
40
39
|
const answer = (await prompt.question(`導入方法: 1=Aiterm単品(公式キュー配送)、2=Codex DesktopへSteerと終了後再開を有効化 [${enabled ? "2" : "1"}]: `)).trim();
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// Steerの正規導入。公式hookを登録・承認してから、旧中継の起動差し替えを解除する。
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { isDeepStrictEqual } from "node:util";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { withCodexReceiver } from "./codex-parent-receiver.js";
|
|
7
|
+
import { realCodexHome } from "./harnesses/codex.js";
|
|
8
|
+
import { SetupError } from "./setup-platform.js";
|
|
9
|
+
import { setupNodeExecutable } from "./setup-node.js";
|
|
10
|
+
import { readRelayConfig } from "./codex-relay-config.js";
|
|
11
|
+
import { configureCodexSteer as configureLegacyRelay, findDesktopBinary } from "./setup-codex-relay.js";
|
|
12
|
+
import { findWindowsCodexBinary } from "./windows-codex-setup.js";
|
|
13
|
+
import { readRuntimeProcesses } from "./process-runtime.js";
|
|
14
|
+
import { resolveWindowsPowerShell7 } from "./windows-powershell.js";
|
|
15
|
+
import { quotePowerShell } from "./windows-codex-state.js";
|
|
16
|
+
import { assertCodexHooksReady, codexHookDirectory, ownedCodexHooks, readCodexHookConfig, writeHookJson } from "./codex-hook-state.js";
|
|
17
|
+
export function codexSteerSelected() { return readCodexHookConfig()?.enabled === true || readRelayConfig()?.enabled === true; }
|
|
18
|
+
export function codexHookCommand(node, hook, directory, platform = process.platform) {
|
|
19
|
+
if (platform === "win32") {
|
|
20
|
+
const script = `& ${[node, hook, directory].map(quotePowerShell).join(" ")}; exit $LASTEXITCODE`;
|
|
21
|
+
return `"${resolveWindowsPowerShell7()}" -NoLogo -NoProfile -NonInteractive -EncodedCommand ${Buffer.from(script, "utf16le").toString("base64")}`;
|
|
22
|
+
}
|
|
23
|
+
return [node, hook, directory].map(value => "'" + value.replace(/'/g, "'\"'\"'") + "'").join(" ");
|
|
24
|
+
}
|
|
25
|
+
export function mergeCodexParentHooks(file, command, previousCommand) {
|
|
26
|
+
if (!fs.existsSync(file)) {
|
|
27
|
+
try {
|
|
28
|
+
if (fs.lstatSync(file).isSymbolicLink())
|
|
29
|
+
throw new SetupError("codex_hook_config_invalid", "hook設定symlinkの参照先がありません");
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (error.code !== "ENOENT")
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const target = fs.existsSync(file) ? fs.realpathSync(file) : file;
|
|
37
|
+
let current = {};
|
|
38
|
+
if (fs.existsSync(target)) {
|
|
39
|
+
try {
|
|
40
|
+
current = JSON.parse(fs.readFileSync(target, "utf8"));
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
throw new SetupError("codex_hook_config_invalid", "Codexのhook設定JSONを読めません");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const object = (value) => value && typeof value === "object" && !Array.isArray(value);
|
|
47
|
+
if (!object(current) || (current.hooks !== undefined && !object(current.hooks)))
|
|
48
|
+
throw new SetupError("codex_hook_config_invalid", "Codexのhooks設定を読めません");
|
|
49
|
+
const hooks = { ...current.hooks };
|
|
50
|
+
for (const event of ["PostToolUse", "Stop"]) {
|
|
51
|
+
const groups = hooks[event] ?? [];
|
|
52
|
+
if (!Array.isArray(groups) || groups.some(group => !object(group) || !Array.isArray(group.hooks)))
|
|
53
|
+
throw new SetupError("codex_hook_config_invalid", `${event}のhook設定を読めません`);
|
|
54
|
+
hooks[event] = groups.map(group => ({ ...group, hooks: group.hooks.filter((hook) => !(hook?.type === "command" && ((command !== null && hook.command === command) || (previousCommand && hook.command === previousCommand)))) })).filter(group => group.hooks.length);
|
|
55
|
+
if (command !== null)
|
|
56
|
+
hooks[event].push({ ...(event === "PostToolUse" ? { matcher: ".*" } : {}),
|
|
57
|
+
hooks: [{ type: "command", command, timeout: 20, ...(event === "PostToolUse" ? { additionalContextLimit: 0 } : {}) }] });
|
|
58
|
+
if (!hooks[event].length)
|
|
59
|
+
delete hooks[event];
|
|
60
|
+
}
|
|
61
|
+
const next = { ...current, hooks };
|
|
62
|
+
if (isDeepStrictEqual(current, next))
|
|
63
|
+
return false;
|
|
64
|
+
if (fs.existsSync(target))
|
|
65
|
+
fs.copyFileSync(target, `${target}.aiterm-backup`);
|
|
66
|
+
writeHookJson(target, next);
|
|
67
|
+
if (!isDeepStrictEqual(JSON.parse(fs.readFileSync(target, "utf8")), next))
|
|
68
|
+
throw new SetupError("codex_hook_readback_failed", "Codex hookの読戻しが一致しません");
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
export async function verifyCodexHookRegistration(config, approve) {
|
|
72
|
+
fs.accessSync(config.node, fs.constants.X_OK);
|
|
73
|
+
fs.accessSync(config.hook, fs.constants.R_OK);
|
|
74
|
+
const file = path.join(config.codex_home, "hooks.json");
|
|
75
|
+
await withCodexReceiver({ thread_id: "00000000-0000-4000-8000-000000000000", codex_home: config.codex_home }, async (request) => {
|
|
76
|
+
const list = () => request("hooks/list", { cwds: [config.codex_home] });
|
|
77
|
+
const hooks = ownedCodexHooks(await list(), config.command, file);
|
|
78
|
+
if (approve) {
|
|
79
|
+
const edits = hooks.filter(hook => !hook.enabled || hook.trustStatus !== "trusted").flatMap(hook => {
|
|
80
|
+
if (typeof hook.key !== "string" || typeof hook.currentHash !== "string")
|
|
81
|
+
throw new SetupError("codex_hook_schema_unknown", "公式hookの承認情報を認識できません");
|
|
82
|
+
const key = `hooks.state.${JSON.stringify(hook.key)}`;
|
|
83
|
+
return [
|
|
84
|
+
{ keyPath: `${key}.trusted_hash`, value: hook.currentHash, mergeStrategy: "replace" },
|
|
85
|
+
{ keyPath: `${key}.enabled`, value: true, mergeStrategy: "replace" },
|
|
86
|
+
];
|
|
87
|
+
});
|
|
88
|
+
if (edits.length)
|
|
89
|
+
await request("config/batchWrite", { edits, filePath: path.join(config.codex_home, "config.toml") });
|
|
90
|
+
}
|
|
91
|
+
assertCodexHooksReady(await list(), config.command, file);
|
|
92
|
+
}, { executable: config.binary });
|
|
93
|
+
}
|
|
94
|
+
export async function configureCodexSteer(action = "status", overrides = {}) {
|
|
95
|
+
const runtime = { platform: process.platform, directory: codexHookDirectory(), codex_home: realCodexHome(),
|
|
96
|
+
node: process.execPath, hook: fileURLToPath(new URL("./codex-parent-hook.js", import.meta.url)),
|
|
97
|
+
findBinary: process.platform === "win32" ? findWindowsCodexBinary : findDesktopBinary,
|
|
98
|
+
processes: readRuntimeProcesses, legacy: readRelayConfig,
|
|
99
|
+
disableLegacy: () => configureLegacyRelay("disable"), verify: verifyCodexHookRegistration, ...overrides };
|
|
100
|
+
const previous = readCodexHookConfig(runtime.directory);
|
|
101
|
+
const legacy = runtime.legacy();
|
|
102
|
+
const file = path.join(action === "disable" && previous ? previous.codex_home : runtime.codex_home, "hooks.json");
|
|
103
|
+
const save = (config) => writeHookJson(path.join(runtime.directory, "config.json"), config);
|
|
104
|
+
const needsRestart = (config) => runtime.processes().some(process => config.stale_processes.some(stale => stale.pid === process.pid && stale.started_identity === process.started_identity));
|
|
105
|
+
if (action === "status") {
|
|
106
|
+
if (!previous?.enabled)
|
|
107
|
+
return legacy?.enabled ? { status: "failed", reason_code: "codex_steer_migration_required" } : { status: "disabled" };
|
|
108
|
+
await runtime.verify(previous, false);
|
|
109
|
+
return legacy?.enabled || needsRestart(previous) ? { status: "restart_required", reason_code: "codex_restart_required" } : { status: "ready" };
|
|
110
|
+
}
|
|
111
|
+
if (action === "disable") {
|
|
112
|
+
if (previous?.enabled) {
|
|
113
|
+
mergeCodexParentHooks(file, null, previous.command);
|
|
114
|
+
save({ ...previous, enabled: false });
|
|
115
|
+
}
|
|
116
|
+
if (legacy?.enabled)
|
|
117
|
+
return runtime.disableLegacy();
|
|
118
|
+
return previous?.enabled ? { status: "restart_required", reason_code: "codex_restart_required" } : { status: "disabled" };
|
|
119
|
+
}
|
|
120
|
+
if (!["darwin", "win32"].includes(runtime.platform))
|
|
121
|
+
return { status: "unsupported", reason_code: "codex_steer_platform_unsupported" };
|
|
122
|
+
if (previous?.enabled && fs.realpathSync(previous.codex_home) !== fs.realpathSync(runtime.codex_home))
|
|
123
|
+
throw new SetupError("codex_steer_configuration_conflict", "別のCODEX_HOMEでSteerが有効です。先に既存の選択を解除してください");
|
|
124
|
+
const node = setupNodeExecutable(runtime.node);
|
|
125
|
+
fs.accessSync(node, fs.constants.X_OK);
|
|
126
|
+
fs.accessSync(runtime.hook, fs.constants.R_OK);
|
|
127
|
+
const binary = runtime.findBinary();
|
|
128
|
+
const command = codexHookCommand(node, runtime.hook, runtime.directory, runtime.platform);
|
|
129
|
+
const changed = mergeCodexParentHooks(file, command, previous?.command);
|
|
130
|
+
const stale = !previous?.enabled || changed || legacy?.enabled
|
|
131
|
+
? runtime.processes().filter(row => row.executable === binary || row.command.startsWith(binary + " "))
|
|
132
|
+
.map(({ pid, started_identity }) => ({ pid, started_identity }))
|
|
133
|
+
: previous.stale_processes;
|
|
134
|
+
const config = { schema: "aiterm.codex-parent-hooks.v1", enabled: true,
|
|
135
|
+
codex_home: runtime.codex_home, binary, command, node, hook: runtime.hook, stale_processes: stale };
|
|
136
|
+
await runtime.verify(config, true);
|
|
137
|
+
// 解除が中断しても、次のenableで移行を続行できるよう所有情報を先に保存する。
|
|
138
|
+
save(config);
|
|
139
|
+
if (legacy?.enabled)
|
|
140
|
+
await runtime.disableLegacy();
|
|
141
|
+
return needsRestart(config) ? { status: "restart_required", reason_code: "codex_restart_required" } : { status: "ready" };
|
|
142
|
+
}
|
|
@@ -29,7 +29,7 @@ function getGui(key) {
|
|
|
29
29
|
throw new SetupError("codex_steer_environment_unavailable", "GUIの起動設定を確認できません");
|
|
30
30
|
return result.stdout.trim() || null;
|
|
31
31
|
}
|
|
32
|
-
function findDesktopBinary() {
|
|
32
|
+
export function findDesktopBinary() {
|
|
33
33
|
const search = spawnSync("/usr/bin/mdfind", ["kMDItemCFBundleIdentifier == 'com.openai.codex'"], { encoding: "utf8", timeout: 10_000 });
|
|
34
34
|
const candidates = [...new Set(["/Applications/Codex.app", "/Applications/ChatGPT.app", ...(search.status === 0 ? search.stdout.trim().split("\n") : [])])];
|
|
35
35
|
const found = candidates.filter(app => {
|
package/dist/setup.js
CHANGED
|
@@ -8,8 +8,7 @@ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
|
|
|
8
8
|
import { CallToolResultSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
9
9
|
import { prepareBackend, runSetupCommand, SetupError } from "./setup-platform.js";
|
|
10
10
|
import { configureIntegrations } from "./setup-integrations.js";
|
|
11
|
-
import { configureCodexSteer } from "./setup-codex-
|
|
12
|
-
import { readRelayConfig } from "./codex-relay-config.js";
|
|
11
|
+
import { configureCodexSteer, codexSteerSelected } from "./setup-codex-hooks.js";
|
|
13
12
|
import { setupNodeExecutable } from "./setup-node.js";
|
|
14
13
|
export function globalRegistration(run = runSetupCommand) {
|
|
15
14
|
const root = run(process.platform === "win32" ? "npm.cmd" : "npm", ["root", "-g"]).trim();
|
|
@@ -80,7 +79,7 @@ export async function runSetup(options = {}) {
|
|
|
80
79
|
result.status = "ready";
|
|
81
80
|
if (result.status === "ready") {
|
|
82
81
|
stage = "codex_steer";
|
|
83
|
-
result.codex_steer = await (options.steer ?? configureCodexSteer)(options.codex_steer ?? (
|
|
82
|
+
result.codex_steer = await (options.steer ?? configureCodexSteer)(options.codex_steer ?? (codexSteerSelected() ? "enable" : "status"));
|
|
84
83
|
if (["failed", "unsupported", "restart_required"].includes(result.codex_steer.status)) {
|
|
85
84
|
result.status = result.codex_steer.status;
|
|
86
85
|
result.reason_code = result.codex_steer.reason_code;
|
package/dist/tmux-runtime.js
CHANGED
|
@@ -217,6 +217,22 @@ export function normalizePaneCommand(cmd) {
|
|
|
217
217
|
}
|
|
218
218
|
// 完了マーカーの方言は実効shellで決める。SSH先のshellはhost OSやsshのprocess名と異なる。
|
|
219
219
|
const POWERSHELL_COMMANDS = new Set(["powershell", "pwsh"]);
|
|
220
|
+
const POSIX_ATOMIC_SHELLS = new Set(["bash", "sh", "zsh", "dash"]);
|
|
221
|
+
// shellにscript全体を取り込ませてから現在のscopeで実行する。生LFはWindowsの
|
|
222
|
+
// Console.ReadKeyでCtrl+Enterとなり、PSReadLineでは行が逆順に挿入される。
|
|
223
|
+
// OSで仕組みを変えず、同じ一括入力を各shellの構文へ適合させる。
|
|
224
|
+
export function atomicShellMultiline(text, foreground) {
|
|
225
|
+
if (POWERSHELL_COMMANDS.has(foreground)) {
|
|
226
|
+
const encoded = Buffer.from(text, "utf8").toString("base64");
|
|
227
|
+
return `. ([scriptblock]::Create([Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encoded}'))))`;
|
|
228
|
+
}
|
|
229
|
+
if (!POSIX_ATOMIC_SHELLS.has(foreground))
|
|
230
|
+
return text;
|
|
231
|
+
const quoted = text.replace(/\\/g, "\\\\").replace(/\n/g, "\\n")
|
|
232
|
+
.replace(/\r/g, "\\r").replace(/\t/g, "\\t").replace(/'/g, `'"'"'`);
|
|
233
|
+
// printf %bで本文を復元する。末尾LFの除去はpty_sendのsubmit Enterと同値。
|
|
234
|
+
return `eval "$(command printf '%b' '${quoted}')"`;
|
|
235
|
+
}
|
|
220
236
|
export function markShellCommand(foreground, screen) {
|
|
221
237
|
if (POWERSHELL_COMMANDS.has(foreground))
|
|
222
238
|
return foreground;
|
package/docs/DESIGN.md
CHANGED
|
@@ -22,6 +22,12 @@ Claude/CursorのJSONは参照先を原子的に更新して変更前backupを
|
|
|
22
22
|
SSHやcontainerを別toolにせず、PTY内で実行するcommandとして扱う。sessionはPOSIXではtmux、
|
|
23
23
|
Windows nativeではpsmux 3.3.8以上に保存され、MCP serverやclientの再起動をまたぐ。
|
|
24
24
|
|
|
25
|
+
sanitize済みの複数行は、POSIX shellでもPowerShellでもscript全体を取り込んでから記述順に実行する。
|
|
26
|
+
`src/tmux-runtime.ts`が改行を持たない1回の入力へ適合させ、POSIXは`eval`、PowerShellは
|
|
27
|
+
UTF-8のBase64を復元したscriptblockのdot-sourceを使う。変数と作業場所は現在のshellに残す。
|
|
28
|
+
生LFをPowerShellへキー入力として流さないため、Windowsの`Ctrl+Enter`による行順の反転を防ぐ。
|
|
29
|
+
単一行、`raw:true`、非shell前面は直接PTYへ送る。`enter:false`では後続のEnterまで実行しない。
|
|
30
|
+
|
|
25
31
|
`pty_read`は制御文字除去、反復圧縮、head+tail、command別reducerでcontext量を減らす。
|
|
26
32
|
完了はprocess exit、shell sentinel、literal/regex `until`、shell復帰を伴うquiescence、timeoutを区別する。
|
|
27
33
|
要求されたsentinel/untilを静止判定より優先し、nested shellで証拠がない状態を完了へ丸めない。
|
|
@@ -68,49 +74,39 @@ Cursorのsubmitはadapterがextended keyboard protocolのEnterへ変換し、呼
|
|
|
68
74
|
宛先はCodexのMCP handshakeと各要求の`_meta.threadId`から取得する。modelが指定したIDや環境変数で代用しない。
|
|
69
75
|
単品導入の`src/codex-parent-receiver.ts`は同じ`CODEX_HOME`の公式app-serverへstdioで接続し、
|
|
70
76
|
`thread/read`と`thread/queue/list`で宛先を確認してから`thread/queue/add`へ本文をJSONで渡す。
|
|
71
|
-
Steerを選択したmacOS・Windows
|
|
72
|
-
`
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
`
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
Windows
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
所有するprocessだけを終了し、公式serverの終了確認後に戻る。
|
|
105
|
-
両OSとも有効化前に元の起動設定を保存し、Aitermのlauncherを設定する。解除は元の値へ戻す。
|
|
106
|
-
旧Windows版で既存launcherを共有した設定は読取り可能だが、次のenableでAitermのlauncherへ移行する。
|
|
107
|
-
起動・配送・設定・診断は他製品のコード、設定、コマンドへ依存しない。
|
|
108
|
-
LinuxのSteer選択はunsupportedとする。通常の単品導入・queue配送は全対応OSで維持する。
|
|
109
|
-
|
|
110
|
-
単品導入のqueue配送は親の実行中turnを中断せず、親がidleになった後に処理される。
|
|
111
|
-
`parent_delivery`が配送IDと状態を返し、自動配送時の`wait_process`/`wait_command`はnullになる。
|
|
112
|
-
`pty_observe`の`parent_deliveries`で状態を確認できる。`submitted`は公式受信口の受付済みを示し、modelの読了を意味しない。
|
|
113
|
-
Steer配送の`queued_submission_id`はnullである。既存の配送record schemaは維持する。
|
|
77
|
+
Steerを選択したmacOS・Windowsでも配送の入口は公式キューとする。
|
|
78
|
+
`src/codex-parent-hooks.ts`の同期`PostToolUse`がAitermの回答を同じターンの文脈へ渡し、
|
|
79
|
+
`Stop`が最終応答の生成中に届いた回答で同じターンを継続する。
|
|
80
|
+
取り込まれていない回答は公式キューに残り、親がidleになった時に通常配送される。終了後の再開には約10秒かかる場合がある。
|
|
81
|
+
親を別processでload/resumeせず、modelや権限のoverrideを渡さない。native sub-agentは自動配送の親にしない。
|
|
82
|
+
|
|
83
|
+
`src/codex-hook-state.ts`は同じCodex home・thread・配送UUID・本文hashへの所有記録を保持する。
|
|
84
|
+
キューの全ページを読んでから所有分を取り出し、他の利用者入力には触れない。
|
|
85
|
+
同時hookの取得は本人専用ディレクトリ内の排他的hard linkで一つに決め、公式の削除結果がtrueの本文だけを出力する。
|
|
86
|
+
削除中はprocess開始識別子を記録する。中断と出力失敗は`unknown`で本文を残し、自動再送しない。
|
|
87
|
+
`parent_deliveries`はその状態と`CODEX_HOOK_DELIVERY_UNCONFIRMED`を表示する。
|
|
88
|
+
`emitted`はhookへの出力完了でありmodelの読了ではない。公式キューが先に通常配送した入力の所有記録は次のhookで整理する。
|
|
89
|
+
|
|
90
|
+
`aiterm-setup --codex-steer enable|disable|status`は`src/setup-codex-hooks.ts`が所有する。
|
|
91
|
+
`CODEX_HOME/hooks.json`の他の登録を保持し、専用の同期hookを追加する。
|
|
92
|
+
公式`hooks/list`から得た2件のkeyとhashだけを公式`config/batchWrite`で承認し、再読して有効・承認済みであることを確認する。
|
|
93
|
+
承認省略flagを恒久設定へ書かない。利用者の別hookを承認しない。
|
|
94
|
+
選択と配送の所有記録は`~/.config/aiterm-mcp/codex-parent-hooks/`に置く。
|
|
95
|
+
Nodeは`src/setup-node.ts`でHomebrewの同じformulaの`opt`へ正規化し、更新で消えるCellar実体を保存しない。
|
|
96
|
+
|
|
97
|
+
新規の起動差し替えは作らない。旧中継の設定は新しいhookの確認後に従来の解除処理で復元する。
|
|
98
|
+
macOSの専用LaunchAgent、Windowsのユーザー環境変数、所有外の値の保持は旧adapterが担う。
|
|
99
|
+
旧中継の互換読取りと移行用コードは残し、setupを実行するまでは旧設定の配送を維持する。
|
|
100
|
+
移行が中断した場合は保存済みの所有情報を使って次のsetupで継続し、未完了をreadyにしない。
|
|
101
|
+
有効化前から動いている公式processのPIDと開始識別子を保存し、それが残っている間はrestart_requiredとする。
|
|
102
|
+
`ready`は公式APIによるhook登録・承認の読戻しと、更新前processの終了を確認した状態である。
|
|
103
|
+
Codexの通常起動はAitermのNode、module、socketに依存しない。
|
|
104
|
+
Windowsでは公式Desktopが展開した実行ファイルを照合して配送用の公式APIへ接続する。Desktop更新後はsetupで再検出する。
|
|
105
|
+
LinuxのSteer選択はunsupportedとし、単品のキュー配送は全対応OSで維持する。
|
|
106
|
+
|
|
107
|
+
`parent_delivery`は配送IDと状態を返し、自動配送時の`wait_process`/`wait_command`はnullとなる。
|
|
108
|
+
`submitted`は公式キューの受付またはhook出力の完了を示す。キューIDはSteer相当の選択時も保持する。
|
|
109
|
+
長いStop継続入力はCodex自身のhook処理で抜粋と全文ファイルへの参照になる場合がある。
|
|
114
110
|
次の依頼へ進める前に前回の回答を保存し、harness所有記録を後の回答と取り違えない。
|
|
115
111
|
|
|
116
112
|
Codexの配送記録と本文はAiterm stateの`parent-deliveries`へ保存する。ownerのPIDと開始識別子で生存を判定し、
|
package/docs/RELEASE.md
CHANGED
|
@@ -35,19 +35,18 @@ Aitermのreleaseはこのrepositoryが所有する。`.github/workflows/product-
|
|
|
35
35
|
|
|
36
36
|
## 公開後smoke
|
|
37
37
|
|
|
38
|
-
Codex Steerを変更した場合は、公式バイナリを指定した`test/codex-
|
|
39
|
-
|
|
40
|
-
`
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
Linuxの未対応Steer選択が理由付きで停止することもCIで確認する。
|
|
38
|
+
Codex Steerを変更した場合は、公式バイナリを指定した`test/codex-parent-hooks-official.test.mjs`で
|
|
39
|
+
通常stdio起動、公式hookの個別承認、同一ターン配送、終了後再開、hook消失、終了との競合、利用者キューの保持を確認する。
|
|
40
|
+
`AITERM_TEST_CODEX_BINARY`に公式Desktopの同梱実行ファイルを指定し、一時HOMEと模擬モデルで実行する。
|
|
41
|
+
試験は実認証と稼働中Desktopの設定を使わない。指定のないCIではこの実機試験をskipする。
|
|
42
|
+
公開packageの`aiterm-setup --json --codex-steer enable`で選択導入し、`restart_required`ならDesktopを完全再起動する。
|
|
43
|
+
再起動後に`aiterm-setup --codex-steer status`が`ready`となり、通常の親が同じターンで子の回答を受け取ることを確認する。
|
|
44
|
+
アプリ内ツールと通常起動も確認し、公式バイナリの隔離試験だけでDesktop統合を完了扱いしない。
|
|
45
|
+
|
|
46
|
+
旧中継の移行を変更した場合は`test/setup-codex-relay.test.mjs`と`test/windows-codex.test.mjs`で
|
|
47
|
+
元の起動設定・専用LaunchAgent・ユーザー環境変数の復元と、所有外の値の保持を確認する。
|
|
48
|
+
新しいhookの読戻し失敗では旧設定を解除しないこと、解除中断では再実行で移行を完了できることも確認する。
|
|
49
|
+
新方式の登録と配送stateは`test/setup-codex-hooks.test.mjs`と`test/codex-parent-hooks.test.mjs`が検証する。
|
|
51
50
|
|
|
52
51
|
setupを変更した場合は、公開packageのglobal install後に`aiterm-setup --json`を実行し、
|
|
53
52
|
端末実行と検出した各AIの登録結果を確認する。初回と再実行は一時設定領域でも試験し、所有外の設定保持を確かめる。
|
|
@@ -76,10 +75,10 @@ aiterm-setup --json
|
|
|
76
75
|
|
|
77
76
|
巻き戻しは既知の正常版を指定する。
|
|
78
77
|
|
|
79
|
-
|
|
80
|
-
macOS
|
|
78
|
+
新しいCodex親hookを持たない旧版へ戻す時は、install前に`aiterm-setup --codex-steer disable`を実行する。
|
|
79
|
+
専用hookの解除を確認し、Codexを完全再起動する。旧中継が残っている場合は、macOSの専用LaunchAgentとWindowsのユーザー環境変数も復元する。
|
|
81
80
|
互換launcherを共有していた場合はAitermのSteer選択だけを解除し、所有外の起動設定を保持する。
|
|
82
|
-
回答record schema
|
|
81
|
+
回答record schemaは従来と共通であり、新方式のSteer相当配送も`queued_submission_id`を保持する。
|
|
83
82
|
|
|
84
83
|
Claude親配送hookのない旧版へ戻す場合は、旧版のinstall前に`aiterm-setup --remove-claude-parent-hooks`を
|
|
85
84
|
実行する。Aiterm専用の3 hookだけを解除し、他製品のhookと設定を保持する。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aiterm-mcp",
|
|
3
|
-
"version": "0.37.
|
|
3
|
+
"version": "0.37.6",
|
|
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": [
|