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 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.34.0...HEAD
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.34.0** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
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`を返し、Codex親には回答本文を公式受信キューへ自動配送する。それ以外の親は`aiterm-wait`を親のターンを塞がない別processで受ける。Cursorのsubmitキーはadapterが現行CLIのextended keyboard protocolへ変換する。送信textがCursorのcomposerへ残った場合は成功receiptを返さず失敗する。
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が完了を観測し、加工前の回答を保存して、依頼元Codexの公式受信キューへ送る。親はidleになった後に回答を処理する。waiter、`pty_read`による回答回収、子への送信指示は不要。Desktop固有の接続を使わず、CLIでも同じ経路になる。子は全対応harnessから選べる。Claude等の親は既存のwaiter経路を使う。
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`はキューの受付済みであり、modelの読了ではない。MCP再接続後は未送信の記録を再開し、送信中に接続が切れて結果が分からない場合は本文を保持して`unknown`とする。自動再送はしない。
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
- every send to an agent session is a non-blocking dispatch. Codex parents now receive the complete answer automatically; other parents use the
180
- `aiterm-wait` waiter whose exit codes mirror the receipt outcome
181
- (`0`=done / `3`=timeout, not finished / `4`=closed / `5`=running for a
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.34.0** · runs on Linux · WSL2 · macOS · native Windows (tmux on POSIX, the tmux-CLI-compatible [psmux](https://github.com/psmux/psmux) on native Windows — no WSL required) · MIT · see the [CHANGELOG](CHANGELOG.md).
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
- The human-readable launch text is accompanied by an `aiterm.agent-launch-result.v1` structured receipt containing the canonical `harness`; the old `provider` field remains for compatibility. The same `harness` is carried by agent dispatch, `aiterm-wait`, `agent_configure`, and agent rows in `pty_list`, while their old vendor/provider/agent fields remain compatibility fields. Codex completion comes from its normal durable rollout transcript, Grok CLI from its normal session events, Claude Code from a launch-specific Stop hook settings addition, and Cursor from its normal agent transcript's terminal `turn_ended` record. Sending to any agent session is a non-blocking **dispatch** — the call returns immediately with an opaque, harness-specific integer `event_cursor`, and Codex parents receive the answer automatically through the official input queue. Other parents use [`aiterm-wait`](#completion-push-for-parent-agents-aiterm-wait). The Cursor adapter translates submit into the current CLI's extended keyboard protocol. If submitted text remains in Cursor's composer, its dispatch fails instead of returning a successful receipt.
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 parents receive the answer automatically. For other parents:
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 parents receive child answers automatically.** Launch or send a request, then continue other work or end the turn. Aiterm observes the child, saves the unabridged answer, and submits it to the requesting parent's official Codex input queue. The parent processes it when idle. No waiter, `pty_read`, child callback instructions, or Desktop-specific connection is needed. This covers all supported child harnesses; Claude and other parent hosts retain the waiter flow below.
552
+ **Codex/Claude Code親には子の回答本文が自動で届く。** 子を起動・dispatchした後は、別作業へ進むか親のturnを終える。Aitermが完了を観測し、加工前の本文を保存して親へ渡す。waiter、`pty_read`による回答回収、子への返送指示は不要。子は全対応harnessから選べる。
555
553
 
556
- Automatic receipts include `parent_delivery` and set `wait_process` / `wait_command` to null. `pty_observe` exposes `parent_deliveries`: `waiting`, `ready`, `sending`, `submitted`, `failed`, or `unknown`. `submitted` means accepted by the queue, not read by the model. Aiterm retains the answer and resumes unsent work after an MCP restart. An interrupted submission becomes `unknown` and is not blindly retried.
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. **The parent never runs the waiter in its own foreground.** Waiting is correct — but the waiter is a separate process, not the parent's turn. A harness that re-invokes its agent when a background task exits (Claude Code) runs the waiter **in the background** and gets woken with zero polling. So that this is not left to interpretation, aiterm reads `clientInfo.name` from the MCP `initialize` handshake and its receipts name the concrete invocation for the detected host — for Claude Code, literally `Bash(command: "aiterm-wait …", run_in_background: true)`. Unknown or undeclared hosts get the generic "start it as a process that does not block the parent's turn" wording; nothing else about the contract changes. Every receipt leads with the same rule: dispatch and let go, then go do something else or end the turn.
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" && (!done || done.turn_id !== o.completion.turn_id || done.operation_id !== o.completion.operation_id)) {
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
- if (parentClientName === "codex-mcp-client") {
2578
- return "回答本文はAitermがこのCodex親へ自動配送する。wait起動・ポーリング・通常の回答回収は不要。" +
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
- if (parentClientName === "codex-mcp-client")
2589
- return "回答本文はこのCodex親へ自動配送される。親は作業を続けるかターンを終える。";
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。${parentClientName === "codex-mcp-client" ? "回答本文はCodex親へ自動配送する" : "完了通知はaiterm-wait"})。中断はpty_key(${sid}, "C-c")、` +
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" 等) で操作する(対話)。`;
@@ -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,
@@ -342,8 +342,8 @@ export function grokFooterHasConfiguration(screen, model, effort) {
342
342
  return modelLabel !== null || effort !== null;
343
343
  });
344
344
  }
345
- // 最後のuser発話以降に確定した最後のassistantメッセージをchat_history.jsonlから抽出する。
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 parent = codexParentFromRequest(server.server.getClientVersion()?.name, extra._meta);
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"));
@@ -1,4 +1,4 @@
1
- // Aitermが所有する、子の完了観測・回答本文の保存・Codex親への配送。
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: submitCodexParentAnswer,
72
- verify: verifyCodexParent, processes: readRuntimeProcesses, ...options.dependencies };
73
- this.root = options.root ?? path.join(ensureStateRoot(), "parent-deliveries");
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(this.root, "claims");
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 = fs.readdirSync(this.results).filter((name) => name.endsWith(".json")).map((name) => path.join(this.results, name));
272
- for (const owner of fs.readdirSync(this.active, { withFileTypes: true })) {
273
- if (!owner.isDirectory())
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 directory = path.join(this.active, owner.name);
276
- files.push(...fs.readdirSync(directory).filter((name) => name !== "owner.json" && name.endsWith(".json")).map((name) => path.join(directory, name)));
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 && ["--help", "-h"].includes(args[0])) {
5
- process.stdout.write("使い方: aiterm-setup [--json]\n製品の依存準備、検出したAIへの登録、MCPと端末の実動作確認を行います。結果はJSONで返します。\n");
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で検証する。
@@ -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
- 配送記録と本文はAiterm stateの`parent-deliveries`へ保存する。ownerのPIDと開始識別子で生存を判定し、
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.34.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": [