aiterm-mcp 0.34.0 → 0.35.1
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 +26 -1
- package/README.ja.md +15 -7
- package/README.md +17 -11
- package/dist/claude-parent-hook.js +28 -0
- package/dist/claude-parent-receiver.js +214 -0
- package/dist/core.js +15 -10
- package/dist/harnesses/claude.js +3 -1
- package/dist/harnesses/grok.js +33 -3
- package/dist/index.js +7 -5
- package/dist/parent-delivery.js +40 -14
- package/dist/setup-cli.js +15 -2
- package/dist/setup-integrations.js +107 -0
- package/docs/00_overview.md +2 -3
- package/docs/DESIGN.md +37 -1
- package/docs/RELEASE.md +7 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.35.1] - 2026-09-10
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Claudeの起動時に、指定された作業ディレクトリの実体パスを記録する。macOSの`/var`やリンク経由のcwdでも、会話記録を使うAPIエラー検出が正しい保存場所を参照する。
|
|
15
|
+
|
|
16
|
+
## [0.35.0] - 2026-09-10
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- Claude Code親への回答自動配送を追加した。公式`asyncRewake` hookが子の回答を受け取り、idle中の親も再開する。待機中も親は別作業と次のturnへ進める。
|
|
21
|
+
- `aiterm-setup`がClaude Codeの専用hookを登録し、MCP要求とhookの実際の会話を照合する。親のwaiter・回答回収と子への返送指示は不要。
|
|
22
|
+
- `/clear`等の会話終了後は未送信の旧回答を新しい会話へ出さず、本文を保持して配送失敗を明示する。hookの出力中断は結果不明とし、自動再送しない。
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- Grok/Composerで追加メッセージが直ちに次turnを開始した場合も、完了したturnの回答を指定して回収する。最新turnの切替で前の回答の自動配送が失敗する競合を修正した。
|
|
27
|
+
|
|
28
|
+
### Compatibility
|
|
29
|
+
|
|
30
|
+
- Claude Code親は2.1.259以上の対話sessionと有効なcommand hookを必要とする。Claude Desktopチャット・Web・`agent_id`付きの会話(`--agent`起動とnative subagent)は対象外。起動時のChannels flagは不要。
|
|
31
|
+
- Claude用配送記録を分け、旧版のCodex readerとの互換性を維持する。旧版へ戻す前に`aiterm-setup --remove-claude-parent-hooks`でAiterm専用hookだけを解除する。
|
|
32
|
+
|
|
10
33
|
## [0.34.0] - 2026-09-10
|
|
11
34
|
|
|
12
35
|
### Added
|
|
@@ -1617,7 +1640,9 @@ prototype (preserved under `prototype/python/` as the porting source and referen
|
|
|
1617
1640
|
`ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
|
|
1618
1641
|
provenance.
|
|
1619
1642
|
|
|
1620
|
-
[Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.
|
|
1643
|
+
[Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.35.1...HEAD
|
|
1644
|
+
[0.35.1]: https://github.com/kitepon/aiterm-mcp/compare/v0.35.0...v0.35.1
|
|
1645
|
+
[0.35.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.34.0...v0.35.0
|
|
1621
1646
|
[0.34.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.33.1...v0.34.0
|
|
1622
1647
|
[0.33.1]: https://github.com/kitepon/aiterm-mcp/compare/v0.33.0...v0.33.1
|
|
1623
1648
|
[0.33.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.32.0...v0.33.0
|
package/README.ja.md
CHANGED
|
@@ -163,7 +163,7 @@ v0.20では、待たずに一度だけ観測する
|
|
|
163
163
|
`running`(exit 5)で返すようにしました。v0.19系では相関済みClaude approval中継を追加し、
|
|
164
164
|
複数行shell配送を維持し、native Windowsのfactory diagnosticsを拡張しました。
|
|
165
165
|
v0.16/0.17以来、親エージェントはaiterm上で一切ブロックしません:
|
|
166
|
-
agent session への send は常に非ブロック dispatch になり、Codex親には回答本文を自動配送し、それ以外の親の完了待ちは `aiterm-wait`
|
|
166
|
+
agent session への send は常に非ブロック dispatch になり、Codex/Claude Code親には回答本文を自動配送し、それ以外の親の完了待ちは `aiterm-wait`
|
|
167
167
|
(exit code が receipt の outcome を映す: 0=done / 3=timeout=未完了 /
|
|
168
168
|
4=closed / 5=running=待たない観測)、初回 prompt 付き
|
|
169
169
|
launch は structured receipt にコピペ可能な `wait_command` を含む。factory diagnostics と local
|
|
@@ -171,7 +171,7 @@ runtime-error store は canonical dotagents config の `collection.enabled: true
|
|
|
171
171
|
場合だけ収集し、既定OFF、network送信は行いません。tag起点CIのnpm provenance(OIDC Trusted
|
|
172
172
|
Publishing)で公開し、GitHub Release が Official MCP Registry を再登録します。
|
|
173
173
|
|
|
174
|
-
**状態:** 開発継続中 · 現行公開版 **v0.
|
|
174
|
+
**状態:** 開発継続中 · 現行公開版 **v0.35.1** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
|
|
175
175
|
|
|
176
176
|
### 更新と巻き戻し
|
|
177
177
|
|
|
@@ -205,7 +205,7 @@ pty_read(id, { wait: true }) → 削減済みの出力を読む(完了
|
|
|
205
205
|
|
|
206
206
|
同じprimitiveが別エージェントのTUIを宿す。`agent_launch`の`harness`はagent loop・認証・hook・session・transcriptを所有する実行基盤、`model`は独立した選択。起動processは直接CLIと同じproject/user環境を使い、通常config、MCP、plugin、skill、permission、trust、memory、historyをcopy・filter・置換しない。
|
|
207
207
|
|
|
208
|
-
`aiterm.agent-launch-result.v1`は正規`harness`を返し、旧`provider`は互換fieldとして残す。同じ`harness`はagent dispatch、`aiterm-wait`、`agent_configure`、`pty_list`のagent行にも載り、旧vendor/provider/agent fieldは互換用に残る。Codexは通常rollout、Grok CLIは通常session event、Claudeはlaunch固有Stop hook、Cursorは通常agent transcript末尾の`turn_ended`を完了正本に使う。`pty_send`は非ブロックdispatchで、vendor別完了境界を表すopaqueな整数`event_cursor
|
|
208
|
+
`aiterm.agent-launch-result.v1`は正規`harness`を返し、旧`provider`は互換fieldとして残す。同じ`harness`はagent dispatch、`aiterm-wait`、`agent_configure`、`pty_list`のagent行にも載り、旧vendor/provider/agent fieldは互換用に残る。Codexは通常rollout、Grok CLIは通常session event、Claudeはlaunch固有Stop hook、Cursorは通常agent transcript末尾の`turn_ended`を完了正本に使う。`pty_send`は非ブロックdispatchで、vendor別完了境界を表すopaqueな整数`event_cursor`を返す。Codex親は公式受信キュー、Claude Code親は公式非同期hookで回答本文を自動受信する。それ以外の親は`aiterm-wait`を親のターンを塞がない別processで受ける。Cursorのsubmitキーはadapterが現行CLIのextended keyboard protocolへ変換する。送信textがCursorのcomposerへ残った場合は成功receiptを返さず失敗する。
|
|
209
209
|
|
|
210
210
|
`agent_launch`・`pty_send`(agent dispatch)・`agent_steer`は任意の`image`(画像ファイルの絶対パスの配列。png/jpg/jpeg/gif/webp)を受ける。aitermが本文末尾へ添付行を付け、どのharnessも自分のfile読取toolでそのpathを画像として開く。呼出し側はharness別の添付手順を覚えない。不正なpathは送信前に拒否する。
|
|
211
211
|
|
|
@@ -225,7 +225,7 @@ agent_launch({ harness: "codex-cli", session_name: "codex1", cwd: "/repo",
|
|
|
225
225
|
→ { session_id: "codex1", … } # Codex が永続端末で稼働開始
|
|
226
226
|
pty_read("codex1", { screen: true }) → 何をしているか読む(トークン削減)
|
|
227
227
|
pty_send("codex1", "also fix the imports it broke") # 非ブロックdispatch=event_cursor入りreceipt
|
|
228
|
-
# Codex親には回答が自動で届く。それ以外の親:
|
|
228
|
+
# Codex/Claude Code親には回答が自動で届く。それ以外の親:
|
|
229
229
|
$ aiterm-wait --session codex1 --cursor <event_cursor> # exit 0=done / 3=timeout(未完了) / 4=closed / 7=error(APIエラー等でturn打ち切り)。回収は pty_read(agent_transcript:true)
|
|
230
230
|
→ 操舵し、Codex の次の入力境界で返る
|
|
231
231
|
```
|
|
@@ -514,14 +514,22 @@ SSH先がPowerShellの場合、`mark:true`は現在の標準`PS ...>`プロン
|
|
|
514
514
|
|
|
515
515
|
`pty_read({ wait: true })`は通常PTYを、process終了/`mark:true` sentinel/`until`一致/shell復帰を伴う出力静止/timeoutの5層で判定する。agent sessionは第6の正確な層を使い、Codexは通常rollout、Grokは通常session event、Claudeはlaunch相関Stop event、Cursorは通常agent transcriptの`turn_ended`を`aiterm-wait --cursor`が観測する。親はブロックもポーリングもしない。
|
|
516
516
|
|
|
517
|
-
### Codex親への回答自動配送
|
|
517
|
+
### Codex/Claude Code親への回答自動配送
|
|
518
518
|
|
|
519
|
-
Codexから子を起動・通常dispatchした後は、別作業へ進むか親のturnを終了するだけでよい。Aiterm
|
|
519
|
+
Codex/Claude Codeから子を起動・通常dispatchした後は、別作業へ進むか親のturnを終了するだけでよい。Aitermが完了を観測し、加工前の回答を保存して親へ届ける。waiter、`pty_read`による回答回収、子への送信指示は不要。子は全対応harnessから選べる。
|
|
520
520
|
|
|
521
|
-
自動配送時はreceiptに`parent_delivery`が付き、`wait_process`/`wait_command`はnullになる。`pty_observe`の`parent_deliveries`で`waiting`、`ready`、`sending`、`submitted`、`failed`、`unknown`を確認できる。`submitted
|
|
521
|
+
自動配送時は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`とする。自動再送はしない。
|
|
522
522
|
|
|
523
523
|
CodexにはMCPの`_meta.threadId`と公式`thread/queue` APIが必要で、Codex CLI 0.154.0で確認している。`aiterm-setup`はインストールされた公式queue入口を確認し、各dispatchでは実際の親threadの受入可否を確認する。Codexのnative sub-agentは外部からのqueue入力を拒否するため、自動配送の親としては未対応。
|
|
524
524
|
|
|
525
|
+
Claude Codeは2.1.259以上の対話sessionに対応する。`aiterm-setup`が専用の`PreToolUse`、`PostToolUse`、`SessionEnd`を登録するため、Channelsの起動flagは不要。公式`asyncRewake` hookだけが裏で待ち、親はその間も次のturnへ進める。回答は`Stop hook feedback`として届く。hookのexit 2は親の再開信号であり、子の成功・失敗は本文の`outcome`で区別する。
|
|
526
|
+
|
|
527
|
+
`/clear`などで会話を終了すると未送信の旧回答の配送を止め、本文は保存する。受信hookの上限は24時間で、終了や出力失敗を成功扱いしない。hookが無効な場合は送信前に明示errorにし、waiterへ黙って切り替えない。Claude Desktopのチャット、Web、`agent_id`付きの会話(`--agent`起動とnative subagent)はこの受信契約に含めない。
|
|
528
|
+
|
|
529
|
+
hookを持たない旧版へ戻す時は、install前に`aiterm-setup --remove-claude-parent-hooks`を実行する。Aiterm専用hookだけを解除し、他製品のhookと設定は保持する。
|
|
530
|
+
|
|
531
|
+
Claudeをリンク経由の`cwd`から起動した場合も、実体パスに対応する会話記録を参照する。
|
|
532
|
+
|
|
525
533
|
|
|
526
534
|
### トークン削減
|
|
527
535
|
|
package/README.md
CHANGED
|
@@ -176,18 +176,16 @@ a non-blocking `aiterm-wait --timeout 0` observation (`running`, exit 5) from a
|
|
|
176
176
|
wait. The v0.19 line added the correlated Claude approval relay,
|
|
177
177
|
preserved multiline shell delivery, and extended factory diagnostics on native
|
|
178
178
|
Windows. As of v0.16/0.17 a parent agent never blocks on aiterm:
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
zero-time observation), and a launch with an
|
|
183
|
-
initial prompt returns a ready-made `wait_command` in its structured receipt.
|
|
179
|
+
agent sessionへの送信は非ブロックdispatchであり、Codex/Claude Code親には回答本文を自動配送する。
|
|
180
|
+
それ以外の親はreceiptのprocess起動情報で`aiterm-wait`を実行する。
|
|
181
|
+
終了コードは`0`=done、`3`=timeout、`4`=closed、待機しない照会の`5`=runningを表す。
|
|
184
182
|
Factory diagnostics and the local runtime-error store collect only when
|
|
185
183
|
canonical dotagents config explicitly sets `collection.enabled: true`;
|
|
186
184
|
collection is off by default and performs no network I/O. It ships via
|
|
187
185
|
tag-triggered CI with npm provenance (OIDC Trusted Publishing); the GitHub
|
|
188
186
|
Release re-registers the Official MCP Registry entry.
|
|
189
187
|
|
|
190
|
-
**Status:** actively maintained · current public release **v0.
|
|
188
|
+
**Status:** actively maintained · current public release **v0.35.1** · 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).
|
|
191
189
|
|
|
192
190
|
### Update and rollback
|
|
193
191
|
|
|
@@ -227,7 +225,7 @@ pty_read(id, { wait: true }) → read the token-reduced output, completion
|
|
|
227
225
|
|
|
228
226
|
The same primitive hosts another agent's TUI. `agent_launch` starts a selected execution harness inside a fresh persistent terminal and returns a `session_id`. `harness` names the component that owns the agent loop, authentication, hooks, session, and transcript; `model` remains an independent choice. The launched process sees the same project and user environment as a direct CLI invocation: normal configuration, MCPs, plugins, skills, permissions, trust decisions, memory, and history are not copied, filtered, or replaced. Aiterm adds only completion correlation and a non-user sub-agent context containing `role=subagent`, the parent session, delegation depth, lineage, and `delegation_allowed=true`.
|
|
229
227
|
|
|
230
|
-
|
|
228
|
+
起動結果には正規`harness`を含む`aiterm.agent-launch-result.v1`が付き、旧`provider`は互換fieldとして残る。同じ`harness`はagent dispatch、`aiterm-wait`、`agent_configure`、`pty_list`にも載る。Codexは通常rollout、Grokは通常session event、Claudeはlaunch固有Stop hook、Cursorは通常agent transcriptの`turn_ended`を完了正本に使う。agentへの送信は非ブロックdispatchで、harnessごとの完了境界を表す整数`event_cursor`を返す。Codex親は公式queue、Claude Code親は公式非同期hookで本文を自動受信する。他の親は[`aiterm-wait`](#completion-push-for-parent-agents-aiterm-wait)を使う。CursorのsubmitはadapterがCLIのextended keyboard protocolへ変換し、送信本文がcomposerへ残る場合は明示errorにする。
|
|
231
229
|
|
|
232
230
|
`agent_launch`, `pty_send` (agent dispatch), and `agent_steer` accept an optional `image`: an array of absolute paths to image files (png/jpg/jpeg/gif/webp). Aiterm appends an attachment block to the prompt, and every harness opens the path with its own file-reading tool and sees the image; the caller never learns harness-specific attachment tricks. Invalid paths are rejected before anything is sent.
|
|
233
231
|
|
|
@@ -250,7 +248,7 @@ agent_launch({ harness: "codex-cli", session_name: "codex1", cwd: "/repo",
|
|
|
250
248
|
pty_read("codex1", { screen: true }) → read what it's doing (token-reduced)
|
|
251
249
|
pty_send("codex1", "also fix the imports it broke")
|
|
252
250
|
→ non-blocking dispatch; receipt carries event_cursor
|
|
253
|
-
# Codex
|
|
251
|
+
# Codex/Claude Code親には回答が自動で届く。それ以外の親:
|
|
254
252
|
$ aiterm-wait --session codex1 --cursor <event_cursor> # never in the parent's foreground; exit 0=done, 3=timeout (not done), 4=closed, 7=error (turn aborted by an API error)
|
|
255
253
|
pty_read("codex1", { agent_transcript: true }) → collect the full answer
|
|
256
254
|
```
|
|
@@ -551,17 +549,25 @@ For PowerShell over SSH, `mark:true` recognizes the current standard `PS ...>` p
|
|
|
551
549
|
|
|
552
550
|
### Completion push for parent agents (`aiterm-wait`)
|
|
553
551
|
|
|
554
|
-
**Codex
|
|
552
|
+
**Codex/Claude Code親には子の回答本文が自動で届く。** 子を起動・dispatchした後は、別作業へ進むか親のturnを終える。Aitermが完了を観測し、加工前の本文を保存して親へ渡す。waiter、`pty_read`による回答回収、子への返送指示は不要。子は全対応harnessから選べる。
|
|
555
553
|
|
|
556
|
-
|
|
554
|
+
自動配送の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`とする。自動再送はしない。
|
|
557
555
|
|
|
558
556
|
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. Ordinary CLI and Desktop parents use the same supported route.
|
|
559
557
|
|
|
558
|
+
Claude Codeは2.1.259以上の対話sessionに対応する。`aiterm-setup`が専用の`PreToolUse`、`PostToolUse`、`SessionEnd`を登録するため、Channelsの起動flagは不要。公式`asyncRewake` hookだけが裏で待ち、親はその間も次のturnへ進める。回答は`Stop hook feedback`として届く。hookのexit 2は親の再開信号であり、子の成功・失敗は本文の`outcome`で区別する。
|
|
559
|
+
|
|
560
|
+
`/clear`等の会話終了後は未送信の旧回答を送らず、本文を保存する。受信hookの上限は24時間。hookの終了・出力失敗・無効化を成功扱いせず、別の待機経路へ黙って切り替えない。Claude Desktopのチャット、Web、`agent_id`付きの会話(`--agent`起動とnative subagent)はこの受信契約に含めない。
|
|
561
|
+
|
|
562
|
+
hookを持たない旧版へ戻す時は、install前に`aiterm-setup --remove-claude-parent-hooks`を実行する。Aiterm専用hookだけを解除し、他製品のhookと設定は保持する。
|
|
563
|
+
|
|
564
|
+
Claudeをリンク経由の`cwd`から起動した場合も、実体パスに対応する会話記録を参照する。
|
|
565
|
+
|
|
560
566
|
**For other parent hosts**, dispatch and start the receipt's waiter in a separate process:
|
|
561
567
|
|
|
562
568
|
1. Launch the child with `agent_launch({ harness: ... })`; every launch shares the normal project/user environment and adds only completion correlation plus lineage. Send a turn with plain `pty_send` (or `claude_turn issue` for durable Claude operations). The call returns immediately with an `event_cursor` in its structured receipt.
|
|
563
569
|
2. Pass the receipt's `wait_process.executable` and `wait_process.args` unchanged to a true argv process API. PowerShell 7's `Start-Process` is the exception because it joins `-ArgumentList` arrays; pass `windows_start_process_argument_list` as its one ready-made argument string instead. This invokes the bundled waiter through the exact Node runtime that is already running aiterm, including on native Windows where npm's human-facing bin is a PowerShell script shim and install paths may contain spaces. `wait_command` remains a compatibility display string for humans. The waiter observes the harness-owned completion source, plus Claude's additive launch hook, as a **pure reader** and exits with a one-line `aiterm.agent-wait-result.v1` receipt. **Exit ≠ done**: the receipt's `outcome` is authoritative (`0` = `done`, `3` = `timeout`, `4` = `closed`, `1` = error).
|
|
564
|
-
3.
|
|
570
|
+
3. **親自身のforegroundでwaiterを実行しない。** receiptのprocess起動情報を、そのhostが持つバックグラウンドprocess APIへ渡す。親は別作業へ進むかturnを終え、process終了の通知で続行する。
|
|
565
571
|
4. Collect the result exactly as before: `pty_read(agent_transcript: true)`, or `claude_turn recover` for durable Claude operations. The waiter carries the signal, never the payload.
|
|
566
572
|
|
|
567
573
|
**If your host has no completion push** (no mechanism that re-invokes the agent when a background process exits), `--timeout 0` is a one-shot check instead of a wait: it scans the event file once and returns `running` (exit `5`) when the turn is still in flight, `done` (exit `0`) when it finished, `closed` (exit `4`) when the session is gone. It is deliberately absent from the receipts and tool descriptions — a host that *does* get pushed should be woken, not poll. An unknown session name is an error, never `running`, so a typo cannot masquerade as a child that is still working.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Claude Codeが直接起動する公式hook。MCP stdioとは別processで本文をstderrへ返す。
|
|
3
|
+
import { prepareClaudeHookRequest, runClaudeResultHook, closeClaudeParentSession } from "./claude-parent-receiver.js";
|
|
4
|
+
async function main() {
|
|
5
|
+
let input = "";
|
|
6
|
+
process.stdin.setEncoding("utf8");
|
|
7
|
+
for await (const chunk of process.stdin)
|
|
8
|
+
input += chunk;
|
|
9
|
+
const event = JSON.parse(input);
|
|
10
|
+
switch (event.hook_event_name) {
|
|
11
|
+
case "PreToolUse":
|
|
12
|
+
prepareClaudeHookRequest(event);
|
|
13
|
+
break;
|
|
14
|
+
case "PostToolUse":
|
|
15
|
+
process.exitCode = await runClaudeResultHook(event, text => new Promise((resolve, reject) => {
|
|
16
|
+
process.stderr.write(text, error => error ? reject(error) : resolve());
|
|
17
|
+
}));
|
|
18
|
+
break;
|
|
19
|
+
case "SessionEnd":
|
|
20
|
+
closeClaudeParentSession(event);
|
|
21
|
+
break;
|
|
22
|
+
default: throw new Error("CLAUDE_PARENT_HOOK_EVENT_INVALID: 未対応のhookです");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
main().catch(error => {
|
|
26
|
+
process.stderr.write(`${error instanceof Error ? error.message : "CLAUDE_PARENT_HOOK_FAILED"}\n`);
|
|
27
|
+
process.exitCode = 2;
|
|
28
|
+
});
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// Claude Codeの公式hookを受信口にする。待機processはharnessが所有し、親のturnを止めない。
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { ensureStateRoot, writeJson0600 } from "./agent-shared.js";
|
|
6
|
+
import { readRuntimeProcesses } from "./process-runtime.js";
|
|
7
|
+
import { AitermError } from "./errors.js";
|
|
8
|
+
const requestId = z.string().regex(/^[A-Za-z0-9_-]{1,160}$/);
|
|
9
|
+
const invocationSchema = z.object({
|
|
10
|
+
request_id: requestId, session_id: z.uuid(), agent_id: z.string().nullable(),
|
|
11
|
+
parent_pid: z.number().int().positive(), parent_started_identity: z.string(),
|
|
12
|
+
}).strict();
|
|
13
|
+
export const claudeParentSchema = z.object({
|
|
14
|
+
kind: z.literal("claude"), request_id: requestId, session_id: z.uuid(), hook_root: z.string(),
|
|
15
|
+
}).strict();
|
|
16
|
+
export class ClaudeDeliveryError extends AitermError {
|
|
17
|
+
delivery_code;
|
|
18
|
+
outcome_unknown;
|
|
19
|
+
constructor(delivery_code, message, outcome_unknown = false) {
|
|
20
|
+
super(`${delivery_code}: ${message}`, 2);
|
|
21
|
+
this.delivery_code = delivery_code;
|
|
22
|
+
this.outcome_unknown = outcome_unknown;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function defaultRoot() { return path.join(ensureStateRoot(), "claude-parent-hooks"); }
|
|
26
|
+
function processIdentity(pid) { return readRuntimeProcesses().find(entry => entry.pid === pid)?.started_identity; }
|
|
27
|
+
function directory(parent) { return path.join(parent.hook_root, parent.request_id); }
|
|
28
|
+
function readInvocation(parent) {
|
|
29
|
+
let value;
|
|
30
|
+
try {
|
|
31
|
+
value = invocationSchema.parse(JSON.parse(fs.readFileSync(path.join(directory(parent), "request.json"), "utf8")));
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
throw new ClaudeDeliveryError("CLAUDE_PARENT_HOOK_UNAVAILABLE", "親のhook記録がありません。aiterm-setupを実行し、Claude Codeのhookを有効にしてください");
|
|
35
|
+
}
|
|
36
|
+
if (value.request_id !== parent.request_id || value.session_id !== parent.session_id) {
|
|
37
|
+
throw new ClaudeDeliveryError("CLAUDE_PARENT_SESSION_MISMATCH", "要求とhookの会話が一致しません");
|
|
38
|
+
}
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
function assertOpen(parent) {
|
|
42
|
+
if (fs.existsSync(path.join(directory(parent), "closed.json"))) {
|
|
43
|
+
throw new ClaudeDeliveryError("CLAUDE_PARENT_SESSION_CLOSED", "依頼元の会話は終了しました。回答を別の会話へ送っていません");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function prepareClaudeHookRequest(input, root = defaultRoot()) {
|
|
47
|
+
const event = z.object({ session_id: z.uuid(), tool_use_id: requestId, agent_id: z.string().optional() }).parse(input);
|
|
48
|
+
const identity = processIdentity(process.ppid);
|
|
49
|
+
if (!identity)
|
|
50
|
+
throw new ClaudeDeliveryError("CLAUDE_PARENT_PROCESS_UNAVAILABLE", "hookを起動した親processを確認できません");
|
|
51
|
+
const dir = path.join(root, event.tool_use_id);
|
|
52
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
53
|
+
writeJson0600(path.join(dir, "request.json"), {
|
|
54
|
+
request_id: event.tool_use_id, session_id: event.session_id, agent_id: event.agent_id ?? null,
|
|
55
|
+
parent_pid: process.ppid, parent_started_identity: identity,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/** 起動時envのsession IDは/clearで古くなるため、実際のPreToolUseとの相関だけを使う。 */
|
|
59
|
+
export function claudeParentFromRequest(clientName, metadata, root = defaultRoot()) {
|
|
60
|
+
if (clientName !== "claude-code")
|
|
61
|
+
return null;
|
|
62
|
+
const parsed = requestId.safeParse(metadata?.["claudecode/toolUseId"]);
|
|
63
|
+
if (!parsed.success)
|
|
64
|
+
throw new ClaudeDeliveryError("CLAUDE_PARENT_ID_UNAVAILABLE", "MCP要求にtoolUseIdがありません。対応するClaude Codeへ更新してください");
|
|
65
|
+
let invocation;
|
|
66
|
+
try {
|
|
67
|
+
invocation = invocationSchema.parse(JSON.parse(fs.readFileSync(path.join(root, parsed.data, "request.json"), "utf8")));
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
throw new ClaudeDeliveryError("CLAUDE_PARENT_HOOK_UNAVAILABLE", "親のPreToolUse hookを確認できません。aiterm-setupを実行し、hookを有効にしてください");
|
|
71
|
+
}
|
|
72
|
+
const parent = { kind: "claude", request_id: parsed.data, session_id: invocation.session_id, hook_root: root };
|
|
73
|
+
verifyClaudeParent(parent);
|
|
74
|
+
return parent;
|
|
75
|
+
}
|
|
76
|
+
export function verifyClaudeParent(parent) {
|
|
77
|
+
const invocation = readInvocation(parent);
|
|
78
|
+
if (invocation.agent_id)
|
|
79
|
+
throw new ClaudeDeliveryError("CLAUDE_PARENT_SUBAGENT_UNSUPPORTED", "agent_id付きのClaude会話(--agent起動またはnative subagent)への自動配送には対応していません");
|
|
80
|
+
assertOpen(parent);
|
|
81
|
+
}
|
|
82
|
+
export function bindClaudeParentDelivery(parent, deliveryId) {
|
|
83
|
+
verifyClaudeParent(parent);
|
|
84
|
+
writeJson0600(path.join(directory(parent), "delivery.json"), { delivery_id: z.uuid().parse(deliveryId) });
|
|
85
|
+
}
|
|
86
|
+
/** SessionEndはその時点の依頼だけを終了する。同じ会話をresumeした新規依頼は別requestになる。 */
|
|
87
|
+
export function closeClaudeParentSession(input, root = defaultRoot()) {
|
|
88
|
+
const { session_id } = z.object({ session_id: z.uuid() }).parse(input);
|
|
89
|
+
if (!fs.existsSync(root))
|
|
90
|
+
return;
|
|
91
|
+
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
92
|
+
if (!entry.isDirectory())
|
|
93
|
+
continue;
|
|
94
|
+
const file = path.join(root, entry.name, "request.json");
|
|
95
|
+
if (!fs.existsSync(file))
|
|
96
|
+
continue;
|
|
97
|
+
const invocation = invocationSchema.parse(JSON.parse(fs.readFileSync(file, "utf8")));
|
|
98
|
+
if (invocation.session_id === session_id)
|
|
99
|
+
writeJson0600(path.join(root, entry.name, "closed.json"), { session_id });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// filesystemの通知を先に登録してから状態を読む。producerの終了は低頻度のprocess照合でも検出する。
|
|
103
|
+
function waitForFileState(dir, inspect) {
|
|
104
|
+
return new Promise((resolve, reject) => {
|
|
105
|
+
let finished = false;
|
|
106
|
+
let timer;
|
|
107
|
+
const watcher = fs.watch(dir, () => check());
|
|
108
|
+
const finish = (error, value) => {
|
|
109
|
+
if (finished)
|
|
110
|
+
return;
|
|
111
|
+
finished = true;
|
|
112
|
+
watcher.close();
|
|
113
|
+
if (timer)
|
|
114
|
+
clearInterval(timer);
|
|
115
|
+
if (error)
|
|
116
|
+
reject(error);
|
|
117
|
+
else
|
|
118
|
+
resolve(value);
|
|
119
|
+
};
|
|
120
|
+
const check = () => {
|
|
121
|
+
if (finished)
|
|
122
|
+
return;
|
|
123
|
+
try {
|
|
124
|
+
const value = inspect();
|
|
125
|
+
if (value !== undefined)
|
|
126
|
+
finish(null, value);
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
finish(error);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
watcher.on("error", error => finish(error));
|
|
133
|
+
timer = setInterval(check, 5000);
|
|
134
|
+
check();
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
function assertParentAlive(invocation) {
|
|
138
|
+
if (processIdentity(invocation.parent_pid) !== invocation.parent_started_identity) {
|
|
139
|
+
throw new ClaudeDeliveryError("CLAUDE_PARENT_PROCESS_CLOSED", "依頼元のClaude processは終了しました。回答は保存したままです");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
export async function submitClaudeParentAnswer(parent, deliveryId, text) {
|
|
143
|
+
const invocation = readInvocation(parent);
|
|
144
|
+
const dir = directory(parent);
|
|
145
|
+
// 会話終了でも確定本文を失わない。終了判定より先に保存する。
|
|
146
|
+
writeJson0600(path.join(dir, "answer.json"), { delivery_id: deliveryId, text });
|
|
147
|
+
await waitForFileState(dir, () => {
|
|
148
|
+
const emitted = path.join(dir, "emitted.json");
|
|
149
|
+
if (fs.existsSync(emitted)) {
|
|
150
|
+
const value = JSON.parse(fs.readFileSync(emitted, "utf8"));
|
|
151
|
+
if (value.delivery_id !== deliveryId)
|
|
152
|
+
throw new ClaudeDeliveryError("CLAUDE_PARENT_DELIVERY_MISMATCH", "hookの配送IDが一致しません", true);
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
assertOpen(parent);
|
|
156
|
+
const failed = path.join(dir, "failed.json");
|
|
157
|
+
if (fs.existsSync(failed)) {
|
|
158
|
+
const value = JSON.parse(fs.readFileSync(failed, "utf8"));
|
|
159
|
+
throw new ClaudeDeliveryError("CLAUDE_PARENT_HOOK_FAILED", "親へのhook出力が失敗しました。回答は保存したままです", value.outcome_unknown === true);
|
|
160
|
+
}
|
|
161
|
+
const hookFile = path.join(dir, "hook.json");
|
|
162
|
+
if (fs.existsSync(hookFile)) {
|
|
163
|
+
const hook = z.object({ pid: z.number().int().positive(), started_identity: z.string() }).parse(JSON.parse(fs.readFileSync(hookFile, "utf8")));
|
|
164
|
+
if (processIdentity(hook.pid) !== hook.started_identity) {
|
|
165
|
+
throw new ClaudeDeliveryError("CLAUDE_PARENT_HOOK_CLOSED", "親の受信hookが終了しました。自動再送はしていません", fs.existsSync(path.join(dir, "sending.json")));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
assertParentAlive(invocation);
|
|
169
|
+
return undefined;
|
|
170
|
+
});
|
|
171
|
+
return { queued_submission_id: null };
|
|
172
|
+
}
|
|
173
|
+
export async function runClaudeResultHook(input, emit, root = defaultRoot()) {
|
|
174
|
+
const event = z.object({ session_id: z.uuid(), tool_use_id: requestId }).parse(input);
|
|
175
|
+
const parent = { kind: "claude", request_id: event.tool_use_id, session_id: event.session_id, hook_root: root };
|
|
176
|
+
const invocation = readInvocation(parent);
|
|
177
|
+
const dir = directory(parent);
|
|
178
|
+
const binding = path.join(dir, "delivery.json");
|
|
179
|
+
if (!fs.existsSync(binding)) {
|
|
180
|
+
fs.rmSync(dir, { recursive: true });
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
183
|
+
const deliveryId = z.uuid().parse(JSON.parse(fs.readFileSync(binding, "utf8")).delivery_id);
|
|
184
|
+
const identity = processIdentity(process.pid);
|
|
185
|
+
if (!identity)
|
|
186
|
+
throw new ClaudeDeliveryError("CLAUDE_PARENT_HOOK_UNAVAILABLE", "受信hookのprocessを確認できません");
|
|
187
|
+
writeJson0600(path.join(dir, "hook.json"), { pid: process.pid, started_identity: identity });
|
|
188
|
+
try {
|
|
189
|
+
const answer = await waitForFileState(dir, () => {
|
|
190
|
+
if (fs.existsSync(path.join(dir, "closed.json")))
|
|
191
|
+
return null;
|
|
192
|
+
assertParentAlive(invocation);
|
|
193
|
+
const file = path.join(dir, "answer.json");
|
|
194
|
+
if (!fs.existsSync(file))
|
|
195
|
+
return undefined;
|
|
196
|
+
const value = z.object({ delivery_id: z.uuid(), text: z.string() }).strict().parse(JSON.parse(fs.readFileSync(file, "utf8")));
|
|
197
|
+
if (value.delivery_id !== deliveryId)
|
|
198
|
+
throw new ClaudeDeliveryError("CLAUDE_PARENT_DELIVERY_MISMATCH", "保存された回答の配送IDが一致しません");
|
|
199
|
+
return value;
|
|
200
|
+
});
|
|
201
|
+
if (!answer)
|
|
202
|
+
return 0;
|
|
203
|
+
assertOpen(parent);
|
|
204
|
+
writeJson0600(path.join(dir, "sending.json"), { delivery_id: deliveryId });
|
|
205
|
+
await emit(answer.text);
|
|
206
|
+
writeJson0600(path.join(dir, "emitted.json"), { delivery_id: deliveryId });
|
|
207
|
+
// Claudeが定めるasyncRewakeの再開信号。子の成功/失敗は本文のoutcomeで区別する。
|
|
208
|
+
return 2;
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
writeJson0600(path.join(dir, "failed.json"), { outcome_unknown: fs.existsSync(path.join(dir, "sending.json")) });
|
|
212
|
+
throw error;
|
|
213
|
+
}
|
|
214
|
+
}
|
package/dist/core.js
CHANGED
|
@@ -2410,7 +2410,8 @@ export async function readAgentTranscriptResult(name, o = {}) {
|
|
|
2410
2410
|
throw new AitermError(`agent session '${name}' はまだターンが完了していません。agent_done 完了後に再取得してください。${agentWaitGuide(name)}`, 2);
|
|
2411
2411
|
}
|
|
2412
2412
|
const done = latestAgentDoneEvent(meta, operationId);
|
|
2413
|
-
if (o.completion && meta.kind !== "codex" &&
|
|
2413
|
+
if (o.completion && meta.kind !== "codex" && meta.kind !== "grok" && meta.kind !== "composer"
|
|
2414
|
+
&& (!done || done.turn_id !== o.completion.turn_id || done.operation_id !== o.completion.operation_id)) {
|
|
2414
2415
|
throw new AitermError("回収対象の完了情報が置換されました。別の回答は配送しません", 2);
|
|
2415
2416
|
}
|
|
2416
2417
|
if (operationId && !done) {
|
|
@@ -2430,9 +2431,9 @@ export async function readAgentTranscriptResult(name, o = {}) {
|
|
|
2430
2431
|
text = codexTranscriptText(meta, turnId, readTranscriptLines, transcriptUnavailable, o.completion !== undefined);
|
|
2431
2432
|
}
|
|
2432
2433
|
else {
|
|
2433
|
-
if (!done)
|
|
2434
|
+
if (!done && !o.completion)
|
|
2434
2435
|
transcriptUnavailable();
|
|
2435
|
-
text = grokTranscriptText(meta, readTranscriptLines, transcriptUnavailable);
|
|
2436
|
+
text = grokTranscriptText(meta, readTranscriptLines, transcriptUnavailable, o.completion?.turn_id);
|
|
2436
2437
|
}
|
|
2437
2438
|
if (!text.trim())
|
|
2438
2439
|
transcriptNotFound(meta.kind);
|
|
@@ -2557,9 +2558,11 @@ export function agentWaitProcess(session, cursor, runtime = {}) {
|
|
|
2557
2558
|
: null,
|
|
2558
2559
|
};
|
|
2559
2560
|
}
|
|
2560
|
-
// 親ホストの識別(MCP initialize の clientInfo.name
|
|
2561
|
-
// 起動形」で名指しするためだけに使う。分からない時は汎用文へ落ち、機能は一切変えない。
|
|
2561
|
+
// 親ホストの識別(MCP initialize の clientInfo.name)。配送の可否はMCP入口が検証し、ここは案内だけを作る。
|
|
2562
2562
|
let parentClientName = null;
|
|
2563
|
+
function autoDeliveryParent() {
|
|
2564
|
+
return parentClientName === "codex-mcp-client" ? "Codex" : parentClientName === "claude-code" ? "Claude Code" : null;
|
|
2565
|
+
}
|
|
2563
2566
|
export function setParentClient(name) {
|
|
2564
2567
|
const trimmed = typeof name === "string" ? name.trim() : "";
|
|
2565
2568
|
parentClientName = trimmed === "" ? null : trimmed;
|
|
@@ -2574,8 +2577,9 @@ export function agentWaitLaunchForm(command) {
|
|
|
2574
2577
|
}
|
|
2575
2578
|
// dispatch / 起動時 prompt 送信後の共通案内。第一文で「待たない」を宣言し、待ち方は後段に置く。
|
|
2576
2579
|
export function agentDispatchGuide(session, cursor) {
|
|
2577
|
-
|
|
2578
|
-
|
|
2580
|
+
const parent = autoDeliveryParent();
|
|
2581
|
+
if (parent) {
|
|
2582
|
+
return `回答本文はAitermがこの${parent}親へ自動配送する。wait起動・ポーリング・通常の回答回収は不要。` +
|
|
2579
2583
|
"親は作業を続けるかターンを終え、順番待ちから届く子の回答で続行する。";
|
|
2580
2584
|
}
|
|
2581
2585
|
const cmd = `aiterm-wait --session ${session} --cursor ${cursor}`;
|
|
@@ -2585,8 +2589,9 @@ export function agentDispatchGuide(session, cursor) {
|
|
|
2585
2589
|
}
|
|
2586
2590
|
// 未完了 session へ触った時の共通案内。ここでも待つのは waiter プロセスであって親ではない。
|
|
2587
2591
|
export function agentWaitGuide(session) {
|
|
2588
|
-
|
|
2589
|
-
|
|
2592
|
+
const parent = autoDeliveryParent();
|
|
2593
|
+
if (parent)
|
|
2594
|
+
return `回答本文はこの${parent}親へ自動配送される。親は作業を続けるかターンを終える。`;
|
|
2590
2595
|
const cmd = `aiterm-wait --session ${session ?? "<session_id>"} --cursor 0`;
|
|
2591
2596
|
return `完了通知は ${agentWaitLaunchForm(cmd)} で受ける(親はここで待たない・polling 不要)。receipt の outcome=done を確認してから再取得する。`;
|
|
2592
2597
|
}
|
|
@@ -3892,7 +3897,7 @@ export function openAgent(kind, opts = {}) {
|
|
|
3892
3897
|
}
|
|
3893
3898
|
const driveHint = agentDone
|
|
3894
3899
|
? `TUI の描画には数秒かかる。少し置いてから pty_read(${sid}, screen:true) で画面を読み、` +
|
|
3895
|
-
`turnはpty_send(${sid}, "...")で送る(自動で非ブロックdispatch。${
|
|
3900
|
+
`turnはpty_send(${sid}, "...")で送る(自動で非ブロックdispatch。${autoDeliveryParent() ? `回答本文は${autoDeliveryParent()}親へ自動配送する` : "完了通知はaiterm-wait"})。中断はpty_key(${sid}, "C-c")、` +
|
|
3896
3901
|
`Stopが来ない場合の解除はpty_close(${sid})を使う。`
|
|
3897
3902
|
: `TUI の描画には数秒かかる。少し置いてから pty_read(${sid}, screen:true) で画面を読み、` +
|
|
3898
3903
|
`pty_send(${sid}, "...") で入力・pty_key(${sid}, "Enter"/"Up"/"C-c" 等) で操作する(対話)。`;
|
package/dist/harnesses/claude.js
CHANGED
|
@@ -224,6 +224,8 @@ export function claudeStartupAction(screen, trustProject) {
|
|
|
224
224
|
// submit座礁観測のcomposer領域マーカー(ready判定と同じ記号を行頭基準で探す)。
|
|
225
225
|
export const CLAUDE_COMPOSER_MARKER_RE = /^\s*❯/;
|
|
226
226
|
export function createClaudeAgentMetadata(name, cwd, initialPrompt, launchOperationId, launchRequestDigest, lineageContext, model, effort) {
|
|
227
|
+
// Claude Codeはリンクを解決したcwdでproject slugを作る。起動時に固定し、記録の監視時に再解決しない。
|
|
228
|
+
const transcriptCwd = cwd === null ? null : fs.realpathSync(cwd);
|
|
227
229
|
const launchId = randomBytes(16).toString("hex");
|
|
228
230
|
const eventFile = agentEventPath(name, launchId);
|
|
229
231
|
const resultFile = agentClaudeResultPath(name, launchId);
|
|
@@ -236,7 +238,7 @@ export function createClaudeAgentMetadata(name, cwd, initialPrompt, launchOperat
|
|
|
236
238
|
launch_id: launchId,
|
|
237
239
|
event_file: eventFile,
|
|
238
240
|
created_at: new Date().toISOString(),
|
|
239
|
-
cwd,
|
|
241
|
+
cwd: transcriptCwd,
|
|
240
242
|
vendor_session_id: randomUUID(),
|
|
241
243
|
initial_prompt: initialPrompt,
|
|
242
244
|
launch_operation_id: launchOperationId,
|
package/dist/harnesses/grok.js
CHANGED
|
@@ -342,8 +342,8 @@ export function grokFooterHasConfiguration(screen, model, effort) {
|
|
|
342
342
|
return modelLabel !== null || effort !== null;
|
|
343
343
|
});
|
|
344
344
|
}
|
|
345
|
-
//
|
|
346
|
-
export function grokTranscriptText(meta, readTranscriptLines, transcriptUnavailable) {
|
|
345
|
+
// 配送では完了eventのturn_numberと履歴のprompt_indexを相関する。通常照会は直近の実user発話を読む。
|
|
346
|
+
export function grokTranscriptText(meta, readTranscriptLines, transcriptUnavailable, completedTurnId) {
|
|
347
347
|
const directory = grokSessionDirectory(meta);
|
|
348
348
|
if (!directory)
|
|
349
349
|
transcriptUnavailable();
|
|
@@ -364,8 +364,38 @@ export function grokTranscriptText(meta, readTranscriptLines, transcriptUnavaila
|
|
|
364
364
|
// 外部 transcript の壊れた1行は残りの完結行を読む妨げにしない。
|
|
365
365
|
}
|
|
366
366
|
}
|
|
367
|
+
let end = records.length;
|
|
368
|
+
if (completedTurnId !== undefined) {
|
|
369
|
+
let activePrompt = null;
|
|
370
|
+
let completedPrompt = null;
|
|
371
|
+
for (const line of readTranscriptLines(path.join(directory, "events.jsonl"))) {
|
|
372
|
+
if (!line.trim())
|
|
373
|
+
continue;
|
|
374
|
+
let event;
|
|
375
|
+
try {
|
|
376
|
+
event = JSON.parse(line);
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
if (event.type === "turn_started")
|
|
382
|
+
activePrompt = Number.isSafeInteger(event.turn_number) && event.turn_number >= 0 ? event.turn_number : null;
|
|
383
|
+
else if (completedTurnId !== null && grokCompletionEvent(meta, event)?.turn_id === completedTurnId) {
|
|
384
|
+
completedPrompt = activePrompt;
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
const starts = records.flatMap((record, index) => record.type === "user" && !("synthetic_reason" in record)
|
|
389
|
+
&& completedPrompt !== null && record.prompt_index === completedPrompt ? [index] : []);
|
|
390
|
+
if (starts.length !== 1)
|
|
391
|
+
throw new AitermError("GROK_TRANSCRIPT_TURN_UNAVAILABLE: 完了turnと回答の相関を確認できません。別turnの回答は使いません", 2);
|
|
392
|
+
lastUser = starts[0];
|
|
393
|
+
const next = records.findIndex((record, index) => index > lastUser && record.type === "user" && !("synthetic_reason" in record));
|
|
394
|
+
if (next >= 0)
|
|
395
|
+
end = next;
|
|
396
|
+
}
|
|
367
397
|
const replies = records
|
|
368
|
-
.slice(lastUser + 1)
|
|
398
|
+
.slice(lastUser + 1, end)
|
|
369
399
|
.filter((record) => record?.type === "assistant" && typeof record?.content === "string")
|
|
370
400
|
.map((record) => record.content.trim())
|
|
371
401
|
.filter(Boolean);
|
package/dist/index.js
CHANGED
|
@@ -17,6 +17,7 @@ import { runtimeErrorStoreDiagnostic } from "./runtime-error-store.js";
|
|
|
17
17
|
import { createRequire } from "node:module";
|
|
18
18
|
import { ParentDeliveryManager } from "./parent-delivery.js";
|
|
19
19
|
import { codexParentFromRequest } from "./codex-parent-receiver.js";
|
|
20
|
+
import { claudeParentFromRequest } from "./claude-parent-receiver.js";
|
|
20
21
|
// package.json の version を実行時に読み、MCP initialize で配るサーバ版と一致させる。
|
|
21
22
|
// createRequire を使うのは、import 属性 `with { type: "json" }` が Node 18.20+ 限定で
|
|
22
23
|
// engines "node >=18"(18.0〜18.19)を SyntaxError で壊し、旧 `assert` 構文は逆に Node 22 で
|
|
@@ -25,10 +26,11 @@ const pkg = createRequire(import.meta.url)("../package.json");
|
|
|
25
26
|
const server = new McpServer({ name: "aiterm", version: pkg.version });
|
|
26
27
|
let parentDelivery = null;
|
|
27
28
|
async function deliveryForRequest(extra) {
|
|
28
|
-
const
|
|
29
|
+
const clientName = server.server.getClientVersion()?.name;
|
|
30
|
+
const parent = codexParentFromRequest(clientName, extra._meta) ?? claudeParentFromRequest(clientName, extra._meta);
|
|
29
31
|
if (!parent)
|
|
30
32
|
return null;
|
|
31
|
-
parentDelivery ??= new ParentDeliveryManager();
|
|
33
|
+
parentDelivery ??= new ParentDeliveryManager({ parent_kind: clientName === "claude-code" ? "claude" : undefined });
|
|
32
34
|
await parentDelivery.prepare(parent);
|
|
33
35
|
return parentDelivery.request(parent);
|
|
34
36
|
}
|
|
@@ -38,7 +40,7 @@ async function deliveryForRequest(extra) {
|
|
|
38
40
|
* ここでは「待つな」を断定形で先に置き、foreground 実行の禁止までを説明の側に含める。
|
|
39
41
|
*/
|
|
40
42
|
const NON_BLOCKING_RULE = "dispatch した子は投げっぱなしでよい=親はここで待たない。" +
|
|
41
|
-
"Codex親にはAitermが回答本文を自動配送する。parent_deliveryがある場合はwait起動も通常の回答回収も不要。親は作業を続けるかターンを終える。" +
|
|
43
|
+
"Codex親とClaude Code親にはAitermが回答本文を自動配送する。parent_deliveryがある場合はwait起動も通常の回答回収も不要。親は作業を続けるかターンを終える。" +
|
|
42
44
|
"その他の親では、完了通知をreceiptの `wait_process.executable` と `wait_process.args` をそのまま親のターンを塞がない別プロセスAPIへ渡して受け、" +
|
|
43
45
|
"PowerShell 7のStart-Processだけは `windows_start_process_argument_list` を単一文字列として渡す。" +
|
|
44
46
|
`exit を完了通知として扱う(${core.AITERM_WAIT_OUTCOME_NOTE}。ポーリング不要)。` +
|
|
@@ -861,8 +863,8 @@ async function main() {
|
|
|
861
863
|
server.server.oninitialized = () => {
|
|
862
864
|
const name = server.server.getClientVersion()?.name;
|
|
863
865
|
core.setParentClient(name ?? null);
|
|
864
|
-
if (name === "codex-mcp-client")
|
|
865
|
-
parentDelivery ??= new ParentDeliveryManager();
|
|
866
|
+
if (name === "codex-mcp-client" || name === "claude-code")
|
|
867
|
+
parentDelivery ??= new ParentDeliveryManager({ parent_kind: name === "claude-code" ? "claude" : undefined });
|
|
866
868
|
};
|
|
867
869
|
server.server.onclose = () => {
|
|
868
870
|
void parentDelivery?.close().catch(() => process.stderr.write("aiterm: PARENT_DELIVERY_CLOSE_FAILED\n"));
|
package/dist/parent-delivery.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Aiterm
|
|
1
|
+
// Aitermが所有する、子の完了観測・回答本文の保存・親への配送。
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { randomUUID, createHash } from "node:crypto";
|
|
@@ -7,13 +7,14 @@ import { observeAgentDone, readAgentTranscriptResult } from "./core.js";
|
|
|
7
7
|
import { ensureStateRoot, writeJson0600 } from "./agent-shared.js";
|
|
8
8
|
import { readRuntimeProcesses } from "./process-runtime.js";
|
|
9
9
|
import { CodexDeliveryError, submitCodexParentAnswer, verifyCodexParent } from "./codex-parent-receiver.js";
|
|
10
|
+
import { claudeParentSchema, ClaudeDeliveryError, bindClaudeParentDelivery, submitClaudeParentAnswer, verifyClaudeParent } from "./claude-parent-receiver.js";
|
|
10
11
|
import { AitermError } from "./errors.js";
|
|
11
12
|
const recordSchema = z.object({
|
|
12
13
|
schema: z.literal("aiterm.parent-delivery.v1"),
|
|
13
14
|
delivery_id: z.uuid(),
|
|
14
15
|
created_at: z.string(),
|
|
15
16
|
updated_at: z.string(),
|
|
16
|
-
parent: z.object({ thread_id: z.uuid(), codex_home: z.string() }).strict(),
|
|
17
|
+
parent: z.union([z.object({ thread_id: z.uuid(), codex_home: z.string() }).strict(), claudeParentSchema]),
|
|
17
18
|
boundary: z.object({
|
|
18
19
|
session_id: z.string().regex(/^[A-Za-z0-9_-]{1,64}$/),
|
|
19
20
|
launch_id: z.string().regex(/^[0-9a-f]{32}$/),
|
|
@@ -28,6 +29,16 @@ const recordSchema = z.object({
|
|
|
28
29
|
error: z.string().nullable(),
|
|
29
30
|
queued_submission_id: z.string().nullable(),
|
|
30
31
|
}).strict();
|
|
32
|
+
const isClaude = (parent) => "kind" in parent && parent.kind === "claude";
|
|
33
|
+
async function verifyParent(parent) {
|
|
34
|
+
if (isClaude(parent))
|
|
35
|
+
verifyClaudeParent(parent);
|
|
36
|
+
else
|
|
37
|
+
await verifyCodexParent(parent);
|
|
38
|
+
}
|
|
39
|
+
async function submitParentAnswer(parent, deliveryId, text) {
|
|
40
|
+
return isClaude(parent) ? submitClaudeParentAnswer(parent, deliveryId, text) : submitCodexParentAnswer(parent, deliveryId, text);
|
|
41
|
+
}
|
|
31
42
|
function readRecord(file) {
|
|
32
43
|
try {
|
|
33
44
|
const record = recordSchema.parse(JSON.parse(fs.readFileSync(file, "utf8")));
|
|
@@ -56,6 +67,7 @@ function answerMessage(record) {
|
|
|
56
67
|
export class ParentDeliveryManager {
|
|
57
68
|
deps;
|
|
58
69
|
root;
|
|
70
|
+
recordRoots;
|
|
59
71
|
active;
|
|
60
72
|
results;
|
|
61
73
|
claims;
|
|
@@ -68,12 +80,15 @@ export class ParentDeliveryManager {
|
|
|
68
80
|
serviceError = null;
|
|
69
81
|
closing = false;
|
|
70
82
|
constructor(options = {}) {
|
|
71
|
-
this.deps = { observe: observeAgentDone, answer: readAgentTranscriptResult, submit:
|
|
72
|
-
verify:
|
|
73
|
-
|
|
83
|
+
this.deps = { observe: observeAgentDone, answer: readAgentTranscriptResult, submit: submitParentAnswer,
|
|
84
|
+
verify: verifyParent, processes: readRuntimeProcesses, ...options.dependencies };
|
|
85
|
+
const stateRoot = ensureStateRoot();
|
|
86
|
+
this.root = options.root ?? path.join(stateRoot, options.parent_kind === "claude" ? "claude-parent-deliveries" : "parent-deliveries");
|
|
87
|
+
// 旧版のCodex readerへ未知のparentを渡さない。公開照会と子の予約だけは両方で共有する。
|
|
88
|
+
this.recordRoots = options.root ? [options.root] : [path.join(stateRoot, "parent-deliveries"), path.join(stateRoot, "claude-parent-deliveries")];
|
|
74
89
|
this.active = path.join(this.root, "active");
|
|
75
90
|
this.results = path.join(this.root, "results");
|
|
76
|
-
this.claims = path.join(
|
|
91
|
+
this.claims = path.join(options.root ?? path.join(stateRoot, "parent-deliveries"), "claims");
|
|
77
92
|
const ownProcess = this.deps.processes().find((entry) => entry.pid === process.pid);
|
|
78
93
|
if (!ownProcess)
|
|
79
94
|
throw new AitermError("PARENT_DELIVERY_OWNER_UNKNOWN: 配送processを識別できません", 2);
|
|
@@ -127,6 +142,8 @@ export class ParentDeliveryManager {
|
|
|
127
142
|
}
|
|
128
143
|
throw error;
|
|
129
144
|
}
|
|
145
|
+
if (isClaude(parent))
|
|
146
|
+
bindClaudeParentDelivery(parent, record.delivery_id);
|
|
130
147
|
this.jobs.set(record.delivery_id, job);
|
|
131
148
|
this.watch(job);
|
|
132
149
|
});
|
|
@@ -243,7 +260,7 @@ export class ParentDeliveryManager {
|
|
|
243
260
|
job.record.state = "submitted";
|
|
244
261
|
}
|
|
245
262
|
catch (error) {
|
|
246
|
-
job.record.state = error instanceof CodexDeliveryError && !error.outcome_unknown ? "failed" : "unknown";
|
|
263
|
+
job.record.state = (error instanceof CodexDeliveryError || error instanceof ClaudeDeliveryError) && !error.outcome_unknown ? "failed" : "unknown";
|
|
247
264
|
job.record.error = error instanceof Error ? error.message : String(error);
|
|
248
265
|
}
|
|
249
266
|
this.finish(job);
|
|
@@ -268,12 +285,20 @@ export class ParentDeliveryManager {
|
|
|
268
285
|
}
|
|
269
286
|
}
|
|
270
287
|
files() {
|
|
271
|
-
const files =
|
|
272
|
-
for (const
|
|
273
|
-
|
|
288
|
+
const files = [];
|
|
289
|
+
for (const root of this.recordRoots) {
|
|
290
|
+
const results = path.join(root, "results");
|
|
291
|
+
if (fs.existsSync(results))
|
|
292
|
+
files.push(...fs.readdirSync(results).filter((name) => name.endsWith(".json")).map((name) => path.join(results, name)));
|
|
293
|
+
const active = path.join(root, "active");
|
|
294
|
+
if (!fs.existsSync(active))
|
|
274
295
|
continue;
|
|
275
|
-
const
|
|
276
|
-
|
|
296
|
+
for (const owner of fs.readdirSync(active, { withFileTypes: true })) {
|
|
297
|
+
if (!owner.isDirectory())
|
|
298
|
+
continue;
|
|
299
|
+
const directory = path.join(active, owner.name);
|
|
300
|
+
files.push(...fs.readdirSync(directory).filter((name) => name !== "owner.json" && name.endsWith(".json")).map((name) => path.join(directory, name)));
|
|
301
|
+
}
|
|
277
302
|
}
|
|
278
303
|
return files;
|
|
279
304
|
}
|
|
@@ -348,11 +373,12 @@ export class ParentDeliveryManager {
|
|
|
348
373
|
}
|
|
349
374
|
else if (record.state === "waiting" || record.state === "ready") {
|
|
350
375
|
try {
|
|
351
|
-
await this.deps.verify(record.parent);
|
|
352
376
|
if (record.state === "waiting")
|
|
353
377
|
this.watch(job);
|
|
354
|
-
else
|
|
378
|
+
else {
|
|
379
|
+
await this.deps.verify(record.parent);
|
|
355
380
|
job.delivery = this.deliver(job).catch((error) => this.reportServiceError(error));
|
|
381
|
+
}
|
|
356
382
|
}
|
|
357
383
|
catch (error) {
|
|
358
384
|
record.state = "failed";
|
package/dist/setup-cli.js
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { runSetup } from "./setup.js";
|
|
3
|
+
import { removeClaudeParentHooks } from "./setup-integrations.js";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
3
6
|
const args = process.argv.slice(2);
|
|
4
|
-
if (args.length === 1 && ["--
|
|
5
|
-
|
|
7
|
+
if (args.length === 1 && args[0] === "--remove-claude-parent-hooks") {
|
|
8
|
+
try {
|
|
9
|
+
const status = removeClaudeParentHooks(join(process.env.CLAUDE_CONFIG_DIR ?? join(process.env.HOME ?? homedir(), ".claude"), "settings.json"));
|
|
10
|
+
process.stdout.write(`${JSON.stringify({ schema: "aiterm.claude-parent-hooks-remove-result.v1", status })}\n`);
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
process.stderr.write(`aiterm-setup: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
14
|
+
process.exitCode = 2;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
else if (args.length === 1 && ["--help", "-h"].includes(args[0])) {
|
|
18
|
+
process.stdout.write("使い方: aiterm-setup [--json | --remove-claude-parent-hooks]\n製品の依存準備、検出したAIへの登録、MCPと端末の実動作確認を行います。結果はJSONで返します。旧版へ戻す前の専用hook解除も、この入口で行います。\n");
|
|
6
19
|
}
|
|
7
20
|
else if (args.length > 1 || (args.length === 1 && args[0] !== "--json")) {
|
|
8
21
|
process.stderr.write("使い方: aiterm-setup [--json]\n");
|
|
@@ -56,6 +56,106 @@ export function mergeJsonMcp(file, registration) {
|
|
|
56
56
|
}
|
|
57
57
|
return "configured";
|
|
58
58
|
}
|
|
59
|
+
export function claudeParentHookEntries(registration) {
|
|
60
|
+
const command = { type: "command", command: registration.command, args: [join(dirname(registration.args[0]), "claude-parent-hook.js")] };
|
|
61
|
+
const matcher = "^mcp__aiterm__(agent_launch|claude_agent|codex_agent|grok_agent|composer_agent|pty_send|claude_turn)$";
|
|
62
|
+
return {
|
|
63
|
+
PreToolUse: [{ matcher, hooks: [{ ...command, timeout: 15 }] }],
|
|
64
|
+
PostToolUse: [{ matcher, hooks: [{ ...command, asyncRewake: true, timeout: 86400 }] }],
|
|
65
|
+
SessionEnd: [{ hooks: [{ ...command, timeout: 15 }] }],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
export function mergeClaudeParentHooks(file, registration) {
|
|
69
|
+
const target = existsSync(file) ? realpathSync(file) : file;
|
|
70
|
+
let current = {};
|
|
71
|
+
if (existsSync(target)) {
|
|
72
|
+
try {
|
|
73
|
+
current = JSON.parse(readFileSync(target, "utf8"));
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
throw new SetupError("config_invalid", "Claudeのhook設定JSONを読めません");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (!record(current) || (current.hooks !== undefined && !record(current.hooks))) {
|
|
80
|
+
throw new SetupError("config_invalid", "Claudeのhooks設定はobjectである必要があります");
|
|
81
|
+
}
|
|
82
|
+
if (current.disableAllHooks === true)
|
|
83
|
+
throw new SetupError("claude_parent_hooks_disabled", "Claudeのhookが無効です。disableAllHooksをfalseに変更してからsetupしてください");
|
|
84
|
+
const hooks = { ...current.hooks };
|
|
85
|
+
for (const [event, additions] of Object.entries(claudeParentHookEntries(registration))) {
|
|
86
|
+
const previous = hooks[event] ?? [];
|
|
87
|
+
if (!Array.isArray(previous) || previous.some(group => !record(group) || !Array.isArray(group.hooks))) {
|
|
88
|
+
throw new SetupError("config_invalid", `Claudeの${event} hook形式を読めません`);
|
|
89
|
+
}
|
|
90
|
+
// 他製品のhookとmatcherはそのまま保ち、当製品の専用entryだけを更新する。
|
|
91
|
+
const retained = previous.map(group => ({ ...group, hooks: group.hooks.filter(hook => !isClaudeParentHook(hook))
|
|
92
|
+
})).filter(group => group.hooks.length > 0);
|
|
93
|
+
hooks[event] = [...retained, ...additions];
|
|
94
|
+
}
|
|
95
|
+
const next = { ...current, hooks };
|
|
96
|
+
if (isDeepStrictEqual(current, next))
|
|
97
|
+
return "unchanged";
|
|
98
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
99
|
+
const temporary = `${target}.aiterm-${randomUUID()}`;
|
|
100
|
+
try {
|
|
101
|
+
writeFileSync(temporary, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600, flag: "wx" });
|
|
102
|
+
if (existsSync(target))
|
|
103
|
+
copyFileSync(target, `${target}.aiterm-backup`);
|
|
104
|
+
renameSync(temporary, target);
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
if (existsSync(temporary))
|
|
108
|
+
unlinkSync(temporary);
|
|
109
|
+
}
|
|
110
|
+
if (!isDeepStrictEqual(JSON.parse(readFileSync(target, "utf8")), next)) {
|
|
111
|
+
throw new SetupError("config_readback_failed", "Claudeのhook登録の読戻しが一致しません");
|
|
112
|
+
}
|
|
113
|
+
return "configured";
|
|
114
|
+
}
|
|
115
|
+
function isClaudeParentHook(hook) {
|
|
116
|
+
return record(hook) && hook.type === "command" && Array.isArray(hook.args)
|
|
117
|
+
&& typeof hook.args[0] === "string" && /[/\\]claude-parent-hook\.js$/.test(hook.args[0]);
|
|
118
|
+
}
|
|
119
|
+
/** hookを持たない旧版へ戻す前に、当製品の登録だけを除く。 */
|
|
120
|
+
export function removeClaudeParentHooks(file) {
|
|
121
|
+
if (!existsSync(file))
|
|
122
|
+
return "unchanged";
|
|
123
|
+
const target = realpathSync(file);
|
|
124
|
+
const current = JSON.parse(readFileSync(target, "utf8"));
|
|
125
|
+
if (!record(current) || (current.hooks !== undefined && !record(current.hooks))) {
|
|
126
|
+
throw new SetupError("config_invalid", "Claudeのhook設定を読めません");
|
|
127
|
+
}
|
|
128
|
+
if (current.hooks === undefined)
|
|
129
|
+
return "unchanged";
|
|
130
|
+
const hooks = { ...current.hooks };
|
|
131
|
+
for (const event of ["PreToolUse", "PostToolUse", "SessionEnd"]) {
|
|
132
|
+
if (hooks[event] === undefined)
|
|
133
|
+
continue;
|
|
134
|
+
if (!Array.isArray(hooks[event]))
|
|
135
|
+
throw new SetupError("config_invalid", "Claudeのhook設定を読めません");
|
|
136
|
+
hooks[event] = hooks[event].map(group => {
|
|
137
|
+
if (!record(group) || !Array.isArray(group.hooks))
|
|
138
|
+
throw new SetupError("config_invalid", "Claudeのhook設定を読めません");
|
|
139
|
+
return { ...group, hooks: group.hooks.filter(hook => !isClaudeParentHook(hook)) };
|
|
140
|
+
}).filter(group => group.hooks.length > 0);
|
|
141
|
+
}
|
|
142
|
+
const next = { ...current, hooks };
|
|
143
|
+
if (isDeepStrictEqual(current, next))
|
|
144
|
+
return "unchanged";
|
|
145
|
+
const temporary = `${target}.aiterm-${randomUUID()}`;
|
|
146
|
+
try {
|
|
147
|
+
writeFileSync(temporary, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600, flag: "wx" });
|
|
148
|
+
copyFileSync(target, `${target}.aiterm-backup`);
|
|
149
|
+
renameSync(temporary, target);
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
if (existsSync(temporary))
|
|
153
|
+
unlinkSync(temporary);
|
|
154
|
+
}
|
|
155
|
+
if (!isDeepStrictEqual(JSON.parse(readFileSync(target, "utf8")), next))
|
|
156
|
+
throw new SetupError("config_readback_failed", "Claude hook解除の読戻しが一致しません");
|
|
157
|
+
return "removed";
|
|
158
|
+
}
|
|
59
159
|
export function configureIntegrations(home, registration, run = runSetupCommand, resolveClient = resolveAgentBin) {
|
|
60
160
|
const results = {};
|
|
61
161
|
for (const client of ["claude", "codex", "grok", "cursor"]) {
|
|
@@ -69,6 +169,13 @@ export function configureIntegrations(home, registration, run = runSetupCommand,
|
|
|
69
169
|
const file = client === "cursor" ? join(home, ".cursor", "mcp.json")
|
|
70
170
|
: process.env.CLAUDE_CONFIG_DIR ? join(process.env.CLAUDE_CONFIG_DIR, ".claude.json") : join(home, ".claude.json");
|
|
71
171
|
mergeJsonMcp(file, client === "claude" ? { type: "stdio", ...registration } : registration);
|
|
172
|
+
if (client === "claude") {
|
|
173
|
+
const version = /\b(\d+)\.(\d+)\.(\d+)\b/.exec(run(executable, ["--version"]));
|
|
174
|
+
if (!version || Number(version[1]) < 2 || (Number(version[1]) === 2 && (Number(version[2]) < 1 || (Number(version[2]) === 1 && Number(version[3]) < 259)))) {
|
|
175
|
+
throw new SetupError("claude_parent_delivery_unavailable", "自動配送にはClaude Code 2.1.259以上が必要です。公式CLIを更新してください");
|
|
176
|
+
}
|
|
177
|
+
mergeClaudeParentHooks(join(process.env.CLAUDE_CONFIG_DIR ?? join(home, ".claude"), "settings.json"), registration);
|
|
178
|
+
}
|
|
72
179
|
}
|
|
73
180
|
else if (client === "codex") {
|
|
74
181
|
// 親threadがないsetupでは公式queue入口まで確認し、宛先は各dispatchで検証する。
|
package/docs/00_overview.md
CHANGED
|
@@ -17,9 +17,8 @@ dotagentsは任意の工場統合を担うが、Aitermの製品正典や実行
|
|
|
17
17
|
Grok/Composerのsandbox起動拒否については、[DESIGNの失敗と復旧](DESIGN.md#failure-and-recovery)に
|
|
18
18
|
検出の所有と適用範囲、[RELEASEの公開後smoke](RELEASE.md#公開後smoke)に検証条件を置く。
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
- [子の回答を親へ自動配送する設計・実装計画](https://github.com/kitepon/aiterm-mcp/blob/main/docs/plan_parent-result-delivery.md): Claude Code/Codex親への本文配送。設計案であり、現行APIは未変更。
|
|
20
|
+
Codex親への回答の自動配送は[DESIGN](DESIGN.md#codex親への自動配送)に、対応範囲と失敗時の状態を置く。
|
|
21
|
+
Claude Code親の非同期hookによる受信は[DESIGN](DESIGN.md#claude-code親への自動配送)を参照する。
|
|
23
22
|
|
|
24
23
|
## 履歴と証拠
|
|
25
24
|
|
package/docs/DESIGN.md
CHANGED
|
@@ -49,10 +49,13 @@ Throughlineの補足記憶はpathを透過搬送するだけで、内容、proje
|
|
|
49
49
|
|
|
50
50
|
agent turnは常に非ブロックdispatchであり、receiptの`event_cursor`がturn境界になる。
|
|
51
51
|
Codex親にはAitermのMCP processが完了を観測し、回答本文を公式受信キューへ自動配送する。
|
|
52
|
+
Claude Code親は公式の非同期hookで本文を受け取り、待機中も新しいturnへ進める。
|
|
52
53
|
それ以外の親には`wait_process`がplatform nativeな別process起動情報を返す。
|
|
53
54
|
waiterは純readerで、親のforeground turnを塞がない。
|
|
54
55
|
回答はharness所有transcriptから同じturnへ相関して回収し、欠落・曖昧・timeout時にpromptを再送しない。
|
|
55
56
|
Grok/Composerの記録先はCLIと同じOS絶対パスへcwdを正規化して導出し、完了通知と回答で同じ関数を使う。
|
|
57
|
+
配送用のGrok回答は`turn_ended.ts`から同じturnの`turn_started.turn_number`を取得し、
|
|
58
|
+
`chat_history.jsonl`の`user.prompt_index`と相関する。次turnが既に始まっていても対象回答だけを回収する。
|
|
56
59
|
`agent_steer`は実行中のCodex/Grok turnへ追加textを差し込み、idleなら送信せず状態を返す。
|
|
57
60
|
Cursorのsubmitはadapterがextended keyboard protocolのEnterへ変換し、呼び出し側は通常のdispatchだけを使う。
|
|
58
61
|
起動直後のClaude sessionへの初回dispatchは、他harnessと同じくTUIの入力受付を確認してから貼付とEnterを送る。
|
|
@@ -72,12 +75,45 @@ native sub-agentを親にした外部queue入力はCodex自身が拒否するた
|
|
|
72
75
|
`pty_observe`の`parent_deliveries`で状態を確認できる。`submitted`はキュー受付済みを示し、modelの読了を意味しない。
|
|
73
76
|
次の依頼へ進める前に前回の回答を保存し、harness所有記録を後の回答と取り違えない。
|
|
74
77
|
|
|
75
|
-
|
|
78
|
+
Codexの配送記録と本文はAiterm stateの`parent-deliveries`へ保存する。ownerのPIDと開始識別子で生存を判定し、
|
|
76
79
|
再接続後は終了したownerの記録だけを原子的に引き継ぐ。`waiting`は同じ境界から観測を再開し、
|
|
77
80
|
`ready`は保存した本文を送る。送信中断は`unknown`として本文を残し、自動再送しない。
|
|
78
81
|
受信口が明示拒否した場合は`failed`、子の異常終了はそのoutcomeを配送する。
|
|
79
82
|
このstateは既存のPTY/harness stateと独立し、旧版は配送を再開しない。
|
|
80
83
|
|
|
84
|
+
### Claude Code親への自動配送
|
|
85
|
+
|
|
86
|
+
`aiterm-setup`はClaude Code 2.1.259以上を確認し、ユーザー設定へAiterm専用の`PreToolUse`、
|
|
87
|
+
`PostToolUse`、`SessionEnd`を追加する。他製品のhookと設定は保持し、`disableAllHooks`の解除は行わない。
|
|
88
|
+
hookはNodeの実行ファイルと引数配列で直接起動し、shellやWindowsのnpm shimを介さない。
|
|
89
|
+
|
|
90
|
+
`PreToolUse`の`tool_use_id`/`session_id`とMCP要求の`_meta["claudecode/toolUseId"]`を照合する。
|
|
91
|
+
起動時のsession環境変数は`/clear`で古くなるため宛先に使わない。hookがない場合と`agent_id`付きの会話は
|
|
92
|
+
子への送信前に明示errorにする。親がIDや待機方法を引数で指定する必要はない。
|
|
93
|
+
|
|
94
|
+
本文の保存と同じ子への連続依頼の制御は`parent-delivery.ts`を共有する。Claude用記録は
|
|
95
|
+
`claude-parent-deliveries`へ分け、旧版のCodex readerに未知のparentを読ませない。
|
|
96
|
+
子の予約は既存の`parent-deliveries/claims`で共有し、親の種類をまたぐ並行送信を防ぐ。
|
|
97
|
+
hookとの受け渡しはAiterm stateの`claude-parent-hooks`へ置く。
|
|
98
|
+
|
|
99
|
+
`PostToolUse`は`asyncRewake:true`で待機し、保存済み本文をstderrへ出してexit 2を返す。
|
|
100
|
+
exit 2はClaudeが規定する再開信号であり、子の成功・失敗は本文の`outcome`で区別する。
|
|
101
|
+
親は待機中も別作業や次のturnへ進める。Claudeの画面では`Stop hook feedback`として届く。
|
|
102
|
+
`submitted`はhookへの本文出力を確認した状態であり、modelの読了を示さない。
|
|
103
|
+
hook出力の切断・中断は`failed`または`unknown`とし、本文を残して自動再送しない。
|
|
104
|
+
|
|
105
|
+
`SessionEnd`はその会話の未送信の依頼を終了させる。`/clear`後の新しい会話へ古い回答を出さず、
|
|
106
|
+
確定本文は保存する。受信hookのtimeoutは24時間であり、timeoutとprocess終了は配送失敗として観測する。
|
|
107
|
+
CLIを終了した後に自動再開するdaemon、Channelsの有効化flag、子の返送コマンドは使わない。
|
|
108
|
+
hookを持たない旧版へ戻す時は、install前に`aiterm-setup --remove-claude-parent-hooks`で専用hookだけを解除する。
|
|
109
|
+
|
|
110
|
+
Claude Desktopのチャット、Web、`agent_id`付きの会話(`--agent`で選んだ主会話とnative subagent)は
|
|
111
|
+
この受信契約の対象に含めない。
|
|
112
|
+
対応対象は公式command hookとMCP metadataを提供するClaude Codeの対話sessionである。
|
|
113
|
+
|
|
114
|
+
Claudeの起動metadataには指定cwdの実体パスを保存する。Claude Codeが実体パスから作るproject slugと
|
|
115
|
+
APIエラー監視の参照先を一致させ、監視中のリンク変更で保存場所を取り違えない。
|
|
116
|
+
|
|
81
117
|
`trust_project:true`は対象projectの既知のworkspace、hooks、MCP初期同意を起動準備として進める意図である。
|
|
82
118
|
promptなしでも入力受付とharness生存を確認して`startup.ready`を返す。指定なしのpromptなし起動は
|
|
83
119
|
従来どおり`startup.not_checked`で返す。初手receiptは未要求・未送信・送信済み未確認・開始確認を分ける。
|
package/docs/RELEASE.md
CHANGED
|
@@ -39,6 +39,10 @@ setupを変更した場合は、公開packageのglobal install後に`aiterm-setu
|
|
|
39
39
|
端末実行と検出した各AIの登録結果を確認する。初回と再実行は一時設定領域でも試験し、所有外の設定保持を確かめる。
|
|
40
40
|
WindowsのGrokパス変更ではスラッシュ区切りcwdで起動し、同じturnの完了通知と回答回収を確認する。
|
|
41
41
|
|
|
42
|
+
親への自動配送を変更した場合は、通常HOMEの親から子を起動し、親がwaiter・回収を呼ばずに
|
|
43
|
+
初手と同じ子への追加依頼の回答を受け取ることを確認する。Claudeではhook待機中に別のturnへ進めること、
|
|
44
|
+
`/clear`後に未送信の旧回答が届かず、Aitermに本文が保存されることも確認する。
|
|
45
|
+
|
|
42
46
|
公式npm packageを隔離またはglobal installし、変更に触れたharnessの起動、non-blocking dispatch、wait outcome、
|
|
43
47
|
transcript回収、`pty_close`後の残骸ゼロを確認する。
|
|
44
48
|
|
|
@@ -58,6 +62,9 @@ aiterm-setup --json
|
|
|
58
62
|
|
|
59
63
|
巻き戻しは既知の正常版を指定する。
|
|
60
64
|
|
|
65
|
+
Claude親配送hookのない旧版へ戻す場合は、旧版のinstall前に`aiterm-setup --remove-claude-parent-hooks`を
|
|
66
|
+
実行する。Aiterm専用の3 hookだけを解除し、他製品のhookと設定を保持する。
|
|
67
|
+
|
|
61
68
|
```bash
|
|
62
69
|
npm install -g "aiterm-mcp@<known-good-version>"
|
|
63
70
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aiterm-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.1",
|
|
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": [
|