aiterm-mcp 0.61.17 → 0.61.19

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,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.61.19] - 2026-10-26
11
+
12
+ - Fix native Windows shell selection for current Git Bash prompts wrapped across pane columns, preserving unknown and historical prompt boundaries. Both ordinary and marked literal multiline sends retain the correct shell syntax.
13
+
14
+ - Preserve typed Claude authentication preflight failures in ordinary MCP launch errors, distinguishing not authenticated, unconfirmed status and timeout without exposing the native status body or creating a session.
15
+
16
+ ## [0.61.18] - 2026-10-25
17
+
18
+ - Fix CURRENT-MENU-1: parse the current Claude auth phase, choices, cursor and occupied code field together; navigation consumes that same parse and rejects stale menus before keys.
19
+
20
+ - Add typed native auth stages and generation-bound `agent_auth respond`; keep existing receipt fields. Authorization codes bypass lastcmd, normal turns and pane logs. Legacy launch UUIDs remain missing rather than synthesized.
21
+ - Distinguish explicit Claude code/choice prompts, official device/browser stages, and unavailable input. Windows secret input remains unavailable until foreground ownership is accepted.
22
+
10
23
  ## [0.61.17] - 2026-10-24
11
24
 
12
25
  ### Changed
@@ -2861,7 +2874,9 @@ prototype (preserved under `prototype/python/` as the porting source and referen
2861
2874
  `ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
2862
2875
  provenance.
2863
2876
 
2864
- [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.17...HEAD
2877
+ [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.19...HEAD
2878
+ [0.61.19]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.18...v0.61.19
2879
+ [0.61.18]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.17...v0.61.18
2865
2880
  [0.61.17]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.16...v0.61.17
2866
2881
  [0.61.16]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.15...v0.61.16
2867
2882
  [0.61.15]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.14...v0.61.15
@@ -3065,3 +3080,6 @@ prototype (preserved under `prototype/python/` as the porting source and referen
3065
3080
  [0.3.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.2.0...v0.3.0
3066
3081
  [0.2.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.1.0...v0.2.0
3067
3082
  [0.1.0]: https://github.com/kitepon/aiterm-mcp/releases/tag/v0.1.0
3083
+
3084
+
3085
+ Claude authentication URL receipts recognize the official claude.com/cai/oauth/authorize PKCE authorization request and its nonsecret code=true display flag. Callback authorization codes and tokens remain excluded; the common URL credential filter is unchanged. This auth-only patch contains no native goal feature.
package/README.ja.md CHANGED
@@ -209,7 +209,7 @@ runtime-error store は canonical dotagents config の `collection.enabled: true
209
209
  場合だけ収集し、既定OFF、network送信は行いません。tag起点CIのnpm provenance(OIDC Trusted
210
210
  Publishing)で公開し、GitHub Release が Official MCP Registry を再登録します。
211
211
 
212
- **状態:** 開発継続中 · 現行公開版 **v0.61.17** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
212
+ **状態:** 開発継続中 · 現行公開版 **v0.61.19** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
213
213
 
214
214
  ### 更新と巻き戻し
215
215
 
@@ -592,7 +592,7 @@ Claudeの相関済み承認は既存の`claude_approval`を使う。
592
592
  | `pty_list` | textと構造化したsession一覧、明示した非秘密環境変数の照会 | `env_keys?` |
593
593
  | `pty_observe` | pane/harnessの生存、native process identity、状態と活動 | `session_id`, `cursor?` |
594
594
  | `agent_launch` | harnessとmodelを別軸で選ぶ正規agent起動入口 | `harness`, `prompt?`, `model?`, `reasoning_effort?`, `fast?`, `cwd?`, `write_scope?`, `trust_project?`, `env_vars?`, `throughline_source_session?`, `throughline_supplement_file?` |
595
- | `agent_auth` | 公式CLIの認証を開始・確認・取消し、公式URL・device code・入力待ちを返す | `harness`, `action`, `session_id?`, `cwd?`, `env_vars?`, `relogin?` |
595
+ | `agent_auth` | 公式CLIの認証を開始・確認・取消し、公式URL・device code・入力待ちを返す | `harness`, `action`, `session_id?`, `cwd?`, `env_vars?`, `relogin?`, `expected_binding?`, `response?` |
596
596
  | `agent_models` | harnessが今選べるmodelとreasoning effortを、そのharness自身の一覧からpromptを送らずに取得 | `harness`, `cwd?`, `include_hidden?` |
597
597
  | `agent_approval` | Codexの現在の承認を検査し、単発許可・拒否を送る | `action`, `session_id`, `approval_choice?`, `observed_prompt_digest?` |
598
598
  | `claude_agent` / `codex_agent` / `grok_agent` | deprecated互換alias(`composer_agent`は0.41.0で削除。Composerは`agent_launch`の`cursor-cli`で使う) | 旧launcher引数 |
@@ -636,7 +636,10 @@ Aitermは、既定では実行時エラーをどこへも送らない。送る
636
636
  - 返りの`fast`は、切り替えた後の実際の状態。`agent_models`のmodelごとの`fast`が、そのmodelで切り替えられるかを示す(GrokとCursorのfast版は別のmodelとして並ばず、元のmodelの`fast: true`になる)。最上位の`fast_unavailable`は、アカウントの都合でどのmodelでも使えない時の訳。
637
637
  - 断りは文の先頭の符号で分かる: `FAST_MODE_NOT_SUPPORTED`(そのmodelは対応していない)、`FAST_MODE_UNAVAILABLE`(アカウントや契約で使えない)、`FAST_MODE_MODEL_CONFLICT`(modelへfast版のIDを書いて`fast: false`を渡した)、`FAST_MODE_UNCONFIRMED`(切り替えた後の状態を確かめられない)。起動で`FAST_MODE_UNAVAILABLE`・`FAST_MODE_UNCONFIRMED`になった時は、起動を断らずに通常の速度で起こし、返りを`fast: false`、`fast_error: { code, message }`にする。
638
638
 
639
- `agent_auth({ harness, action:"start"|"status"|"cancel", session_id?, cwd?, env_vars?, relogin? })`は、各harnessの公式CLIで認証を進める。Claudeは`claude auth login`、CodexとGrokは`login --device-auth`、Cursorは`NO_OPEN_BROWSER=1 cursor-agent login`を使い、資格情報は各CLIだけが保存する。Aitermは資格情報を読取り・copy・編集せず、独自OAuthも実装しない。`remote`は他toolと同じ標準対応。
639
+ `agent_auth({ harness, action:"start"|"status"|"cancel"|"respond", session_id?, cwd?, env_vars?, relogin?, expected_binding?, response? })`は、各harnessの公式CLIで認証を進める。Claudeは`claude auth login`、CodexとGrokは`login --device-auth`、Cursorは`NO_OPEN_BROWSER=1 cursor-agent login`を使い、資格情報は各CLIだけが保存する。Aitermは資格情報を読取り・copy・編集せず、独自OAuthも実装しない。`remote`は他toolと同じ標準対応。
640
+
641
+ 認証UIは追加の`auth_ui`を使う。`stage`は`open_url` / `device_code` / `paste_code` / `select` / `waiting` / `complete` / `failed` / `unavailable`。公式URL・表示code・具体的な選択ラベル・入力種別をNativeが返し、`input_required`だけから入力内容や端末キーを推測しない。`respond`は直近の`auth_ui.binding`を`expected_binding`へ渡し、`expected_prompt_digest`には同じ`auth_ui.prompt_digest`を渡す。`response:{kind:"authorization_code",value}`または`{kind:"choice",id}`を同じ認証flowへ送る。認証コードを`pty_send`へ渡さない。専用入力はlastcmd・通常turn・paneログ・argv・一時fileへコードを保存せず、CLIの画面echoもpaneログへ保存しない。未発行と入力後不明は構造化errorの`effect:not_issued|unknown`で区別し、自動再送しない。legacy flowの`launch_id:null`は実native識別情報で照合し、UUIDを補わない。Windowsの専用入力は前面所有確認が未受入のため`not_issued`で拒否する。
642
+
640
643
 
641
644
  `start`は、公式の状態が認証済みなら何も起こさず`authenticated`(`session_id:null`)を返す。`relogin:true`を付けると、認証済みに見えても公式ログインを開始し、`session_id`付きの結果を返す。別のアカウントへ入り直す時と、ログインの期限切れを状態から見抜けない時に使う。Aitermは資格情報を消さず、置き換えは公式CLIが行う。ただし、公式CLIがログインを始めた時点で元のログインを消す事がある(Codex 0.160.0の`codex login --device-auth`は、始めた時点で`auth.json`を消す。途中で`cancel`しても戻らない)。Claude CodeとCursorは、途中で`cancel`すれば元の資格情報が残る(無効な資格情報で確認)。
642
645
 
@@ -649,9 +652,9 @@ Aitermは、既定では実行時エラーをどこへも送らない。送る
649
652
  | Grok | `grok models`(状態の命令は無い) | `blocked`(`You are not authenticated.`)。Grokは使えない資格情報を自分で消す |
650
653
  | Cursor | `cursor-agent status` | `failed`(`Logged in (unable to fetch user details)`。期限切れか通信できないかは区別できない)。`relogin:true`で入り直す |
651
654
 
652
- 結果は`aiterm.agent-auth-result.v1`。`status`は`waiting`/`authenticated`/`blocked`/`failed`、`session_id`・`url`・`user_code`・`input_required`・`message`を返す。`start`のsession IDを保存して`status`へ渡す。公式HTTPS URLと明示device codeだけを返し、CLIの生出力・token・OAuth callback codeを結果へ載せない。`input_required:true`なら同じsessionの`pty_read(screen:true)`で公式画面を表示し、`pty_send`/`pty_key`で人の入力を中継する。
655
+ 結果は`aiterm.agent-auth-result.v1`。`status`は`waiting`/`authenticated`/`blocked`/`failed`、`session_id`・`url`・`user_code`・`input_required`・`message`を返す。`start`のsession IDを保存して`status`へ渡す。公式HTTPS URLと明示device codeだけを返し、CLIの生出力・token・OAuth callback codeを結果へ載せない。`auth_ui`の安全な案内だけを表示し、入力は同じbindingとprompt_digestを持つ専用respondへ渡す。生画面や通常PTY入力へ認証コードを中継しない。
653
656
 
654
- Grokには公式認証status commandが無いため、session付きの確認は公式loginのexit 0を正本にし、session無しの確認は`blocked`を返す。他harnessは公式statusも照合する。Claudeは認証後の公式初回案内を同じsessionで進め、選択待ちは`blocked`/`input_required:true`で返す。`authenticated`は認証結果であり、`agent_launch`の起動準備完了は別途確認する。`cancel`は指定した認証sessionだけを閉じ、資格情報を削除しない。
657
+ Grokには公式認証status commandが無いため、session付きの確認は公式loginのexit 0を正本にし、session無しの確認は公式`models`の認証要求を照合し、不明なら成功扱いしない。他harnessは公式statusも照合する。Claudeは認証後の公式初回案内を同じsessionで進め、選択待ちは`blocked`/`input_required:true`で返す。`authenticated`は認証結果であり、`agent_launch`の起動準備完了は別途確認する。`cancel`は指定した認証sessionだけを閉じ、資格情報を削除しない。
655
658
 
656
659
  PTYが消失した場合は、相関記録の有無にかかわらず`status`が`failed`、`cancel`が既に終了・取消済みを示す`blocked`を返し、どちらも`session_id:null`となる。保存したsession IDを解除して`start`で再開始できる。生存中の通常PTYやharness不一致、記録の破損・読取り失敗はエラーを返す。
657
660
 
@@ -691,6 +694,8 @@ handoff contextを前置きできる。この任意経路は`throughline >= 0.9.
691
694
 
692
695
  SSH先がPowerShellの場合、`mark:true`は現在の標準`PS ...>`プロンプトから方言を判定する。Aiterm自身がmacOS・Linux上でもPowerShell構文を送り、過去の出力に残ったプロンプトは判定に使わない。
693
696
 
697
+ Windows nativeで現在のGit Bashプロンプトが折り返されている時は、実pane列数と一致する表示幅の連続行だけを接続して方言を補正する。過去の入力済みcommand・別prompt・未知の幅を根拠にBashへ切り替えず、通常送信とmark送信が同じruntimeの判定を使う。
698
+
694
699
  `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`が観測する。親はブロックもポーリングもしない。
695
700
 
696
701
  ### Codex/Claude Code/Cursor親への回答自動配送
@@ -712,6 +717,8 @@ Cursor親(`clientInfo.name`が`cursor-vscode`)も同じ完了観測と本文
712
717
  Claudeをリンク経由の`cwd`から起動した場合も、実体パスに対応する会話記録を参照する。
713
718
 
714
719
 
720
+ Claudeの認証事前確認による起動拒否は従来のエラー文を保ち、`structuredContent`へ`schema:"aiterm.agent-launch-error.v1"`、`harness:"claude-code"`、`phase:"authentication_preflight"`、`effect:"not_issued"`、`session_created:false`を返す。`code`は`CLAUDE_AUTH_NOT_AUTHENTICATED`(公式statusが未認証)、`CLAUDE_AUTH_STATUS_UNCONFIRMED`(確認不能)、`CLAUDE_AUTH_STATUS_TIMEOUT`(確認が時間切れ)を区別する。資格情報の故障原因やログイン期限切れは断定せず、公式status本文・stderrは返さない。
721
+
715
722
  ### 別端末のagentを1回で起動する(`remote`)
716
723
 
717
724
  `agent_launch`に`remote`を付けると、SSHで入った別端末のAitermで同じ起動を行う。端末へ入る操作と現地のagentを起動する操作が1回で済み、親から見た完了の届き方はこの端末の子と同じになる。Linux・macOS・Windowsの端末へ同じ依頼を並行して投げる用途を想定している。
@@ -804,3 +811,6 @@ aiterm が、あなたの AI に別のエージェントへ仕事を渡させた
804
811
  ## ライセンス
805
812
 
806
813
  MIT
814
+
815
+
816
+ Claude認証は公式claude.com/cai/oauth/authorizeのPKCE要求と非秘密のcode=true表示flagを認証URLとして返す。callback認可codeやtokenは除外し、共通URL credential filterは変更しない。この認証専用差分にnative goal機能は含まない。
package/README.md CHANGED
@@ -227,7 +227,7 @@ collection is off by default and performs no network I/O. It ships via
227
227
  tag-triggered CI with npm provenance (OIDC Trusted Publishing); the GitHub
228
228
  Release re-registers the Official MCP Registry entry.
229
229
 
230
- **Status:** actively maintained · current public release **v0.61.17** · 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).
230
+ **Status:** actively maintained · current public release **v0.61.19** · 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).
231
231
 
232
232
  ### Update and rollback
233
233
 
@@ -631,7 +631,7 @@ continue to use `claude_approval`.
631
631
  | `pty_list` | Text and structured session list (each agent session carries its harness conversation ID as `vendor_session_id`), with explicitly requested non-secret environment values | `env_keys?` |
632
632
  | `pty_observe` | Pane/harness liveness, native process identity, state, and activity | `session_id`, `cursor?` |
633
633
  | `agent_launch` | Canonical agent launch; harness and model are independent | `harness`, `prompt?`, `model?`, `reasoning_effort?`, `fast?`, `cwd?`, `write_scope?`, `trust_project?`, `env_vars?`, `throughline_source_session?`, `throughline_supplement_file?` |
634
- | `agent_auth` | 公式CLIの認証を開始・確認・取消し、公式URL・device code・入力待ちを返す | `harness`, `action`, `session_id?`, `cwd?`, `env_vars?`, `relogin?` |
634
+ | `agent_auth` | 公式CLIの認証を開始・確認・取消し、公式URL・device code・入力待ちを返す | `harness`, `action`, `session_id?`, `cwd?`, `env_vars?`, `relogin?`, `expected_binding?`, `response?` |
635
635
  | `agent_models` | List the models and reasoning efforts a harness offers now, read from that harness's own catalog without sending a prompt | `harness`, `cwd?`, `include_hidden?` |
636
636
  | `agent_approval` | Inspect a Codex approval and submit a one-time approval or denial | `action`, `session_id`, `approval_choice?`, `observed_prompt_digest?` |
637
637
  | `claude_agent` / `codex_agent` / `grok_agent` | Deprecated compatibility aliases (`composer_agent` was removed in 0.41.0; run Composer with `agent_launch` on `cursor-cli`) | legacy launcher arguments |
@@ -680,7 +680,10 @@ enables collection on that host.
680
680
 
681
681
  Claude Codeの席では、`/model`の選ぶ画面の`s`(use this session only)で替える。`/model <名前>`・`/effort <値>`は選んだ値を端末の`~/.claude/settings.json`へ新しい会話の既定として保存するので、打たない(ADR 0111)。その選び方が無い版、選ぶ画面に無いmodel、そのmodelで選べないeffortは、何も変えずに断る(`CLAUDE_MODEL_SESSION_ONLY_UNAVAILABLE`・`CLAUDE_MODEL_NOT_IN_PICKER`・`CLAUDE_EFFORT_NOT_SELECTABLE`)。席を閉じて立て直すと端末の既定へ戻るので、立て直す時は`agent_launch`へ同じ`model`・`reasoning_effort`を渡す。
682
682
 
683
- `agent_auth({ harness, action:"start"|"status"|"cancel", session_id?, cwd?, env_vars?, relogin? })`は、各harnessの公式CLIで認証を進める。Claudeは`claude auth login`、CodexとGrokは`login --device-auth`、Cursorは`NO_OPEN_BROWSER=1 cursor-agent login`を使い、資格情報は各CLIだけが保存する。Aitermは資格情報を読取り・copy・編集せず、独自OAuthも実装しない。`remote`は他toolと同じ標準対応。
683
+ `agent_auth({ harness, action:"start"|"status"|"cancel"|"respond", session_id?, cwd?, env_vars?, relogin?, expected_binding?, response? })`は、各harnessの公式CLIで認証を進める。Claudeは`claude auth login`、CodexとGrokは`login --device-auth`、Cursorは`NO_OPEN_BROWSER=1 cursor-agent login`を使い、資格情報は各CLIだけが保存する。Aitermは資格情報を読取り・copy・編集せず、独自OAuthも実装しない。`remote`は他toolと同じ標準対応。
684
+
685
+ 認証UIは追加の`auth_ui`を使う。`stage`は`open_url` / `device_code` / `paste_code` / `select` / `waiting` / `complete` / `failed` / `unavailable`。公式URL・表示code・具体的な選択ラベル・入力種別をNativeが返し、`input_required`だけから入力内容や端末キーを推測しない。`respond`は直近の`auth_ui.binding`を`expected_binding`へ渡し、`expected_prompt_digest`には同じ`auth_ui.prompt_digest`を渡す。`response:{kind:"authorization_code",value}`または`{kind:"choice",id}`を同じ認証flowへ送る。認証コードを`pty_send`へ渡さない。専用入力はlastcmd・通常turn・paneログ・argv・一時fileへコードを保存せず、CLIの画面echoもpaneログへ保存しない。未発行と入力後不明は構造化errorの`effect:not_issued|unknown`で区別し、自動再送しない。legacy flowの`launch_id:null`は実native識別情報で照合し、UUIDを補わない。Windowsの専用入力は前面所有確認が未受入のため`not_issued`で拒否する。
686
+
684
687
 
685
688
  `start`は、公式の状態が認証済みなら何も起こさず`authenticated`(`session_id:null`)を返す。`relogin:true`を付けると、認証済みに見えても公式ログインを開始し、`session_id`付きの結果を返す。別のアカウントへ入り直す時と、ログインの期限切れを状態から見抜けない時に使う。Aitermは資格情報を消さず、置き換えは公式CLIが行う。ただし、公式CLIがログインを始めた時点で元のログインを消す事がある(Codex 0.160.0の`codex login --device-auth`は、始めた時点で`auth.json`を消す。途中で`cancel`しても戻らない)。Claude CodeとCursorは、途中で`cancel`すれば元の資格情報が残る(無効な資格情報で確認)。
686
689
 
@@ -693,9 +696,9 @@ Claude Codeの席では、`/model`の選ぶ画面の`s`(use this session only
693
696
  | Grok | `grok models`(状態の命令は無い) | `blocked`(`You are not authenticated.`)。Grokは使えない資格情報を自分で消す |
694
697
  | Cursor | `cursor-agent status` | `failed`(`Logged in (unable to fetch user details)`。期限切れか通信できないかは区別できない)。`relogin:true`で入り直す |
695
698
 
696
- 結果は`aiterm.agent-auth-result.v1`。`status`は`waiting`/`authenticated`/`blocked`/`failed`、`session_id`・`url`・`user_code`・`input_required`・`message`を返す。`start`のsession IDを保存して`status`へ渡す。公式HTTPS URLと明示device codeだけを返し、CLIの生出力・token・OAuth callback codeを結果へ載せない。`input_required:true`なら同じsessionの`pty_read(screen:true)`で公式画面を表示し、`pty_send`/`pty_key`で人の入力を中継する。
699
+ 結果は`aiterm.agent-auth-result.v1`。`status`は`waiting`/`authenticated`/`blocked`/`failed`、`session_id`・`url`・`user_code`・`input_required`・`message`を返す。`start`のsession IDを保存して`status`へ渡す。公式HTTPS URLと明示device codeだけを返し、CLIの生出力・token・OAuth callback codeを結果へ載せない。`auth_ui`の安全な案内だけを表示し、入力は同じbindingとprompt_digestを持つ専用respondへ渡す。生画面や通常PTY入力へ認証コードを中継しない。
697
700
 
698
- Grokには公式認証status commandが無いため、session付きの確認は公式loginのexit 0を正本にし、session無しの確認は`blocked`を返す。他harnessは公式statusも照合する。Claudeは認証後の公式初回案内を同じsessionで進め、選択待ちは`blocked`/`input_required:true`で返す。`authenticated`は認証結果であり、`agent_launch`の起動準備完了は別途確認する。`cancel`は指定した認証sessionだけを閉じ、資格情報を削除しない。
701
+ Grokには公式認証status commandが無いため、session付きの確認は公式loginのexit 0を正本にし、session無しの確認は公式`models`の認証要求を照合し、不明なら成功扱いしない。他harnessは公式statusも照合する。Claudeは認証後の公式初回案内を同じsessionで進め、選択待ちは`blocked`/`input_required:true`で返す。`authenticated`は認証結果であり、`agent_launch`の起動準備完了は別途確認する。`cancel`は指定した認証sessionだけを閉じ、資格情報を削除しない。
699
702
 
700
703
  PTYが消失した場合は、相関記録の有無にかかわらず`status`が`failed`、`cancel`が既に終了・取消済みを示す`blocked`を返し、どちらも`session_id:null`となる。保存したsession IDを解除して`start`で再開始できる。生存中の通常PTYやharness不一致、記録の破損・読取り失敗はエラーを返す。
701
704
 
@@ -723,6 +726,8 @@ The result (`aiterm.agent-models.v1`) is `{ harness, source, harness_version, de
723
726
 
724
727
  The selected harness CLI must be installed and authenticated. Use each product owner's official installer and updater; Aiterm does not distribute alternate CLI tarballs. For Cursor Agent CLI, use `curl https://cursor.com/install -fsS | bash` on macOS/Linux/WSL or `irm 'https://cursor.com/install?win32=true' | iex` on native Windows, authenticate once with `agent login`, and update with `agent update`; Aiterm invokes the unambiguous `cursor-agent` binary. Missing binaries, invalid model/effort values, unavailable Grok catalog models, and nonexistent `cwd` fail before a session exists.
725
728
 
729
+ Claude authentication preflight refusals retain the existing error text and add `structuredContent` with schema `aiterm.agent-launch-error.v1`, `harness:"claude-code"`, `phase:"authentication_preflight"`, `effect:"not_issued"`, and `session_created:false`. `code` distinguishes `CLAUDE_AUTH_NOT_AUTHENTICATED`, `CLAUDE_AUTH_STATUS_UNCONFIRMED`, and `CLAUDE_AUTH_STATUS_TIMEOUT`. These describe the official status check, not the cause of a credential failure or a confirmed login expiry. Native status bodies and stderr are omitted.
730
+
726
731
  Set `throughline_source_session` together with a non-empty mission in `prompt` to prepend
727
732
  Throughline's read-only handoff context. This optional route requires `throughline >= 0.9.0`,
728
733
  cannot be combined with `launch_operation_id`, and leaves the source session's database ownership
@@ -736,6 +741,8 @@ When an agent's answer is longer than the on-screen tail (pane height ≈ 24 lin
736
741
 
737
742
  For PowerShell over SSH, `mark:true` recognizes the current standard `PS ...>` prompt and emits PowerShell syntax even when Aiterm runs on macOS or Linux. A prompt left in earlier output is not used to select the syntax.
738
743
 
744
+ On native Windows, a wrapped current Git Bash prompt is reconciled with the measured pane columns before choosing shell syntax. Only full-width continuation rows of the current empty prompt are joined; earlier commands, other prompts and unknown widths do not authorize a correction. Ordinary and marked sends use the same runtime decision.
745
+
739
746
  `pty_read({ wait: true })` decides "is the command done?" via five layers: process exit / a `mark:true` sentinel / an `until` match / output quiescence with shell return / timeout. `mark` emits the shell's exit status on POSIX shells and `0` (success) or `1` (failure) on PowerShell; fish/csh/tcsh are rejected before send because they do not share either status syntax. When `mark` or `until` is active, that requested evidence takes precedence and a momentarily quiet shell cannot complete the read as quiescent. Agent sessions add a sixth exact layer: Codex observes normal rollout `task_complete`; Grok observes normal session `turn_ended`; Claude observes its additive launch-correlated Stop event; Cursor observes `turn_ended(status:"success")` in the launch-bound normal agent transcript. `aiterm-wait --cursor` performs that harness-specific observation without the parent blocking or polling. Pre-send readiness failures are MCP errors, and late completion remains recoverable without resending.
740
747
 
741
748
  ### Completion push for parent agents (`aiterm-wait`)
@@ -875,3 +882,6 @@ started the persistent multiplexer server (since 0.50.0). Every harness still ac
875
882
  ## License
876
883
 
877
884
  MIT
885
+
886
+
887
+ Claude authentication URL receipts recognize the official claude.com/cai/oauth/authorize PKCE authorization request and its nonsecret code=true display flag. Callback authorization codes and tokens remain excluded; the common URL credential filter is unchanged. This auth-only patch contains no native goal feature.
@@ -0,0 +1,52 @@
1
+ import { createHash } from "node:crypto";
2
+ import { AitermError } from "./errors.js";
3
+ import { z } from "zod";
4
+ // Existing native identities, not an application flow ID. Legacy launch_id stays null.
5
+ export const authBindingSchema = z.object({
6
+ session_id: z.string(), launch_id: z.string().nullable(),
7
+ metadata_identity: z.string(), native_id: z.string(), pane_id: z.string(),
8
+ server_pid: z.number().int().positive(), server_started: z.string(),
9
+ pane_pid: z.number().int().positive(), pane_started: z.string(),
10
+ }).strict();
11
+ export const authResponseSchema = z.discriminatedUnion("kind", [
12
+ z.object({ kind: z.literal("authorization_code"), value: z.string().min(1).max(8192).regex(/^[!-~]+$/) }).strict(),
13
+ z.object({ kind: z.literal("choice"), id: z.string().regex(/^option-[1-9]$/) }).strict(),
14
+ ]);
15
+ export const authUiSchema = z.object({
16
+ schema: z.literal("aiterm.auth-ui.v1"),
17
+ stage: z.enum(["open_url", "device_code", "paste_code", "select", "waiting", "complete", "failed", "unavailable"]),
18
+ title: z.string(), message: z.string(), binding: authBindingSchema.nullable(),
19
+ url: z.string().nullable(), user_code: z.string().nullable(),
20
+ input: z.enum(["authorization_code", "choice"]).nullable(),
21
+ prompt_digest: z.string().nullable(),
22
+ choices: z.array(z.object({ id: z.string(), label: z.string() }).strict()),
23
+ }).strict();
24
+ export function authPromptDigest(pane) {
25
+ return pane.ui ? createHash("sha256").update(JSON.stringify({ ui: pane.ui, url: pane.url })).digest("hex") : null;
26
+ }
27
+ export function authUi(result, pane, binding) {
28
+ const base = { schema: "aiterm.auth-ui.v1", binding, url: result.url, user_code: result.user_code,
29
+ choices: [], input: null, prompt_digest: pane ? authPromptDigest(pane) : null };
30
+ if (result.status === "authenticated")
31
+ return { ...base, stage: "complete", title: "認証完了", message: "公式CLIで認証完了を確認しました。" };
32
+ if (result.status === "failed")
33
+ return { ...base, stage: "failed", title: "認証を完了できません", message: result.message ?? "公式CLIの状態を確認してください。" };
34
+ if (pane?.ui && binding)
35
+ return { ...base, ...pane.ui };
36
+ if (binding && result.url)
37
+ return { ...base, stage: result.user_code ? "device_code" : "open_url", title: "公式認証ページを開く",
38
+ message: result.user_code ? "公式ページで表示のデバイスコードを入力してください。" : "公式ページで認証し、この画面で完了をお待ちください。" };
39
+ return { ...base, stage: result.input_required ? "unavailable" : "waiting", title: "認証状態を確認中",
40
+ message: result.input_required ? "公式画面の入力段階を安全に識別できません。入力は送信できません。" : "公式CLIの認証結果を待っています。" };
41
+ }
42
+ export class AuthResponseError extends AitermError {
43
+ reason_code;
44
+ effect;
45
+ constructor(reason_code, effect, message) {
46
+ super(`${reason_code}: ${message}`, 2);
47
+ this.reason_code = reason_code;
48
+ this.effect = effect;
49
+ }
50
+ }
51
+ export const authResponseFailureSchema = z.object({ schema: z.literal("aiterm.auth-response-error.v1"),
52
+ harness: z.string(), session_id: z.string().nullable(), code: z.string(), effect: z.enum(["not_issued", "unknown"]), message: z.string() }).strict();
package/dist/core.js CHANGED
@@ -27,8 +27,10 @@ import { unfinishedDeliveriesOwnedBy } from "./parent-delivery-owners.js";
27
27
  import { readRuntimeProcesses, readProcessIdentities, processSubtree, parentProcess, processIdentity, backgroundProcesses, lazyRelayProcess, aitermProcessProbe, windowsConsoleHost, processElapsedSeconds } from "./process-runtime.js";
28
28
  import { liveNodeExecutable } from "./live-node.js";
29
29
  import { AitermError, telemetryOwnedFailure, ownTelemetryFailure } from "./errors.js";
30
- import { isWin, SOCKDIR, SOCK, tmuxCommand, sendPsmuxPayload, loadPtyBufferChunk, pasteBufferBaseArgs, tmuxNewSession, openPsmuxServerToDesktop, registeredSessionEnvironment, attachCommand, normalizePaneCommand, atomicShellMultiline, appendMarkSentinel, paneShellForSubmission, adaptShellSubmission, settlePaneLog, paneCwdArgument, sessionEnvironmentLaunch, } from "./tmux-runtime.js";
30
+ import { isWin, SOCKDIR, SOCK, tmuxCommand, tmuxCommandWithInput, sendPsmuxPayload, loadPtyBufferChunk, pasteBufferBaseArgs, tmuxNewSession, openPsmuxServerToDesktop, registeredSessionEnvironment, attachCommand, normalizePaneCommand, atomicShellMultiline, appendMarkSentinel, paneShellForSubmission, adaptShellSubmission, settlePaneLog, paneCwdArgument, sessionEnvironmentLaunch, } from "./tmux-runtime.js";
31
31
  import { AGENT_DEFAULT_PREFACE, sleep, currentUid, runtimeStateBase, safeStatSize, readFileRange, writeJson0600, createEmpty0600, shq, LAUNCH_ID_RE, AGENT_DONE_POLL_MS, AGENT_RATE_LIMIT_POLL_MS, pollGate, AGENT_EVENT_MAX_BYTES, assertSessionName, agentsDir, agentEventPath, agentMetadataPath, writeAgentMetadata, AGENT_EVENT_TAIL_BYTES, agentLabel, agentHarness, subagentInstruction, agentLineageFields, } from "./agent-shared.js";
32
+ import { AuthResponseError, authUi, authPromptDigest, authBindingSchema, authResponseSchema } from "./agent-auth-ui.js";
33
+ import { processStartIdentity } from "./runtime-error-os.js";
32
34
  import { catalogUnavailable } from "./model-catalog.js";
33
35
  import { FastModeError, fastModeFailure } from "./fast-mode.js";
34
36
  import { readFileOrNull, restoreJsonSetting, withUserConfigLock } from "./user-config.js";
@@ -4357,6 +4359,131 @@ function authSessionEnvironment(name, keys) {
4357
4359
  }
4358
4360
  return env;
4359
4361
  }
4362
+ /** Dedicated authentication panes never persist terminal output, including a CLI echo. */
4363
+ function disableAuthLogging(name) {
4364
+ if (tmux("pipe-pane", "-t", name).code !== 0)
4365
+ throw new AitermError("AGENT_AUTH_LOGGING_UNCONFIRMED: 認証入力を送信していません。", 2);
4366
+ }
4367
+ function readAuthBinding(name, kind) {
4368
+ const meta = loadAuthMetadata(name, kind);
4369
+ if (!meta)
4370
+ return null;
4371
+ const stat = fs.statSync(authMetadataPath(name));
4372
+ const native = tmux("display-message", "-p", "-t", `=${name}:`, "#{pid}\t#{session_id}\t#{pane_id}\t#{pane_pid}\t#{pane_dead}");
4373
+ if (native.code !== 0)
4374
+ return null;
4375
+ const [server, session, pane, pid, dead] = native.stdout.trim().split("\t");
4376
+ if (dead !== "0" || !/^\$\d+$/.test(session ?? "") || !/^%\d+$/.test(pane ?? ""))
4377
+ return null;
4378
+ const serverStarted = processStartIdentity(Number(server), process.platform);
4379
+ const paneStarted = processStartIdentity(Number(pid), process.platform);
4380
+ if (!serverStarted || !paneStarted)
4381
+ return null;
4382
+ return authBindingSchema.parse({ session_id: name, launch_id: meta.launch_id ?? null,
4383
+ metadata_identity: `${stat.dev}:${stat.ino}:${stat.birthtimeMs}:${stat.mtimeMs}`, native_id: session, pane_id: pane,
4384
+ server_pid: Number(server), server_started: serverStarted, pane_pid: Number(pid), pane_started: paneStarted });
4385
+ }
4386
+ async function respondAgentAuth(kind, options) {
4387
+ const name = options.session_id;
4388
+ const expected = authBindingSchema.safeParse(options.expected_binding);
4389
+ const response = authResponseSchema.safeParse(options.response);
4390
+ if (!name || !expected.success || !response.success || expected.data.session_id !== name || !/^[a-f0-9]{64}$/.test(options.expected_prompt_digest ?? ""))
4391
+ throw new AuthResponseError("AGENT_AUTH_RESPONSE_INVALID", "not_issued", "入力と対象の指定が不正です。入力は未送信です。");
4392
+ const release = acquireSessionSendFileLock(name);
4393
+ let issued = false;
4394
+ try {
4395
+ const check = () => {
4396
+ const binding = readAuthBinding(name, kind);
4397
+ if (!binding || JSON.stringify(binding) !== JSON.stringify(expected.data))
4398
+ throw new AitermError("AGENT_AUTH_STALE_BINDING: 認証の世代が一致しません。", 2);
4399
+ // The current native process, not a shell/background job, must own the foreground tty.
4400
+ if (!isWin) {
4401
+ const checkedProcess = spawnSync("/bin/ps", ["-p", String(binding.pane_pid), "-o", "stat="], { encoding: "utf8", timeout: 1000 });
4402
+ if (checkedProcess.status !== 0 || !checkedProcess.stdout.includes("+") || /^T/.test(checkedProcess.stdout.trim()))
4403
+ throw new AitermError("AGENT_AUTH_INPUT_UNAVAILABLE: 公式認証processが端末の前面にありません。", 2);
4404
+ }
4405
+ else {
4406
+ // Native psmux transport exists, but foreground ownership has not yet been accepted on Windows.
4407
+ throw new AitermError("AGENT_AUTH_INPUT_UNAVAILABLE: Windows認証入力の所有確認が未対応です。", 2);
4408
+ }
4409
+ return binding;
4410
+ };
4411
+ const binding = check();
4412
+ const meta = loadAuthMetadata(name, kind);
4413
+ let pane = authPane(meta, stripControl(captureScreen(name, 0)));
4414
+ const answer = response.data;
4415
+ if (authPromptDigest(pane) !== options.expected_prompt_digest)
4416
+ throw new AitermError("AGENT_AUTH_STAGE_MISMATCH: 入力画面が変わりました。", 2);
4417
+ if (answer.kind === "authorization_code" && pane.ui?.input !== "authorization_code")
4418
+ throw new AitermError("AGENT_AUTH_STAGE_MISMATCH: 認証コードの入力待ちではありません。", 2);
4419
+ if (answer.kind === "choice" && !pane.ui?.choices.some(choice => choice.id === answer.id))
4420
+ throw new AitermError("AGENT_AUTH_STAGE_MISMATCH: 表示された選択肢と一致しません。", 2);
4421
+ disableAuthLogging(binding.pane_id);
4422
+ check();
4423
+ if (answer.kind === "authorization_code") {
4424
+ const buffer = `auth-${process.pid}-${randomBytes(8).toString("hex")}`;
4425
+ try {
4426
+ if (tmuxCommandWithInput(true, answer.value, "load-buffer", "-b", buffer, "-").code !== 0)
4427
+ throw new AitermError("AGENT_AUTH_INPUT_UNAVAILABLE: 入力を準備できません。", 2);
4428
+ check();
4429
+ // Fresh phase and empty field immediately before the first native input effect.
4430
+ pane = authPane(meta, stripControl(captureScreen(name, 0)));
4431
+ if (pane.ui?.input !== "authorization_code" || authPromptDigest(pane) !== options.expected_prompt_digest)
4432
+ throw new AitermError("AGENT_AUTH_STAGE_MISMATCH: 入力段階が変わりました。", 2);
4433
+ issued = true;
4434
+ if (tmux("paste-buffer", "-d", "-r", "-b", buffer, "-t", binding.pane_id).code !== 0)
4435
+ throw new Error("native input unconfirmed");
4436
+ check();
4437
+ if (tmux("send-keys", "-t", binding.pane_id, "Enter").code !== 0)
4438
+ throw new Error("native submit unconfirmed");
4439
+ }
4440
+ finally {
4441
+ tmux("delete-buffer", "-b", buffer);
4442
+ }
4443
+ }
4444
+ else {
4445
+ // Move only over freshly observed choices. Never expose raw keys to the host UI.
4446
+ for (let step = 0; step < 8; step++) {
4447
+ check();
4448
+ const screen = stripControl(captureScreen(name, 0));
4449
+ pane = authPane(meta, screen);
4450
+ if (authPromptDigest(pane) !== options.expected_prompt_digest || !pane.ui?.choices.some(choice => choice.id === answer.id))
4451
+ throw new Error("native choice changed");
4452
+ const current = pane.native_input?.kind === "choice" ? pane.native_input.selected_id : null;
4453
+ if (!current)
4454
+ throw new Error("native selection unknown");
4455
+ issued = true;
4456
+ if (tmux("send-keys", "-t", binding.pane_id, current === answer.id ? "Enter" : "Down").code !== 0)
4457
+ throw new Error("native choice unconfirmed");
4458
+ if (current === answer.id)
4459
+ break;
4460
+ if (step === 7)
4461
+ throw new Error("native choice unconfirmed");
4462
+ await sleep(80);
4463
+ }
4464
+ }
4465
+ }
4466
+ catch (error) {
4467
+ if (issued)
4468
+ throw new AuthResponseError("AGENT_AUTH_RESPONSE_UNKNOWN", "unknown", "入力後の結果を確認できません。自動再送しないでください。");
4469
+ if (error instanceof AitermError)
4470
+ throw new AuthResponseError(/^AGENT_AUTH_[A-Z_]+/.exec(error.message)?.[0] ?? "AGENT_AUTH_RESPONSE_NOT_ISSUED", "not_issued", error.message);
4471
+ throw new AuthResponseError("AGENT_AUTH_RESPONSE_NOT_ISSUED", "not_issued", "入力を送信できません。");
4472
+ }
4473
+ finally {
4474
+ release();
4475
+ }
4476
+ await sleep(100);
4477
+ try {
4478
+ const result = await authenticateAgent(kind, { action: "status", session_id: name });
4479
+ if (result.auth_ui?.input === "authorization_code" && response.data.kind === "authorization_code")
4480
+ throw new Error("native input consumption unconfirmed");
4481
+ return result;
4482
+ }
4483
+ catch {
4484
+ throw new AuthResponseError("AGENT_AUTH_RESPONSE_UNKNOWN", "unknown", "入力後の状態を確認できません。自動再送しないでください。");
4485
+ }
4486
+ }
4360
4487
  function launchAuthProcess(name, meta) {
4361
4488
  const plan = authPlan(meta.kind, meta.phase);
4362
4489
  const env = plan.env.map(([key, value]) => shq(`${key}=${value}`));
@@ -4371,9 +4498,18 @@ function launchAuthProcess(name, meta) {
4371
4498
  export async function authenticateAgent(kind, options) {
4372
4499
  if (options.action !== "status" || options.session_id !== undefined)
4373
4500
  assertExecutionContext(options.session_id);
4374
- const result = (status, session, message, pane) => ({ schema: "aiterm.agent-auth-result.v1", harness: agentHarness(kind),
4375
- status, session_id: session, url: pane?.url ?? null, user_code: pane?.user_code ?? null,
4376
- input_required: pane?.input_required ?? false, message: message ?? pane?.message ?? null });
4501
+ const result = (status, session, message, pane) => {
4502
+ const receipt = { schema: "aiterm.agent-auth-result.v1", harness: agentHarness(kind), status,
4503
+ session_id: session, url: pane?.url ?? null, user_code: pane?.user_code ?? null,
4504
+ input_required: pane?.input_required ?? false, message: message ?? pane?.message ?? null };
4505
+ receipt.auth_ui = authUi(receipt, pane, session ? readAuthBinding(session, kind) : null);
4506
+ if (isWin && receipt.auth_ui.input !== null)
4507
+ receipt.auth_ui = { ...receipt.auth_ui, stage: "unavailable", input: null, choices: [],
4508
+ message: "Windowsの認証入力は端末の前面所有の受入が未完了です。入力は送信できません。" };
4509
+ return receipt;
4510
+ };
4511
+ if (options.action === "respond")
4512
+ return respondAgentAuth(kind, options);
4377
4513
  if (options.action === "cancel") {
4378
4514
  if (!options.session_id)
4379
4515
  throw new AitermError("AGENT_AUTH_SESSION_REQUIRED: cancelにはsession_idが必要です。", 2);
@@ -4411,8 +4547,9 @@ export async function authenticateAgent(kind, options) {
4411
4547
  const kept = tmux("set-option", "-t", name, "remain-on-exit", "on");
4412
4548
  if (kept.code !== 0)
4413
4549
  throw new AitermError("AGENT_AUTH_PANE_CONFIG_FAILED: 認証processの終了観測を設定できません。", 2);
4550
+ disableAuthLogging(name);
4551
+ meta.launch_id = randomBytes(16).toString("hex");
4414
4552
  if (hasExecutionCreationScope()) {
4415
- meta.launch_id = randomBytes(16).toString("hex");
4416
4553
  meta.execution_native = executionCreatedNativeBinding(name);
4417
4554
  }
4418
4555
  withExecutionPreparation(name, () => writeJson0600(authMetadataPath(name), meta));
@@ -4452,7 +4589,7 @@ export async function authenticateAgent(kind, options) {
4452
4589
  if (observed.code !== 0)
4453
4590
  return result("failed", session, "認証processの終了状態を取得できません。");
4454
4591
  const [dead, exit] = observed.stdout.trim().split("\t");
4455
- const pane = authPane(meta, stripControl(captureScreen(session, 120)));
4592
+ const pane = authPane(meta, stripControl(captureScreen(session, 0)));
4456
4593
  if (dead === "1") {
4457
4594
  if (exit !== "0")
4458
4595
  return result("failed", session, `公式認証processが失敗しました(exit=${/^\d+$/.test(exit ?? "") ? exit : "unknown"})。`);
package/dist/errors.js CHANGED
@@ -7,6 +7,16 @@ export class AitermError extends Error {
7
7
  this.code = code;
8
8
  }
9
9
  }
10
+ /** A harness authentication check refused launch before any session creation.
11
+ * Keep the native CLI's status body and credential diagnostics out of the wire. */
12
+ export class AgentAuthenticationPreflightError extends AitermError {
13
+ reasonCode;
14
+ constructor(reasonCode, message) {
15
+ super(message, 2);
16
+ this.reasonCode = reasonCode;
17
+ this.name = "AgentAuthenticationPreflightError";
18
+ }
19
+ }
10
20
  export class TelemetryOwnedError extends AitermError {
11
21
  telemetryCode;
12
22
  constructor(message, code, telemetryCode, cause) {
@@ -7,7 +7,7 @@ import * as os from "node:os";
7
7
  import * as path from "node:path";
8
8
  import { createHash, randomBytes, randomUUID } from "node:crypto";
9
9
  import { fileURLToPath } from "node:url";
10
- import { AitermError } from "../errors.js";
10
+ import { AitermError, AgentAuthenticationPreflightError } from "../errors.js";
11
11
  import { modeBitsWorldAccessible } from "../tmux-runtime.js";
12
12
  import { runAgentProtocolCommand, spawnAgentControlCommand } from "../agent-resolver.js";
13
13
  import { catalogInvalid, catalogUnavailable, checkedCatalog, findJsonLine, processSummary } from "../model-catalog.js";
@@ -289,13 +289,13 @@ export function assertClaudeAuthenticationReady(bin) {
289
289
  typeof status === "object" &&
290
290
  !Array.isArray(status) &&
291
291
  status.loggedIn === false) {
292
- throw new AitermError("Claude Codeの認証を利用できません。sessionは作成していません。" +
292
+ throw new AgentAuthenticationPreflightError("CLAUDE_AUTH_NOT_AUTHENTICATED", "Claude Codeの認証を利用できません。sessionは作成していません。" +
293
293
  "通常端末で `claude doctor` を実行し、Keychain/credential storeを直してから一度だけ `claude auth login` を実行してください。" +
294
- "aiterm相関付きClaude session内で /login を繰り返さないでください。", 2);
294
+ "aiterm相関付きClaude session内で /login を繰り返さないでください。");
295
295
  }
296
296
  const timedOut = result.error && result.error.code === "ETIMEDOUT";
297
- throw new AitermError(`Claude Codeの認証状態を起動前に確認できません${timedOut ? "(5秒でtimeout)" : ""}。sessionは作成していません。` +
298
- "`claude auth status --json` と `claude doctor` が成功することを通常端末で確認してください。", 2);
297
+ throw new AgentAuthenticationPreflightError(timedOut ? "CLAUDE_AUTH_STATUS_TIMEOUT" : "CLAUDE_AUTH_STATUS_UNCONFIRMED", `Claude Codeの認証状態を起動前に確認できません${timedOut ? "(5秒でtimeout)" : ""}。sessionは作成していません。` +
298
+ "`claude auth status --json` と `claude doctor` が成功することを通常端末で確認してください。");
299
299
  }
300
300
  /**
301
301
  * 起動時に席へ渡す、Aitermの届け方の説明(ADR 0116)。Claude Codeは貼り付けを包み、包みの中の指示を利用者の言葉と分けて読む。
@@ -704,13 +704,88 @@ export function claudeAuthStatus(bin, cwd, env = process.env) {
704
704
  return { status: "unauthenticated", message: null };
705
705
  return { status: "failed", message: "Claude Codeの公式認証状態の確認に失敗しました。" };
706
706
  }
707
+ /** Current official Claude authorization request. `code=true` requests a
708
+ * displayed authorization code; a callback's `code=<credential>` remains
709
+ * private. Keep this exception at the vendor owner, not the shared filter. */
710
+ export function claudeAuthorizationUrl(screen) {
711
+ const legacy = authUrl(screen, ["claude.ai", "console.anthropic.com", "platform.claude.com"]);
712
+ if (legacy)
713
+ return legacy;
714
+ for (const match of screen.matchAll(/https:\/\/[^\s<>"']+/gu)) {
715
+ const candidate = match[0].replace(/[).,;]+$/u, "");
716
+ let url;
717
+ try {
718
+ url = new URL(candidate);
719
+ }
720
+ catch {
721
+ continue;
722
+ }
723
+ if (url.hostname !== "claude.com" || url.pathname !== "/cai/oauth/authorize"
724
+ || url.searchParams.getAll("code").length !== 1 || url.searchParams.get("code") !== "true"
725
+ || url.searchParams.get("response_type") !== "code"
726
+ || url.searchParams.get("code_challenge_method") !== "S256"
727
+ || !/^[A-Za-z0-9_-]{43}$/u.test(url.searchParams.get("code_challenge") ?? ""))
728
+ continue;
729
+ url.searchParams.delete("code");
730
+ if (authUrl(url.href, ["claude.com"]) !== null)
731
+ return candidate;
732
+ }
733
+ return null;
734
+ }
735
+ /** One current-phase parse owns UI choices, selected item and composer occupancy. */
736
+ export function claudeAuthInput(screen) {
737
+ const lines = screen.split("\n");
738
+ let phase = null;
739
+ for (const [at, line] of lines.entries()) {
740
+ if (line.includes("Choose the text style that looks best with your terminal"))
741
+ phase = { kind: "theme", at };
742
+ else if (line.includes("Select login method:"))
743
+ phase = { kind: "login", at };
744
+ else if (/^\s*(?:Paste (?:the )?code|Enter (?:the )?(?:authorization )?code)/i.test(line))
745
+ phase = { kind: "code", at };
746
+ else if (/Press (?:Enter|Return)|do you trust|trust this folder/i.test(line))
747
+ phase = { kind: "unknown", at };
748
+ }
749
+ if (!phase || phase.kind === "unknown")
750
+ return {};
751
+ if (phase.kind === "code") {
752
+ const empty = /^(?:Paste (?:the )?code(?: here)?(?: if prompted)?|Enter (?:the )?(?:authorization )?code)\s*[:>]?\s*$/i.test(lines[phase.at].trim())
753
+ && lines.slice(phase.at + 1).every(line => !line.trim());
754
+ return { native_input: { kind: "authorization_code", empty }, ...(empty ? { ui: {
755
+ stage: "paste_code", title: "公式ページの認証コードを入力", message: "公式ページを開いて認証し、表示されたコードをこの画面の伏せ字欄へ貼り付けてください。",
756
+ input: "authorization_code", choices: [],
757
+ } } : {}) };
758
+ }
759
+ const theme = phase.kind === "theme";
760
+ const rows = [];
761
+ for (const line of lines.slice(phase.at + 1)) {
762
+ const match = /^\s*(❯)?\s*([1-7])\.\s*(.+?)\s*$/.exec(line);
763
+ if (!match) {
764
+ // A later composer/cursor or unrecognised numbered row is not this menu.
765
+ if (/^\s*❯|^\s*\d+[.)]/.test(line))
766
+ return {};
767
+ continue;
768
+ }
769
+ const allowed = theme ? /^(?:Auto \(match terminal\)|(?:Dark|Light) mode(?: \([^\n]+\))?)$/ : /^(?:Claude account with subscription|Anthropic Console account|3rd-party platform)$/;
770
+ if (!allowed.test(match[3]))
771
+ return {};
772
+ rows.push({ id: `option-${match[2]}`, label: match[3], cursor: !!match[1] });
773
+ }
774
+ const selected = rows.filter(row => row.cursor);
775
+ if (!rows.length || selected.length !== 1 || new Set(rows.map(row => row.id)).size !== rows.length)
776
+ return {};
777
+ return { native_input: { kind: "choice", selected_id: selected[0].id }, ui: {
778
+ stage: "select", title: theme ? "表示テーマを選ぶ" : "Claudeのログイン方法を選ぶ", message: "公式CLIに表示された選択肢を選んでください。",
779
+ input: "choice", choices: rows.map(({ id, label }) => ({ id, label })),
780
+ } };
781
+ }
707
782
  export function claudeAuthPane(screen, onboarding = false) {
708
- const url = authUrl(screen, ["claude.ai", "console.anthropic.com", "platform.claude.com"]);
783
+ const url = claudeAuthorizationUrl(screen);
709
784
  const input = /Paste (?:the )?code|Enter (?:the )?(?:authorization )?code|Choose the text style|Select login method:|Press (?:Enter|Return)|do you trust|trust this folder/i.test(screen);
710
785
  const complete = onboarding && (claudeTuiReady(screen) || /Is this a project you created or one you trust/.test(screen));
711
786
  return { url, user_code: null, input_required: input && !complete,
712
787
  message: complete ? null : input ? "Claude Codeの公式画面で入力または初回案内の選択を完了してください。" : null,
713
- onboarding_complete: complete };
788
+ onboarding_complete: complete, ...(complete ? {} : claudeAuthInput(screen)) };
714
789
  }
715
790
  /** `/fast on|off` を送る前の画面と後の画面から、増えた結果の行を読む。まだ出ていなければnull。 */
716
791
  export function claudeFastResult(before, after) {
package/dist/index.js CHANGED
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env node
2
+ import { AuthResponseError, authUiSchema, authBindingSchema, authResponseSchema } from "./agent-auth-ui.js";
3
+ import { AgentAuthenticationPreflightError } from "./errors.js";
2
4
  /**
3
5
  * aiterm-mcp — AI が握るローカル永続端末を stdio MCP サーバとして公開する(Node/TS 版)。
4
6
  *
@@ -169,6 +171,12 @@ export function createAitermServer(options = {}) {
169
171
  }
170
172
  function fail(e) {
171
173
  const msg = e instanceof Error ? e.message : String(e);
174
+ if (e instanceof AgentAuthenticationPreflightError)
175
+ return {
176
+ content: [{ type: "text", text: "aiterm: " + msg }], isError: true,
177
+ structuredContent: { schema: "aiterm.agent-launch-error.v1", harness: "claude-code", code: e.reasonCode,
178
+ phase: "authentication_preflight", effect: "not_issued", session_created: false, message: msg },
179
+ };
172
180
  return { content: [{ type: "text", text: "aiterm: " + msg }], isError: true };
173
181
  }
174
182
  /**
@@ -882,14 +890,17 @@ export function createAitermServer(options = {}) {
882
890
  });
883
891
  registerRemoteAwareTool("agent_auth", {
884
892
  description: "各harnessの公式CLI認証を開始・確認・取消する。CLIごとのコマンドと認証URL/codeの抽出はAitermが所有し、資格情報は各CLIだけが保存する。" +
885
- "waitingのURL/codeを人へ表示し、input_requiredなら同じsessionのpty_send/pty_keyで公式画面へ入力する。" +
893
+ "auth_uiの段階と公式URL・選択肢を表示する。入力はrespondで同じbindingへ送り、pty_send/pty_keyへ認証コードを渡さない。" +
886
894
  "authenticatedは認証の結果であり、agent_launchの起動準備完了とは別。remoteは他toolと同じ標準対応。" +
887
895
  "startは公式の状態が認証済みなら何も起こさずauthenticatedを返す。relogin:trueを付けると、認証済みに見えても公式ログインを開始する" +
888
896
  "(別のアカウントへ入り直す時、ログインの期限切れを状態から見抜けない時)。Aitermは資格情報を消さず、置き換えは公式CLIが行う。" +
889
897
  "Codexの公式ログインは、始めた時点で元のログインを消す(途中でcancelしても戻らない)。",
890
898
  inputSchema: {
891
899
  harness: z.enum(["claude-code", "codex-cli", "grok-cli", "cursor-cli"]),
892
- action: z.enum(["start", "status", "cancel"]),
900
+ action: z.enum(["start", "status", "cancel", "respond"]),
901
+ expected_binding: authBindingSchema.optional(),
902
+ expected_prompt_digest: z.string().regex(/^[a-f0-9]{64}$/).optional(),
903
+ response: authResponseSchema.optional(),
893
904
  session_id: z.string().regex(/^[A-Za-z0-9_-]{1,64}$/).optional(),
894
905
  cwd: z.string().optional().describe("公式CLIを実行する作業ディレクトリの絶対パス"),
895
906
  env_vars: z.array(z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/)).optional().describe("startで認証PTYへ引き継ぐ環境変数名。値はreceiptへ返さない"),
@@ -897,6 +908,7 @@ export function createAitermServer(options = {}) {
897
908
  },
898
909
  outputSchema: {
899
910
  schema: z.literal("aiterm.agent-auth-result.v1"),
911
+ auth_ui: authUiSchema.optional(),
900
912
  harness: z.enum(["claude-code", "codex-cli", "grok-cli", "cursor-cli"]),
901
913
  status: z.enum(["waiting", "authenticated", "blocked", "failed"]),
902
914
  session_id: z.string().nullable(),
@@ -911,6 +923,11 @@ export function createAitermServer(options = {}) {
911
923
  return { content: [{ type: "text", text: JSON.stringify(result) }], structuredContent: { ...result } };
912
924
  }
913
925
  catch (error) {
926
+ if (error instanceof AuthResponseError)
927
+ return { ...fail(error), structuredContent: {
928
+ schema: "aiterm.auth-response-error.v1", harness, session_id: options.session_id ?? null,
929
+ code: error.reason_code, effect: error.effect, message: error.message,
930
+ } };
914
931
  return fail(error);
915
932
  }
916
933
  });
@@ -7,6 +7,7 @@ import * as path from "node:path";
7
7
  import * as os from "node:os";
8
8
  import { createHash } from "node:crypto";
9
9
  import { fileURLToPath } from "node:url";
10
+ import stringWidth from 'string-width';
10
11
  import { AitermError, ptyDependencyError } from "./errors.js";
11
12
  import { guardTmuxExecution, beforeExecutionEffect, hasExecutionCreationScope, prepareExecutionCreation, captureExecutionCreatedNative } from "./execution-effect.js";
12
13
  import { ExecutionError } from "./execution-wire.js";
@@ -605,7 +606,7 @@ export function atomicShellMultiline(text, foreground) {
605
606
  // printf %bで本文を復元する。末尾LFの除去はpty_sendのsubmit Enterと同値。
606
607
  return `eval "$(command printf '%b' '${quoted}')"`;
607
608
  }
608
- export function markShellCommand(foreground, screen, platform = process.platform) {
609
+ export function markShellCommand(foreground, screen, platform = process.platform, columns) {
609
610
  if (platform !== "win32" && foreground !== "ssh")
610
611
  return foreground;
611
612
  // psmuxの前面名はprocess snapshotやOSCの古い値を返しうる。
@@ -615,6 +616,22 @@ export function markShellCommand(foreground, screen, platform = process.platform
615
616
  return "pwsh";
616
617
  if (platform === "win32" && /(?:^|\n)[^\r\n]*\b(?:MINGW(?:32|64)|UCRT64|MSYS)\b[^\r\n]*\r?\n\$\s*$/.test(current))
617
618
  return "bash";
619
+ if (platform === "win32" && columns !== undefined && Number.isSafeInteger(columns) && columns > 0 && columns <= 10000) {
620
+ const lines = current.split(/\r?\n/u);
621
+ if (/^\$\s*$/u.test(lines.at(-1) ?? "")) {
622
+ let at = lines.length - 2, header = lines[at] ?? "";
623
+ // psmux may retain soft wraps even with capture-pane -J. Reconstruct
624
+ // only this trailing prompt's full-width rows, stopping at any prior
625
+ // command/prompt boundary. Never join arbitrary old output lines.
626
+ while (at > 0 && !/\b(?:MINGW(?:32|64)|UCRT64|MSYS)\b/u.test(header)) {
627
+ if ([lines[at], lines[at - 1]].some(line => /^\s*(?:[$>#❯]|PS\s)/u.test(line) || /[\x00-\x1f\x7f]/u.test(line)) || stringWidth(lines[at - 1]) !== columns)
628
+ break;
629
+ header = lines[--at] + header;
630
+ }
631
+ if (/^[^\r\n]*\b(?:MINGW(?:32|64)|UCRT64|MSYS)\b[^\r\n]*$/u.test(header))
632
+ return "bash";
633
+ }
634
+ }
618
635
  return foreground;
619
636
  }
620
637
  /** The runtime owns platform foreground queries and shell-dialect reconciliation. */
@@ -624,7 +641,13 @@ export function paneShellForSubmission(name) {
624
641
  if (!isWin && foreground !== "ssh")
625
642
  return foreground;
626
643
  const screen = tmuxCommand(true, "capture-pane", "-p", "-J", "-t", name, "-S", "-4");
627
- return markShellCommand(foreground, screen.code === 0 ? screen.stdout : "");
644
+ const text = screen.code === 0 ? screen.stdout : "";
645
+ const selected = markShellCommand(foreground, text);
646
+ if (!isWin || selected !== foreground || selected === "bash" || !/(?:^|\n)\$\s*$/u.test(text.trimEnd()))
647
+ return selected;
648
+ const width = tmuxCommand(true, "display-message", "-p", "-t", name, "#{pane_width}");
649
+ const columns = width.code === 0 && /^\d+\s*$/u.test(width.stdout) ? Number(width.stdout.trim()) : undefined;
650
+ return markShellCommand(foreground, text, "win32", columns);
628
651
  }
629
652
  /** Common send code supplies intent; the runtime decides whether/how to encode it. */
630
653
  export function adaptShellSubmission(name, text, options = {}) {
package/docs/DESIGN.md CHANGED
@@ -63,6 +63,7 @@ argv・lineage・metadata・native admission・receipt・TUI本文は変えず
63
63
  同じ入口へ渡す。共通sendはraw/TUI/startupという送信意図と符号化前の上限検査を所有し、
64
64
  Windowsの前面名だけから方言を決めない。psmuxのprocess snapshot/OSCが古い時は、
65
65
  現在のPowerShell又はGit Bash promptで双方向に補正する。過去のpromptは使わない。
66
+ psmuxが`capture-pane -J`でも現在のGit Bash promptを折り返す場合は、実測したpane列数とUnicode表示セル数が一致する連続行だけを現在promptへ接続する。入力済みの`$ command`・別prompt・制御文字を跨がず、列数が不明/不一致なら補正しない。表示幅はNode18対応string-width 7.2.0へ委ね、文字数で日本語/emojiの幅を代用しない。通常の未折返しpromptやPOSIXのshell判定に追加の列数照会は行わない。
66
67
 
67
68
  TUIの準備判定、送信文の入力欄への残存、完了後の画面安定待ちは`agent-screen-observation.ts`が所有する。
68
69
  このmoduleはcoreをimportせず、既存harness adapterと渡されたsample・sleep・時計だけで判断する。
@@ -494,3 +495,18 @@ transcript、path、credentialを保存・公開しない。
494
495
  公開schema、完了境界、state ownership、platform backendを変える時は、原因の最小再現、focused test、
495
496
  関連ADR、日英README、CHANGELOGを同じ変更で同期する。現行制御から外れた完了・棄却・中断・失効・置換済みの設計planはcurrentへ残さずarchiveへ移す。
496
497
  旧設計draftは[`archive/01_design-plan.md`](https://github.com/kitepon/aiterm-mcp/blob/main/docs/archive/01_design-plan.md)に保存する。
498
+
499
+
500
+ Claude authentication URL receipts recognize the official claude.com/cai/oauth/authorize PKCE authorization request and its nonsecret code=true display flag. Callback authorization codes and tokens remain excluded; the common URL credential filter is unchanged. This auth-only patch contains no native goal feature.
501
+
502
+
503
+ ### 認証画面と専用応答
504
+
505
+ Claude起動前の公式auth status拒否はharnessが`AgentAuthenticationPreflightError`として所有し、通常MCPのfailが`aiterm.agent-launch-error.v1`へ投影する。既存数値exit codeと文言を維持し、未認証・確認不能・timeoutの機械code、authentication_preflight/not_issued/session_created:falseだけを追加する。CLI status本文・stderr・資格・推定したexpiry原因は持ち出さず、この拒否は認証PTYの入力や起動後turnの終端とは分ける。
506
+
507
+ `agent-auth-ui.ts`が既v1 receiptに追加する`auth_ui`とrespondの型を所有する。Native harnessが公式URL・device code・空code欄・現在のラベル付き選択肢を識別し、Rootはキー列を作らない。不明な入力段階はunavailable。coreが既auth metadataのdev/ino/作成・更新時刻、native session/pane/server/PID開始識別を照合する。legacyのlaunch UUID欠測はnullのまま扱い、無関係なflowをadoptしない。新startは既launch_id欄を必ず設定する。
508
+
509
+ respondは既session send lock内で現在の空欄/段階と世代を再検証し、専用auth paneのpipe loggingを入力前に停止する。新auth paneは公式CLI起動前にloggingを停止する。通常send・lastcmd・mark・turn・journalを通さず、POSIX tmux load-bufferへstdinで送り、immutable pane IDへpaste/Enterを一回送る。secretはargv/fileへ渡さずbufferを消す。CLIの独自資格保存はCLI所有で、Aitermの非保存保証とは区別する。入力前の拒否はnot_issued、入力開始後の所有/結果欠測はunknown。元画面へ勝手に戻す・再送する回復はしない。Windowsは既psmux送信だけで前面所有を証明できないため専用respondを未対応として拒否する。macOSはPOSIX経路のsource実装であり実OS受入は別gate。
510
+
511
+
512
+ Claude authのcurrent menuはharnessの単一parseがphase/rows/selected/入力占有を所有する。古いthemeと新loginが同viewportに残っても最後の現在phaseだけを採り、coreはそのselected_idでnavigateする。code欄が現在phaseならoccupied状態から古いmenu UIへ戻さない。cursor欠測/複数cursor/未知row/後続の未知promptは入力前拒否。Native内部の入力観測は公開auth_uiへ直接返さず、公開schemaを拡張しない。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiterm-mcp",
3
- "version": "0.61.17",
3
+ "version": "0.61.19",
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": [
@@ -74,6 +74,7 @@
74
74
  "dependencies": {
75
75
  "@modelcontextprotocol/sdk": "^1.29.0",
76
76
  "aiterm-steer-delivery": "^0.5.0",
77
+ "string-width": "7.2.0",
77
78
  "ws": "8.21.3",
78
79
  "zod": "^4.4.3"
79
80
  },