aiterm-mcp 0.43.7 → 0.44.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,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.44.1] - 2026-09-30
11
+
12
+ ### 修正
13
+
14
+ - `agent_models`のGrokが、ときどき`MODEL_CATALOG_INVALID: initializeの応答がありません(exit=0 … stdout=0bytes)`で失敗していた。要求を書いてすぐ標準入力を閉じていたため、grok 1.0.41がinitializeの処理中に入力の終わりを受けて、応答せずに終わっていた。ClaudeとGrokは、応答が出力に現れるまで標準入力を開けておき、現れてから閉じる。macOSのAqua外(launchd経由)でも同じ待ち方をする。
15
+
16
+ ## [0.44.0] - 2026-09-30
17
+
18
+ ### 追加
19
+
20
+ - 公開tool `agent_models`。harnessを指定すると、そのharnessが今選べるmodel IDとreasoning effortを返す(`aiterm.agent-models.v1`)。promptもturnも送らない。取得不能は`MODEL_CATALOG_UNAVAILABLE`、形式異常は`MODEL_CATALOG_INVALID`で、別の一覧へfallbackしない(ADR 0074)。
21
+ - Codex:公式App Serverの`model/list`。modelごとのeffortと既定effortを返す。`include_hidden`で隠しmodelも返す。
22
+ - Claude Code:stream-jsonの制御要求`initialize`の`models`(Agent SDKの`supportedModels()`と同じ)。hookとMCP serverを止め、sessionを保存しない。`ultracode`はeffort対応modelへadapterが足し、`adapter_efforts`で示す。
23
+ - Grok:`grok agent stdio`の`initialize`の`_meta.modelState`。modelごとのeffortを返す(`grok models`はeffortを出さない)。
24
+ - Cursor:`cursor-agent models`を、素のmodel IDと`<model>-<effort>`で作れるeffortへ分ける。
25
+ - `spawnAgentControlCommand`がstdinを渡せるようにした。macOSのAqua外(launchd経由)でも入力をつなぐ。Windowsの`.cmd`は空白を含むpathを引用する。
26
+
10
27
  ## [0.43.7] - 2026-09-29
11
28
 
12
29
  ### 修正
@@ -1907,7 +1924,9 @@ prototype (preserved under `prototype/python/` as the porting source and referen
1907
1924
  `ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
1908
1925
  provenance.
1909
1926
 
1910
- [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.43.7...HEAD
1927
+ [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.44.1...HEAD
1928
+ [0.44.1]: https://github.com/kitepon/aiterm-mcp/compare/v0.44.0...v0.44.1
1929
+ [0.44.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.43.7...v0.44.0
1911
1930
  [0.43.7]: https://github.com/kitepon/aiterm-mcp/compare/v0.43.6...v0.43.7
1912
1931
  [0.43.6]: https://github.com/kitepon/aiterm-mcp/compare/v0.43.5...v0.43.6
1913
1932
  [0.43.5]: https://github.com/kitepon/aiterm-mcp/compare/v0.43.4...v0.43.5
package/README.ja.md CHANGED
@@ -143,7 +143,7 @@ diagnostics、recovery、update、releaseを所有します。このREADMEと[
143
143
 
144
144
  **言葉でなく実測で:** 記録済み203テストのベンチマークでは、`pty_read` はコンテキストに載るトークンを生ログの **約 7.1 分の 1** に減らす。しかも pass/fail の判定は畳んでも残る。→ [組み込みシェルツールとの使い分け](#組み込みシェルツールとの使い分け)
145
145
 
146
- 16ツール: 7つのPTYツール、正規のagent起動入口`agent_launch`、移行用の旧3alias、`agent_configure`、`agent_approval`、`claude_turn`、`claude_approval`、`diagnostics`。backendはPOSIXのtmux/Windows nativeのpsmuxなので、MCPサーバやAIクライアントが再起動してもsessionは生き残る。
146
+ 17ツール: 7つのPTYツール、正規のagent起動入口`agent_launch`、移行用の旧3alias、`agent_models`、`agent_configure`、`agent_approval`、`claude_turn`、`claude_approval`、`diagnostics`。backendはPOSIXのtmux/Windows nativeのpsmuxなので、MCPサーバやAIクライアントが再起動してもsessionは生き残る。
147
147
 
148
148
  **v0.28.0では実行基盤harnessとmodelを分離した。** harnessはagent loop・認証・hook・session・transcriptを所有し、modelはその上で選ぶ。Cursor Agent CLIでGPT/Claude/Grokを選んでも完了契約はCursor方式のまま。ComposerはCursorのmodelの一つで、harnessでもGrokのmodelでもない。`harness:"cursor-cli", model:"composer-2.5-fast"`(または`composer-2.5`)で表す。旧起動ツールは同じ実装へ流れる互換alias。
149
149
 
@@ -203,7 +203,7 @@ runtime-error store は canonical dotagents config の `collection.enabled: true
203
203
  場合だけ収集し、既定OFF、network送信は行いません。tag起点CIのnpm provenance(OIDC Trusted
204
204
  Publishing)で公開し、GitHub Release が Official MCP Registry を再登録します。
205
205
 
206
- **状態:** 開発継続中 · 現行公開版 **v0.43.7** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
206
+ **状態:** 開発継続中 · 現行公開版 **v0.44.1** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
207
207
 
208
208
  ### 更新と巻き戻し
209
209
 
@@ -408,7 +408,7 @@ MCP クライアントが aiterm を stdio 越しにプログラムから駆動
408
408
 
409
409
  ```mermaid
410
410
  flowchart LR
411
- AI["AI / MCP client<br/>(the orchestrator)"] -->|"pty_send · pty_observe · agent_launch · agent_configure · agent_approval · claude_turn · claude_approval<br/>旧launcher alias · diagnostics"| S["aiterm-mcp<br/>stdio MCP · 16 tools"]
411
+ AI["AI / MCP client<br/>(the orchestrator)"] -->|"pty_send · pty_observe · agent_launch · agent_models · agent_configure · agent_approval · claude_turn · claude_approval<br/>旧launcher alias · diagnostics"| S["aiterm-mcp<br/>stdio MCP · 17 tools"]
412
412
  S -->|"pty_read<br/>token-reduced"| AI
413
413
  S -->|"tmux / psmux<br/>send · capture"| P["persistent PTYs<br/>再起動を跨ぐ"]
414
414
  P -->|"ssh · docker · repl"| R["nested<br/>remote · container · REPL"]
@@ -524,6 +524,7 @@ Claudeの相関済み承認は既存の`claude_approval`を使う。
524
524
  | `pty_list` | textと構造化したsession一覧、明示した非秘密環境変数の照会 | `env_keys?` |
525
525
  | `pty_observe` | pane/harnessの生存、native process identity、状態と活動 | `session_id`, `cursor?` |
526
526
  | `agent_launch` | harnessとmodelを別軸で選ぶ正規agent起動入口 | `harness`, `prompt?`, `model?`, `reasoning_effort?`, `cwd?`, `write_scope?`, `trust_project?`, `env_vars?`, `throughline_source_session?`, `throughline_supplement_file?` |
527
+ | `agent_models` | harnessが今選べるmodelとreasoning effortを、そのharness自身の一覧からpromptを送らずに取得 | `harness`, `cwd?`, `include_hidden?` |
527
528
  | `agent_approval` | Codexの現在の承認を検査し、単発許可・拒否を送る | `action`, `session_id`, `approval_choice?`, `observed_prompt_digest?` |
528
529
  | `claude_agent` / `codex_agent` / `grok_agent` | deprecated互換alias(`composer_agent`は0.41.0で削除。Composerは`agent_launch`の`cursor-cli`で使う) | 旧launcher引数 |
529
530
  | `agent_configure` | 起動中のClaude/Codex/Grok/Cursorを再起動せずmodel/effort変更 | `session_id`, `model?`, `reasoning_effort?` |
@@ -545,6 +546,21 @@ consumer は `aiterm-runtime-errors snapshot` を読み、durable ingestion 後
545
546
 
546
547
  `agent_configure({ session_id, model?, reasoning_effort? })`はharness標準操作で起動中のClaude/Codex/Grok/Cursorを変更し、PTYと会話contextを維持する。
547
548
 
549
+ `agent_models({ harness, cwd?, include_hidden? })`は、導入済みのharnessが今選べるmodel IDとreasoning effortを返す。画面の候補を、実際にagentを動かす端末の一覧から作れる。各harness自身の一覧を読むだけで、promptもturnも送らない。
550
+
551
+ | `harness` | 取得元 | 補足 |
552
+ | --- | --- | --- |
553
+ | `codex-cli` | App Serverの`model/list` | modelごとのeffortと既定effort。`include_hidden: true`でCodexが隠すmodelも返す |
554
+ | `claude-code` | stream-jsonの制御要求`initialize`(Agent SDKの`supportedModels()`と同じ`models`) | hookとMCP serverを止め、sessionを保存しない。Claude Codeは`--effort ultracode`を受け付けるが一覧は返さないため、effort対応modelへ`ultracode`を足し、`adapter_efforts`で示す |
555
+ | `grok-cli` | `grok agent stdio`の`initialize`(`_meta.modelState`) | modelごとのeffort。sessionは作らない |
556
+ | `cursor-cli` | `cursor-agent models` | 素のmodel IDと、Aitermが`<model>-<effort>`へ連結できるeffortへ分ける。`-fast`版とeffortが途中に入るIDは候補にしない。使う時は完成形をeffortなしで`model`に渡す |
557
+
558
+ 結果(`aiterm.agent-models.v1`)は`{ harness, source, harness_version, default_model, efforts, adapter_efforts, models: [{ id, display_name, efforts, default_effort, hidden }] }`。`id`とeffortはそのまま`agent_launch`/`agent_configure`へ渡せる。上位の`efforts`は全modelの和。取得不能は`MODEL_CATALOG_UNAVAILABLE`、形式異常は`MODEL_CATALOG_INVALID`で、別の一覧へfallbackしない。`remote`を付けると、その端末のharnessの一覧を返す。
559
+
560
+ ```json
561
+ { "name": "agent_models", "arguments": { "harness": "grok-cli" } }
562
+ ```
563
+
548
564
  | `harness` | 起動するもの | modelの扱い |
549
565
  | --- | --- | --- |
550
566
  | `claude-code` | Claude Code CLI | Claude model/effort |
package/README.md CHANGED
@@ -145,7 +145,7 @@ Aiterm and is not a runtime dependency.
145
145
 
146
146
  **Measured, not claimed:** in the recorded 203-test benchmark, a `pty_read` puts **~7.1× fewer tokens** in your context than the raw log — and the pass/fail verdict survives the fold. → [When to reach for it vs. the built-in shell](#when-to-reach-for-it-vs-the-built-in-shell)
147
147
 
148
- Sixteen tools: seven **PTY tools** — `pty_open` / `pty_send` / `pty_read` / `pty_key` / `pty_close` / `pty_list` / `pty_observe` — to open, drive, read, and observe one persistent terminal; one canonical **agent launcher**, `agent_launch`, which selects `claude-code`, `codex-cli`, `grok-cli`, or `cursor-cli` as the execution harness; three deprecated launcher aliases kept for migration; `agent_configure`; `agent_approval`; `claude_turn`; `claude_approval`; and `diagnostics`. The backend is **tmux on POSIX and psmux on native Windows**, so sessions survive even if the MCP server or the AI client restarts.
148
+ Seventeen tools: seven **PTY tools** — `pty_open` / `pty_send` / `pty_read` / `pty_key` / `pty_close` / `pty_list` / `pty_observe` — to open, drive, read, and observe one persistent terminal; one canonical **agent launcher**, `agent_launch`, which selects `claude-code`, `codex-cli`, `grok-cli`, or `cursor-cli` as the execution harness; three deprecated launcher aliases kept for migration; `agent_models`; `agent_configure`; `agent_approval`; `claude_turn`; `claude_approval`; and `diagnostics`. The backend is **tmux on POSIX and psmux on native Windows**, so sessions survive even if the MCP server or the AI client restarts.
149
149
 
150
150
  **v0.28.0 separates the execution harness from the model.** The harness owns the agent loop, authentication, hooks, session, and transcript; `model` is what that harness runs. Cursor Agent CLI can therefore select GPT, Claude, or Grok without changing the completion contract from Cursor hooks to another harness's. Composer is one of Cursor's models, not a harness and not a Grok model: use `harness: "cursor-cli", model: "composer-2.5-fast"` (or `composer-2.5`). The old launcher tools are thin compatibility aliases over the same implementation.
151
151
 
@@ -217,7 +217,7 @@ collection is off by default and performs no network I/O. It ships via
217
217
  tag-triggered CI with npm provenance (OIDC Trusted Publishing); the GitHub
218
218
  Release re-registers the Official MCP Registry entry.
219
219
 
220
- **Status:** actively maintained · current public release **v0.43.7** · 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).
220
+ **Status:** actively maintained · current public release **v0.44.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).
221
221
 
222
222
  ### Update and rollback
223
223
 
@@ -396,7 +396,7 @@ The only edits to the captures above are the two `⋮` lines (a long head/tail r
396
396
  `aiterm-setup --json`が`ready`になったら、利用するMCP clientを再起動して接続を確認する。Claude Codeの場合:
397
397
 
398
398
  ```bash
399
- /mcp # aiterm should show as connected, exposing 16 tools
399
+ /mcp # aiterm should show as connected, exposing 17 tools
400
400
  ```
401
401
 
402
402
  Your first session — four calls, one persistent terminal:
@@ -437,7 +437,7 @@ The terminal is real and shared, so a human *can* jump in ([A human can watch](#
437
437
 
438
438
  ```mermaid
439
439
  flowchart LR
440
- AI["AI / MCP client<br/>(the orchestrator)"] -->|"pty_send · pty_observe · agent_launch · agent_configure · agent_approval · claude_turn · claude_approval<br/>legacy launcher aliases · diagnostics"| S["aiterm-mcp<br/>stdio MCP · 16 tools"]
440
+ AI["AI / MCP client<br/>(the orchestrator)"] -->|"pty_send · pty_observe · agent_launch · agent_models · agent_configure · agent_approval · claude_turn · claude_approval<br/>legacy launcher aliases · diagnostics"| S["aiterm-mcp<br/>stdio MCP · 17 tools"]
441
441
  S -->|"pty_read<br/>token-reduced"| AI
442
442
  S -->|"tmux / psmux<br/>send · capture"| P["persistent PTYs<br/>survive restarts"]
443
443
  P -->|"ssh · docker · repl"| R["nested<br/>remote · container · REPL"]
@@ -557,6 +557,7 @@ continue to use `claude_approval`.
557
557
  | `pty_list` | Text and structured session list, with explicitly requested non-secret environment values | `env_keys?` |
558
558
  | `pty_observe` | Pane/harness liveness, native process identity, state, and activity | `session_id`, `cursor?` |
559
559
  | `agent_launch` | Canonical agent launch; harness and model are independent | `harness`, `prompt?`, `model?`, `reasoning_effort?`, `cwd?`, `write_scope?`, `trust_project?`, `env_vars?`, `throughline_source_session?`, `throughline_supplement_file?` |
560
+ | `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?` |
560
561
  | `agent_approval` | Inspect a Codex approval and submit a one-time approval or denial | `action`, `session_id`, `approval_choice?`, `observed_prompt_digest?` |
561
562
  | `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 |
562
563
  | `agent_configure` | Change model/effort in a running Claude, Codex, Grok, or Cursor session without restarting it | `session_id`, `model?`, `reasoning_effort?` |
@@ -580,6 +581,21 @@ Consumer flow is `aiterm-runtime-errors snapshot`, then `aiterm-runtime-errors a
580
581
 
581
582
  `agent_configure({ session_id, model?, reasoning_effort? })` changes a running Claude, Codex, Grok, or Cursor TUI through the harness's standard controls, preserving the PTY and conversation context.
582
583
 
584
+ `agent_models({ harness, cwd?, include_hidden? })` returns the model IDs and reasoning efforts the installed harness offers right now, so a UI can build its choices from the machine that actually runs the agents. It reads each harness's own catalog and never sends a prompt or starts a turn:
585
+
586
+ | `harness` | Source | Notes |
587
+ | --- | --- | --- |
588
+ | `codex-cli` | App Server `model/list` | Per-model efforts and default effort. `include_hidden: true` also returns models Codex hides. |
589
+ | `claude-code` | stream-json `initialize` control request (the same `models` the Agent SDK's `supportedModels()` returns) | Hooks and MCP servers are disabled and the session is not persisted. `ultracode` is added to models that support effort and reported in `adapter_efforts`, because Claude Code accepts `--effort ultracode` but the catalog does not report it. |
590
+ | `grok-cli` | `grok agent stdio` `initialize` (`_meta.modelState`) | Per-model efforts; no session is created. |
591
+ | `cursor-cli` | `cursor-agent models` | Split into base model IDs and the efforts Aiterm can append as `<model>-<effort>`. `-fast` variants and IDs with an embedded effort are not offered; pass such a full ID as `model` without an effort. |
592
+
593
+ The result (`aiterm.agent-models.v1`) is `{ harness, source, harness_version, default_model, efforts, adapter_efforts, models: [{ id, display_name, efforts, default_effort, hidden }] }`. Every `id` and effort can be passed to `agent_launch` and `agent_configure` as is; `efforts` at the top is the union across models. An unavailable catalog is `MODEL_CATALOG_UNAVAILABLE` and a malformed one is `MODEL_CATALOG_INVALID`; Aiterm never falls back to another list. With `remote`, the catalog comes from the harness on that host.
594
+
595
+ ```json
596
+ { "name": "agent_models", "arguments": { "harness": "grok-cli" } }
597
+ ```
598
+
583
599
  | `harness` | Launches | Model behavior |
584
600
  | --- | --- | --- |
585
601
  | `claude-code` | Claude Code CLI | Claude catalog model; native effort controls |
@@ -1,6 +1,6 @@
1
1
  // エージェント CLI(claude / codex / grok / cursor-agent)・Throughline・pane shell(Windows は Git Bash)の
2
2
  // 実行ファイルをどう見つけ、どう起動するかの所有者(OS 分岐の所有者。tmux とは独立)。
3
- import { spawnSync } from "node:child_process";
3
+ import { spawn, spawnSync } from "node:child_process";
4
4
  import * as fs from "node:fs";
5
5
  import * as path from "node:path";
6
6
  import * as os from "node:os";
@@ -74,8 +74,8 @@ export function spawnAgentControlCommand(bin, args, _cwd, options) {
74
74
  if (isWin && !/\.(?:exe|com)$/i.test(bin)) {
75
75
  if (/\.(?:cmd|bat)$/i.test(bin)) {
76
76
  // Node は CVE-2024-27980 対処以降、.cmd/.bat の直接 spawn を EINVAL で拒否する。
77
- // 受入が .cmd/.bat を許す以上、control 経路は shell 経由で実行する(args は固定語彙)。
78
- return spawnSync(bin, args, { ...options, shell: true });
77
+ // 受入が .cmd/.bat を許す以上、control 経路は shell 経由で実行する(args は固定語彙とAiterm所有の一時path)。
78
+ return spawnSync(bin, windowsControlArgs(bin, args), { ...options, shell: true });
79
79
  }
80
80
  // 受入は pane shell(Git Bash)が shebang で実行できる script も許す。Windows の
81
81
  // CreateProcess は shebang を解さないため、control command も同じ Git Bash で実行する。
@@ -84,6 +84,72 @@ export function spawnAgentControlCommand(bin, args, _cwd, options) {
84
84
  }
85
85
  return spawnInMacGuiWhenOutsideAqua(bin, args, options) ?? spawnSync(bin, args, options);
86
86
  }
87
+ function windowsControlArgs(bin, args) {
88
+ // shell経由ではNodeが引数を引用しないので、空白を含むpathだけ二重引用符でくくる。引用符とshell記号は受け付けない。
89
+ if (args.some(arg => /["%^&|<>!]/.test(arg))) {
90
+ throw new AitermError(`${path.basename(bin)} のcontrol commandへ渡せない文字を含む引数があります`, 2);
91
+ }
92
+ return args.map(arg => /\s/.test(arg) ? `"${arg}"` : arg);
93
+ }
94
+ /**
95
+ * 標準入出力で要求と応答をやりとりするcontrol command(`claude -p --input-format stream-json`、`grok agent stdio`)。
96
+ * inputを書いた後、untilの文字列がstdoutに現れるまで標準入力を開けておき、現れたら閉じて終了を待つ。
97
+ * 応答前に標準入力を閉じると、応答せずに正常終了するCLIがある(grok 1.0.41で実測)。
98
+ * 起動方法のOS差はspawnAgentControlCommandと同じ。macOSのAqua外はlaunchdの仕事の中で同じ待ち方をする。
99
+ */
100
+ export async function runAgentProtocolCommand(bin, args, options) {
101
+ const gui = spawnInMacGuiWhenOutsideAqua(bin, args, options);
102
+ if (gui)
103
+ return { status: gui.status, signal: gui.signal, stdout: gui.stdout, stderr: gui.stderr, ...(gui.error ? { error: gui.error } : {}) };
104
+ let command = bin;
105
+ let argv = args;
106
+ let shell = false;
107
+ if (isWin && !/\.(?:exe|com)$/i.test(bin)) {
108
+ if (/\.(?:cmd|bat)$/i.test(bin)) {
109
+ argv = windowsControlArgs(bin, args);
110
+ shell = true;
111
+ }
112
+ else {
113
+ command = resolveWinPaneShell("bash");
114
+ argv = [bin, ...args];
115
+ }
116
+ }
117
+ return new Promise((resolve) => {
118
+ const child = spawn(command, argv, { cwd: options.cwd, env: options.env, shell, windowsHide: true, stdio: ["pipe", "pipe", "pipe"] });
119
+ let stdout = "";
120
+ let stderr = "";
121
+ let error;
122
+ let settled = false;
123
+ const finish = (status, signal) => {
124
+ if (settled)
125
+ return;
126
+ settled = true;
127
+ clearTimeout(timer);
128
+ resolve({ status, signal, stdout, stderr, ...(error ? { error } : {}) });
129
+ };
130
+ const stop = (reason) => {
131
+ error ??= reason;
132
+ child.stdin.destroy();
133
+ child.kill();
134
+ };
135
+ const timer = setTimeout(() => stop(Object.assign(new Error(`${path.basename(bin)} ${args.join(" ")} が${options.timeout}ms以内に応答しませんでした`), { code: "ETIMEDOUT" })), options.timeout);
136
+ child.on("error", (reason) => { error ??= reason; finish(null, null); });
137
+ child.on("close", (status, signal) => finish(status, signal));
138
+ child.stdin.on("error", () => { });
139
+ child.stderr.setEncoding("utf8");
140
+ child.stderr.on("data", (chunk) => { if (stderr.length < options.maxBuffer)
141
+ stderr += chunk; });
142
+ child.stdout.setEncoding("utf8");
143
+ child.stdout.on("data", (chunk) => {
144
+ stdout += chunk;
145
+ if (stdout.length > options.maxBuffer)
146
+ return stop(new Error(`${path.basename(bin)} の出力が${options.maxBuffer}bytesを超えました`));
147
+ if (child.stdin.writable && stdout.includes(options.until))
148
+ child.stdin.end();
149
+ });
150
+ child.stdin.write(options.input);
151
+ });
152
+ }
87
153
  export function resolveWinPaneShell(shell) {
88
154
  if (!isWin)
89
155
  return shell;
package/dist/core.js CHANGED
@@ -19,10 +19,11 @@ import { readRuntimeProcesses, processSubtree, parentProcess, processIdentity, b
19
19
  import { AitermError, telemetryOwnedFailure, ownTelemetryFailure } from "./errors.js";
20
20
  import { isWin, SOCKDIR, tmuxCommand, sendPsmuxPayload, loadPtyBufferChunk, pasteBufferBaseArgs, TMUX_EMPTY_CONFIG, attachCommand, normalizePaneCommand, atomicShellMultiline, appendMarkSentinel, markShellCommand, settlePaneLog, paneCwdArgument, sessionEnvironmentLaunch, } from "./tmux-runtime.js";
21
21
  import { sleep, currentUid, runtimeStateBase, safeStatSize, readFileRange, writeJson0600, createEmpty0600, shq, LAUNCH_ID_RE, AGENT_DONE_POLL_MS, AGENT_EVENT_MAX_BYTES, assertSessionName, agentsDir, agentEventPath, agentMetadataPath, writeAgentMetadata, AGENT_EVENT_TAIL_BYTES, agentLabel, agentHarness, subagentInstruction, agentLineageFields, } from "./agent-shared.js";
22
- import { realGrokHome, resolveAndValidateGrokAuth, assertGrokModelAvailable, grokEventsTranscript, latestGrokCompletion, observeGrokDone, buildGrokAgentCmd, grokLaunchNote, grokEnvTokens, grokTuiReady, grokTuiBusy, grokPaneObservation, grokRateLimitDialog, grokStartupAction, grokLaunchBlockingDialog, assertGrokSandboxNotRejected, GROK_COMPOSER_MARKER_RE, grokFooterHasConfiguration, grokTranscriptText, createGrokAgentMetadata, } from "./harnesses/grok.js";
23
- import { bindCodexTranscriptSession, latestCodexCompletion, observeCodexDone, buildCodexAgentCmd, codexLaunchNote, codexTuiReady, codexPaneObservation, codexRateLimitModelSwitchDialog, codexApprovalDialog, codexStartupAction, CODEX_COMPOSER_MARKER_RE, codexModelChoice, codexEffortChoice, codexMoreReasoningChoice, codexTranscriptText, createCodexAgentMetadata, } from "./harnesses/codex.js";
24
- import { OPERATION_ID_RE, CLAUDE_RESULT_MAX_BYTES, CLAUDE_EFFORTS, agentManagedClaudeSettingsPath, agentClaudeResultPath, agentClaudeOperationPath, agentClaudeApprovalReceiptPath, agentClaudeDispatchReceiptPath, validateOperationId, readClaudeResultText, assertClaudeAuthenticationReady, buildClaudeAgentCmd, claudeLaunchNote, claudeTuiReady, claudePaneObservation, claudeUsageLimit, claudeStartupAction, claudeLoginMethodMenu, CLAUDE_COMPOSER_MARKER_RE, createClaudeAgentMetadata, claudeSessionTranscriptPath, claudeApiErrorFromLine, claudeApiErrorAfter, } from "./harnesses/claude.js";
25
- import { bindCursorTranscriptSession, cursorTurnBoundary, latestCursorCompletion, observeCursorDone, cursorTranscriptText, assertCursorAuthenticationReady, assertCursorModelAvailable, buildCursorAgentCmd, cursorAgentArgv, cursorPwshLaunchLine, cursorPromptWithLineage, createCursorAgentMetadata, cursorLaunchNote, cursorEffortNavigation, cursorTuiReady, cursorPaneObservation, cursorPromptHooksRunning, cursorUsageLimit, CURSOR_SUBMIT_SEQUENCE, CURSOR_COMPOSER_CONTENT_MARKER_RE, validateCursorModelEffort, } from "./harnesses/cursor.js";
22
+ import { catalogUnavailable } from "./model-catalog.js";
23
+ import { realGrokHome, resolveAndValidateGrokAuth, assertGrokModelAvailable, grokModelChoices, grokEventsTranscript, latestGrokCompletion, observeGrokDone, buildGrokAgentCmd, grokLaunchNote, grokEnvTokens, grokTuiReady, grokTuiBusy, grokPaneObservation, grokRateLimitDialog, grokStartupAction, grokLaunchBlockingDialog, assertGrokSandboxNotRejected, GROK_COMPOSER_MARKER_RE, grokFooterHasConfiguration, grokTranscriptText, createGrokAgentMetadata, } from "./harnesses/grok.js";
24
+ import { bindCodexTranscriptSession, latestCodexCompletion, observeCodexDone, buildCodexAgentCmd, codexLaunchNote, codexTuiReady, codexPaneObservation, codexRateLimitModelSwitchDialog, codexApprovalDialog, codexStartupAction, CODEX_COMPOSER_MARKER_RE, codexModelChoice, codexEffortChoice, codexMoreReasoningChoice, codexTranscriptText, createCodexAgentMetadata, codexModelChoices, } from "./harnesses/codex.js";
25
+ import { OPERATION_ID_RE, CLAUDE_RESULT_MAX_BYTES, CLAUDE_EFFORTS, agentManagedClaudeSettingsPath, agentClaudeResultPath, agentClaudeOperationPath, agentClaudeApprovalReceiptPath, agentClaudeDispatchReceiptPath, validateOperationId, readClaudeResultText, assertClaudeAuthenticationReady, claudeModelChoices, buildClaudeAgentCmd, claudeLaunchNote, claudeTuiReady, claudePaneObservation, claudeUsageLimit, claudeStartupAction, claudeLoginMethodMenu, CLAUDE_COMPOSER_MARKER_RE, createClaudeAgentMetadata, claudeSessionTranscriptPath, claudeApiErrorFromLine, claudeApiErrorAfter, } from "./harnesses/claude.js";
26
+ import { bindCursorTranscriptSession, cursorTurnBoundary, latestCursorCompletion, observeCursorDone, cursorTranscriptText, assertCursorAuthenticationReady, assertCursorModelAvailable, cursorModelChoices, buildCursorAgentCmd, cursorAgentArgv, cursorPwshLaunchLine, cursorPromptWithLineage, createCursorAgentMetadata, cursorLaunchNote, cursorEffortNavigation, cursorTuiReady, cursorPaneObservation, cursorPromptHooksRunning, cursorUsageLimit, CURSOR_SUBMIT_SEQUENCE, CURSOR_COMPOSER_CONTENT_MARKER_RE, validateCursorModelEffort, } from "./harnesses/cursor.js";
26
27
  import { readInterimWords, recordInterimBoundary } from "./interim-words.js";
27
28
  import { resolveAgentBin, resolveThroughlineBin, runThroughlineHandoffContext, isWindowsNativeExecutable, agentBinForPaneShell, resolveWinPaneShell } from "./agent-resolver.js";
28
29
  export { AitermError } from "./errors.js";
@@ -3340,6 +3341,26 @@ async function waitAgentTuiReadyAfterCodexRateLimitRecovery(name, meta, timeoutM
3340
3341
  return { ready, codexRateLimitModelSwitch };
3341
3342
  }
3342
3343
  /** 同じ対話sessionを保ったまま、harness標準の操作でmodel/effortを変更する。 */
3344
+ /**
3345
+ * harnessが今返すmodelとreasoning effortの候補(agent_models)。各harnessの公式の一覧を読むだけで、
3346
+ * promptもturnも送らない。取得不能・形式異常はfallbackせずエラーにする。
3347
+ */
3348
+ export async function listAgentModels(kind, options = {}) {
3349
+ const cwd = options.cwd ?? process.cwd();
3350
+ if (!path.isAbsolute(cwd))
3351
+ throw new AitermError("cwd は絶対パスで指定してください", 2);
3352
+ if (!fs.existsSync(cwd) || !fs.statSync(cwd).isDirectory())
3353
+ throw new AitermError(`cwd が見つかりません: ${cwd}`, 2);
3354
+ const bin = resolveAgentBin(kind);
3355
+ if (!bin)
3356
+ throw catalogUnavailable(agentLabel(kind), `${agentLabel(kind)} の CLI が見つかりません`);
3357
+ switch (kind) {
3358
+ case "claude": return claudeModelChoices(bin, cwd);
3359
+ case "codex": return codexModelChoices(bin, options.include_hidden === true);
3360
+ case "grok": return grokModelChoices(bin, cwd);
3361
+ case "cursor": return cursorModelChoices(bin, cwd);
3362
+ }
3363
+ }
3343
3364
  export async function configureAgent(name, opts) {
3344
3365
  assertSessionName(name);
3345
3366
  const model = opts.model?.trim() || null;
@@ -8,7 +8,8 @@ import { createHash, randomBytes, randomUUID } from "node:crypto";
8
8
  import { fileURLToPath } from "node:url";
9
9
  import { AitermError } from "../errors.js";
10
10
  import { modeBitsWorldAccessible } from "../tmux-runtime.js";
11
- import { spawnAgentControlCommand } from "../agent-resolver.js";
11
+ import { runAgentProtocolCommand, spawnAgentControlCommand } from "../agent-resolver.js";
12
+ import { catalogInvalid, catalogUnavailable, checkedCatalog, findJsonLine, processSummary } from "../model-catalog.js";
12
13
  import { currentUid, writeJson0600, shq, subagentInstruction, writeScopeLaunchNote, agentEventPath, createEmpty0600, writeAgentMetadata, agentLineageFields, assertSessionName, agentsDir, LAUNCH_ID_RE, AGENT_EVENT_TAIL_BYTES, readFileRange, } from "../agent-shared.js";
13
14
  export const OPERATION_ID_RE = /^sha256:[0-9a-f]{64}$/;
14
15
  export const CLAUDE_RESULT_MAX_BYTES = 4 * 1024 * 1024;
@@ -134,6 +135,78 @@ export function readClaudeResultText(meta, done, operationId, transcriptUnavaila
134
135
  return result.text;
135
136
  }
136
137
  const CLAUDE_AUTH_STATUS_TIMEOUT_MS = 5_000;
138
+ const CLAUDE_MODELS_TIMEOUT_MS = 30_000;
139
+ const CLAUDE_MODELS_MAX_BYTES = 4 * 1024 * 1024;
140
+ // ultracodeはClaude Codeのsession設定で、`--effort ultracode` で有効になる(2.1.285実測:警告なしで受け付け、
141
+ // 未知の値は「Unknown --effort value」と警告して無視)。initializeのModelInfoは対応modelを返さないため、
142
+ // effortに対応したmodelへadapterが足し、その事実をadapter_effortsで示す。
143
+ const CLAUDE_ULTRACODE_NOTE = "Claude Codeの--effortが受け付けるsession設定(dynamic workflow)。initializeのModelInfoは対応modelを返さないため、Aitermがeffort対応modelへ足している";
144
+ /**
145
+ * Claude Codeのmodel候補。公式Agent SDKのsupportedModels()と同じく、stream-jsonの制御要求 `initialize`
146
+ * (SDKの公開型 SDKControlRequest/SDKControlInitializeResponse)の応答にある models を読む。
147
+ * promptは送らず、sessionを保存せず、利用者のhookとMCP serverを起動しない。
148
+ */
149
+ export async function claudeModelChoices(bin, cwd) {
150
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "aiterm-claude-models-"));
151
+ try {
152
+ const settings = path.join(dir, "settings.json");
153
+ fs.writeFileSync(settings, JSON.stringify({ disableAllHooks: true }), { mode: 0o600 });
154
+ const requestId = `aiterm-models-${randomUUID()}`;
155
+ const request = { type: "control_request", request_id: requestId, request: { subtype: "initialize" } };
156
+ const result = await runAgentProtocolCommand(bin, [
157
+ "-p", "--output-format", "stream-json", "--verbose", "--input-format", "stream-json",
158
+ "--no-session-persistence", "--strict-mcp-config", "--settings", settings,
159
+ ], {
160
+ cwd,
161
+ env: process.env,
162
+ input: JSON.stringify(request) + "\n",
163
+ until: requestId,
164
+ timeout: CLAUDE_MODELS_TIMEOUT_MS,
165
+ maxBuffer: CLAUDE_MODELS_MAX_BYTES,
166
+ });
167
+ if (result.error || result.status !== 0) {
168
+ throw catalogUnavailable("Claude Code", result.error?.message || result.stderr?.trim() || `exit=${result.status ?? "unknown"}`);
169
+ }
170
+ return claudeCatalogFromInitialize(result.stdout, requestId, processSummary(result));
171
+ }
172
+ finally {
173
+ fs.rmSync(dir, { recursive: true, force: true });
174
+ }
175
+ }
176
+ /** stream-jsonのcontrol_response(initialize)からmodel候補を作る。 */
177
+ export function claudeCatalogFromInitialize(stdout, requestId, summary = "") {
178
+ const response = findJsonLine(stdout, (value) => value?.type === "control_response" && value.response?.request_id === requestId);
179
+ if (!response)
180
+ throw catalogInvalid("Claude Code", `initializeの応答がありません${summary ? `(${summary})` : ""}`);
181
+ if (response.response.subtype !== "success") {
182
+ throw catalogUnavailable("Claude Code", `initializeが拒否されました: ${String(response.response.error ?? response.response.subtype)}`);
183
+ }
184
+ const models = response.response.response?.models;
185
+ if (!Array.isArray(models))
186
+ throw catalogInvalid("Claude Code", "initializeの応答に models がありません");
187
+ const choices = models.map((model) => {
188
+ if (typeof model?.value !== "string")
189
+ throw catalogInvalid("Claude Code", "valueの無いmodelがあります");
190
+ const levels = model.supportsEffort === true ? model.supportedEffortLevels : [];
191
+ if (!Array.isArray(levels) || levels.some((level) => typeof level !== "string")) {
192
+ throw catalogInvalid("Claude Code", `${model.value} の supportedEffortLevels を読めません`);
193
+ }
194
+ return {
195
+ id: model.value,
196
+ display_name: typeof model.displayName === "string" ? model.displayName : null,
197
+ efforts: levels.length > 0 ? [...levels, "ultracode"] : [],
198
+ default_effort: null,
199
+ hidden: false,
200
+ };
201
+ });
202
+ return checkedCatalog("Claude Code", {
203
+ source: "claude stream-json control_request initialize (models)",
204
+ harness_version: null,
205
+ default_model: choices.some((choice) => choice.id === "default") ? "default" : null,
206
+ adapter_efforts: choices.some((choice) => choice.efforts.includes("ultracode")) ? { ultracode: CLAUDE_ULTRACODE_NOTE } : {},
207
+ models: choices,
208
+ });
209
+ }
137
210
  export function assertClaudeAuthenticationReady(bin) {
138
211
  const result = spawnAgentControlCommand(bin, ["auth", "status", "--json"], process.cwd(), {
139
212
  encoding: "utf8",
@@ -6,6 +6,9 @@ import * as os from "node:os";
6
6
  import * as path from "node:path";
7
7
  import { randomBytes } from "node:crypto";
8
8
  import { AitermError } from "../errors.js";
9
+ import * as steer from "aiterm-steer-delivery";
10
+ import { AITERM_PROFILE } from "../steer-profile.js";
11
+ import { catalogInvalid, catalogUnavailable, checkedCatalog } from "../model-catalog.js";
9
12
  import { shq, subagentInstruction, writeScopeLaunchNote, safeStatSize, readFileRange, sleep, agentMetadataPath, writeAgentMetadata, agentEventPath, createEmpty0600, agentLineageFields, AGENT_DONE_POLL_MS, AGENT_EVENT_MAX_BYTES, AGENT_EVENT_TAIL_BYTES, CODEX_TRANSCRIPT_INCREMENT_MAX_BYTES, agentHarness, } from "../agent-shared.js";
10
13
  export function realCodexHome() {
11
14
  return process.env.CODEX_HOME || path.join(process.env.HOME ?? os.homedir(), ".codex");
@@ -706,3 +709,73 @@ export function createCodexAgentMetadata(name, cwd, initialPrompt, overrides = {
706
709
  writeAgentMetadata(meta);
707
710
  return meta;
708
711
  }
712
+ const CODEX_MODELS_TIMEOUT_MS = 30_000;
713
+ const CODEX_MODELS_PAGE_LIMIT = 100;
714
+ // model/listを無限に辿らない上限。1ページ100件で十分に余る。
715
+ const CODEX_MODELS_MAX_PAGES = 20;
716
+ /**
717
+ * Codexのmodel候補。公式App Serverの `model/list` を読む(https://learn.chatgpt.com/docs/app-server#list-models-modellist)。
718
+ * 親配送と同じ接続(aiterm-steer-deliveryのwithCodexReceiver)で、threadもturnも作らない。
719
+ * `codex debug models` も同じ内容を返すが、debug用の命令なので使わない。
720
+ */
721
+ export async function codexModelChoices(bin, includeHidden) {
722
+ const parent = { thread_id: "00000000-0000-4000-8000-000000000000", codex_home: path.resolve(steer.realCodexHome()) };
723
+ let pages;
724
+ try {
725
+ pages = await steer.withCodexReceiver(AITERM_PROFILE, parent, async (request) => {
726
+ const collected = [];
727
+ let cursor = null;
728
+ for (let page = 0; page < CODEX_MODELS_MAX_PAGES; page++) {
729
+ const result = await request("model/list", { cursor, limit: CODEX_MODELS_PAGE_LIMIT, includeHidden });
730
+ collected.push(result);
731
+ if (result?.nextCursor === null || result?.nextCursor === undefined)
732
+ return collected;
733
+ if (typeof result.nextCursor !== "string" || result.nextCursor === cursor)
734
+ throw catalogInvalid("Codex", "model/listの次ページが不正です");
735
+ cursor = result.nextCursor;
736
+ }
737
+ throw catalogInvalid("Codex", `model/listが${CODEX_MODELS_MAX_PAGES}ページを超えました`);
738
+ }, { executable: bin, timeout_ms: CODEX_MODELS_TIMEOUT_MS });
739
+ }
740
+ catch (error) {
741
+ if (error instanceof AitermError)
742
+ throw error;
743
+ throw catalogUnavailable("Codex", error instanceof Error ? error.message : String(error));
744
+ }
745
+ return codexCatalogFromPages(pages);
746
+ }
747
+ /** `model/list` の応答(全ページ)からmodel候補を作る。 */
748
+ export function codexCatalogFromPages(pages) {
749
+ let defaultModel = null;
750
+ const models = pages.flatMap((page) => {
751
+ if (!Array.isArray(page?.data))
752
+ throw catalogInvalid("Codex", "model/listの応答に data がありません");
753
+ return page.data.map((model) => {
754
+ if (typeof model?.id !== "string")
755
+ throw catalogInvalid("Codex", "idの無いmodelがあります");
756
+ if (!Array.isArray(model.supportedReasoningEfforts))
757
+ throw catalogInvalid("Codex", `${model.id} に supportedReasoningEfforts がありません`);
758
+ const efforts = model.supportedReasoningEfforts.map((level) => {
759
+ if (typeof level?.reasoningEffort !== "string")
760
+ throw catalogInvalid("Codex", `${model.id} のreasoning effortを読めません`);
761
+ return level.reasoningEffort;
762
+ });
763
+ if (model.isDefault === true)
764
+ defaultModel = model.id;
765
+ return {
766
+ id: model.id,
767
+ display_name: typeof model.displayName === "string" ? model.displayName : null,
768
+ efforts,
769
+ default_effort: typeof model.defaultReasoningEffort === "string" ? model.defaultReasoningEffort : null,
770
+ hidden: model.hidden === true,
771
+ };
772
+ });
773
+ });
774
+ return checkedCatalog("Codex", {
775
+ source: "codex app-server model/list",
776
+ harness_version: null,
777
+ default_model: defaultModel,
778
+ adapter_efforts: {},
779
+ models,
780
+ });
781
+ }
@@ -6,6 +6,7 @@ import * as path from "node:path";
6
6
  import { randomBytes } from "node:crypto";
7
7
  import { AitermError } from "../errors.js";
8
8
  import { spawnAgentControlCommand } from "../agent-resolver.js";
9
+ import { checkedCatalog } from "../model-catalog.js";
9
10
  import { AGENT_DONE_POLL_MS, AGENT_EVENT_MAX_BYTES, agentEventPath, agentHarness, agentLineageFields, agentMetadataPath, createEmpty0600, readFileRange, safeStatSize, shq, sleep, subagentInstruction, writeAgentMetadata, writeScopeLaunchNote, } from "../agent-shared.js";
10
11
  const CURSOR_TRANSCRIPT_MATCH_MAX_BYTES = 1024 * 1024;
11
12
  const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
@@ -306,6 +307,9 @@ const CURSOR_AUTH_TIMEOUT_MS = 5_000;
306
307
  const CURSOR_MODELS_TIMEOUT_MS = 5_000;
307
308
  const CURSOR_MODELS_MAX_BYTES = 256 * 1024;
308
309
  export function cursorModelCatalog(bin, cwd) {
310
+ return cursorModelLines(bin, cwd).map((line) => line.id);
311
+ }
312
+ export function cursorModelLines(bin, cwd) {
309
313
  const result = spawnAgentControlCommand(bin, ["models"], cwd, {
310
314
  cwd,
311
315
  encoding: "utf8",
@@ -322,22 +326,14 @@ export function cursorModelCatalog(bin, cwd) {
322
326
  if (!/^Available models\s*$/m.test(text)) {
323
327
  throw new AitermError("Cursor model catalog の出力形式が不正です(Available models がありません)", 2);
324
328
  }
325
- const models = text.split(/\r?\n/)
326
- .map((line) => line.match(/^([^\s]+)\s+-\s+.+$/)?.[1] ?? null)
327
- .filter((value) => value !== null);
329
+ const models = text.split(/\r?\n/).flatMap((line) => {
330
+ const match = line.match(/^([^\s]+)\s+-\s+(.+)$/);
331
+ return match ? [{ id: match[1], label: match[2].replace(/[\u200b\s]+$/u, "") }] : [];
332
+ });
328
333
  if (models.length === 0)
329
334
  throw new AitermError("Cursor model catalog に利用可能なmodelがありません", 2);
330
335
  return models;
331
336
  }
332
- export function assertCursorModelAvailable(bin, cwd, model, effort) {
333
- const effective = cursorModelArgument(model, effort);
334
- const models = cursorModelCatalog(bin, cwd);
335
- const available = effort ? models.includes(effective) : models.includes(model) || models.some((id) => id.startsWith(`${model}-`));
336
- if (!available) {
337
- throw new AitermError(`Cursor model catalog に ${JSON.stringify(effective)} がありません。` +
338
- "modelとreasoning_effortから作る正規IDが存在しないため、別modelへfallbackせず中止しました", 2);
339
- }
340
- }
341
337
  const CURSOR_EFFORT_LABELS = {
342
338
  none: "None",
343
339
  low: "Low",
@@ -347,6 +343,75 @@ const CURSOR_EFFORT_LABELS = {
347
343
  "extra-high": "Extra High",
348
344
  max: "Max",
349
345
  };
346
+ // Cursorのcatalogは「素のmodel ID + effort(+ -fast)」を連結した完成形で並ぶ。Aitermは起動時にmodelとeffortを
347
+ // `${model}-${effort}` へ連結し、稼働中はparameter画面でeffortを選び直す。候補は素のIDとそのIDで作れるeffortにする。
348
+ // -fast版やeffortが途中に入るID(claude-4.6-sonnet-medium-thinking等)は連結で作れないので候補にしない
349
+ // (BellTeamの裁定 2026-09-03。使う時はeffortなしで完成形をmodelに指定する)。
350
+ const CURSOR_EFFORT_TOKENS = ["none", "minimal", "low", "medium", "high", "xhigh", "extra-high", "max"];
351
+ const CURSOR_EFFORT_SUFFIXES = [...CURSOR_EFFORT_TOKENS].sort((a, b) => b.length - a.length);
352
+ // parameter画面にラベルの無いminimalは稼働中に選び直せないので出さない(cursorEffortNavigation)。
353
+ const CURSOR_SELECTABLE_EFFORTS = CURSOR_EFFORT_TOKENS.filter((effort) => effort in CURSOR_EFFORT_LABELS);
354
+ function cursorBaseModel(id) {
355
+ const fast = id.endsWith("-fast");
356
+ let rest = fast ? id.slice(0, -"-fast".length) : id;
357
+ let effort = null;
358
+ for (const token of CURSOR_EFFORT_SUFFIXES) {
359
+ if (rest.endsWith(`-${token}`)) {
360
+ rest = rest.slice(0, -(token.length + 1));
361
+ effort = token;
362
+ break;
363
+ }
364
+ }
365
+ const parts = rest.split("-");
366
+ const embedded = parts.some((part) => CURSOR_EFFORT_TOKENS.includes(part) || part === "fast") ||
367
+ CURSOR_EFFORT_TOKENS.some((token) => token.includes("-") && rest.includes(`-${token}`));
368
+ return embedded ? null : { base: rest, effort, fast };
369
+ }
370
+ export function cursorModelChoices(bin, cwd) {
371
+ return cursorCatalogFromLines(cursorModelLines(bin, cwd));
372
+ }
373
+ /** `cursor-agent models` の各行(IDと表示名)から、素のmodel IDとeffortの候補を作る。 */
374
+ export function cursorCatalogFromLines(lines) {
375
+ const choices = new Map();
376
+ let defaultModel = null;
377
+ for (const line of lines) {
378
+ const parsed = cursorBaseModel(line.id);
379
+ if (!parsed)
380
+ continue;
381
+ const choice = choices.get(parsed.base) ?? { efforts: new Set(), label: null };
382
+ // -fast版だけにあるeffortは `${model}-${effort}` の連結で作れないので数えない。
383
+ if (parsed.effort && !parsed.fast)
384
+ choice.efforts.add(parsed.effort);
385
+ if (line.id === parsed.base) {
386
+ choice.label = line.label.replace(/\s*\([^)]*\)\s*$/, "");
387
+ if (/\((?:[^)]*,\s*)?default\)\s*$/.test(line.label))
388
+ defaultModel = parsed.base;
389
+ }
390
+ choices.set(parsed.base, choice);
391
+ }
392
+ return checkedCatalog("Cursor", {
393
+ source: "cursor-agent models",
394
+ harness_version: null,
395
+ default_model: defaultModel,
396
+ adapter_efforts: {},
397
+ models: [...choices].map(([id, choice]) => ({
398
+ id,
399
+ display_name: choice.label,
400
+ efforts: CURSOR_SELECTABLE_EFFORTS.filter((effort) => choice.efforts.has(effort)),
401
+ default_effort: null,
402
+ hidden: false,
403
+ })),
404
+ });
405
+ }
406
+ export function assertCursorModelAvailable(bin, cwd, model, effort) {
407
+ const effective = cursorModelArgument(model, effort);
408
+ const models = cursorModelCatalog(bin, cwd);
409
+ const available = effort ? models.includes(effective) : models.includes(model) || models.some((id) => id.startsWith(`${model}-`));
410
+ if (!available) {
411
+ throw new AitermError(`Cursor model catalog に ${JSON.stringify(effective)} がありません。` +
412
+ "modelとreasoning_effortから作る正規IDが存在しないため、別modelへfallbackせず中止しました", 2);
413
+ }
414
+ }
350
415
  export function cursorEffortNavigation(screen, effort) {
351
416
  const label = CURSOR_EFFORT_LABELS[effort.toLowerCase()];
352
417
  if (!label)
@@ -7,7 +7,8 @@ import * as os from "node:os";
7
7
  import * as path from "node:path";
8
8
  import { randomBytes, randomUUID } from "node:crypto";
9
9
  import { AitermError } from "../errors.js";
10
- import { spawnAgentControlCommand } from "../agent-resolver.js";
10
+ import { runAgentProtocolCommand, spawnAgentControlCommand } from "../agent-resolver.js";
11
+ import { catalogInvalid, catalogUnavailable, checkedCatalog, findJsonLine, processSummary } from "../model-catalog.js";
11
12
  import { shq, subagentInstruction, writeScopeLaunchNote, safeStatSize, readFileRange, sleep, agentMetadataPath, writeAgentMetadata, agentEventPath, createEmpty0600, agentLineageFields, AGENT_DONE_POLL_MS, AGENT_EVENT_MAX_BYTES, GROK_TRANSCRIPT_INCREMENT_MAX_BYTES, agentHarness, } from "../agent-shared.js";
12
13
  const GROK_MODELS_MAX_BYTES = 1024 * 1024;
13
14
  const GROK_MODELS_TIMEOUT_MS = 15_000;
@@ -60,6 +61,71 @@ export function grokModelCatalog(bin, cwd) {
60
61
  throw new AitermError("Grok model catalog に利用可能なmodelがありません", 2);
61
62
  return models;
62
63
  }
64
+ const GROK_AGENT_CATALOG_TIMEOUT_MS = 30_000;
65
+ /**
66
+ * GrokのmodelとReasoning effortの候補。`grok models` はeffortを出さないため、公式のagent protocol(ACP)の
67
+ * `initialize` 応答にある `_meta.modelState` を読む。`session/new` は同じ候補を返すがsessionを保存するので使わない。
68
+ * promptもsessionも作らず、推論は起きない。
69
+ */
70
+ export async function grokModelChoices(bin, cwd) {
71
+ const request = { jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: 1, clientCapabilities: {} } };
72
+ // 応答前に標準入力を閉じると、grok 1.0.41は応答せずに正常終了することがある。応答を読むまで開けておく。
73
+ const result = await runAgentProtocolCommand(bin, ["agent", "--no-leader", "stdio"], {
74
+ cwd,
75
+ env: process.env,
76
+ input: JSON.stringify(request) + "\n",
77
+ until: '"id":1',
78
+ timeout: GROK_AGENT_CATALOG_TIMEOUT_MS,
79
+ maxBuffer: GROK_MODELS_MAX_BYTES,
80
+ });
81
+ if (result.error || result.status !== 0) {
82
+ throw catalogUnavailable("Grok", result.error?.message || result.stderr?.trim() || `exit=${result.status ?? "unknown"}`);
83
+ }
84
+ return grokCatalogFromInitialize(result.stdout, processSummary(result));
85
+ }
86
+ /** `grok agent stdio` のinitialize応答(JSON Lines)からmodel候補を作る。 */
87
+ export function grokCatalogFromInitialize(stdout, summary = "") {
88
+ const response = findJsonLine(stdout, (value) => value?.id === 1 && ("result" in value || "error" in value));
89
+ if (!response)
90
+ throw catalogInvalid("Grok", `initializeの応答がありません${summary ? `(${summary})` : ""}`);
91
+ if (response.error)
92
+ throw catalogUnavailable("Grok", `initializeが拒否されました: ${JSON.stringify(response.error)}`);
93
+ const meta = response.result?._meta;
94
+ const state = meta?.modelState;
95
+ if (!state || !Array.isArray(state.availableModels))
96
+ throw catalogInvalid("Grok", "initializeの応答に _meta.modelState.availableModels がありません");
97
+ const models = state.availableModels.map((model) => {
98
+ if (typeof model?.modelId !== "string")
99
+ throw catalogInvalid("Grok", "modelIdの無いmodelがあります");
100
+ const info = model._meta ?? {};
101
+ const levels = info.supportsReasoningEffort === false ? [] : info.reasoningEfforts;
102
+ if (!Array.isArray(levels))
103
+ throw catalogInvalid("Grok", `${model.modelId} に reasoningEfforts がありません`);
104
+ const efforts = levels.map((level) => {
105
+ const value = level?.value ?? level?.id;
106
+ if (typeof value !== "string")
107
+ throw catalogInvalid("Grok", `${model.modelId} のreasoning effortを読めません`);
108
+ return value;
109
+ });
110
+ const marked = levels.find((level) => level?.default === true);
111
+ const defaultEffort = (marked?.value ?? marked?.id ?? info.reasoningEffort ?? null);
112
+ return {
113
+ id: model.modelId,
114
+ display_name: typeof model.name === "string" ? model.name : null,
115
+ efforts,
116
+ // 候補に無い既定値は黙って捨てず、checkedCatalogで形式異常にする。
117
+ default_effort: efforts.length > 0 ? defaultEffort : null,
118
+ hidden: false,
119
+ };
120
+ });
121
+ return checkedCatalog("Grok", {
122
+ source: "grok agent stdio initialize (_meta.modelState)",
123
+ harness_version: typeof meta.agentVersion === "string" ? meta.agentVersion : null,
124
+ default_model: typeof state.currentModelId === "string" ? state.currentModelId : null,
125
+ adapter_efforts: {},
126
+ models,
127
+ });
128
+ }
63
129
  export function assertGrokModelAvailable(bin, cwd, model) {
64
130
  const models = grokModelCatalog(bin, cwd);
65
131
  if (!models.includes(model)) {
package/dist/index.js CHANGED
@@ -12,6 +12,7 @@
12
12
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
13
13
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
14
14
  import { z } from "zod";
15
+ import { agentModelsResult } from "./model-catalog.js";
15
16
  import * as core from "./core.js";
16
17
  import { runtimeErrorStoreDiagnostic } from "./runtime-error-store.js";
17
18
  import { createRequire } from "node:module";
@@ -763,6 +764,47 @@ registerRemoteAwareTool("agent_configure", {
763
764
  return fail(e);
764
765
  }
765
766
  });
767
+ const agentModelChoiceSchema = z.object({
768
+ id: z.string().describe("agent_launch/agent_configureのmodelへ渡すID"),
769
+ display_name: z.string().nullable(),
770
+ efforts: z.array(z.string()).describe("このmodelで選べるreasoning_effort。空ならeffort非対応"),
771
+ default_effort: z.string().nullable(),
772
+ hidden: z.boolean().describe("harnessが一覧で隠すmodel(Codexのinclude_hidden時だけtrueがある)"),
773
+ });
774
+ registerRemoteAwareTool("agent_models", {
775
+ description: "harnessが今返すmodelとreasoning effortの候補を、そのharnessの公式の一覧から取得する。promptもturnも送らず推論を消費しない。" +
776
+ "Codexはapp-server model/list、Claude Codeはstream-json initializeのmodels、Grokはagent stdio initializeのmodelState、" +
777
+ "Cursorはcursor-agent modelsを素のmodel IDとeffortへ分けたもの。返るidとeffortsはagent_launch/agent_configureへそのまま渡せる。" +
778
+ "取得不能はMODEL_CATALOG_UNAVAILABLE、形式異常はMODEL_CATALOG_INVALIDのエラーで返し、別の一覧へfallbackしない。" +
779
+ "remoteを付けると別端末のharnessの候補を返す。",
780
+ inputSchema: {
781
+ harness: z.enum(["claude-code", "codex-cli", "grok-cli", "cursor-cli"]).describe("候補を取得するharness"),
782
+ cwd: z.string().nullish().describe("CLIを実行する作業ディレクトリ(絶対パス・任意)。project設定で候補が変わるharness向け"),
783
+ include_hidden: z.boolean().optional().describe("Codexが一覧で隠すmodelも返す(既定false)。他のharnessには隠すmodelが無い"),
784
+ },
785
+ outputSchema: {
786
+ schema: z.literal("aiterm.agent-models.v1"),
787
+ harness: z.enum(["claude-code", "codex-cli", "grok-cli", "cursor-cli"]),
788
+ source: z.string().describe("取得に使ったharnessの公式の入口"),
789
+ harness_version: z.string().nullable(),
790
+ default_model: z.string().nullable().describe("model省略時にharnessが使うmodel。一覧のIDで表せなければnull"),
791
+ efforts: z.array(z.string()).describe("全modelのeffortの和"),
792
+ adapter_efforts: z.record(z.string(), z.string()).describe("harnessの一覧には無く、Aitermのadapterが足したeffortと理由"),
793
+ models: z.array(agentModelChoiceSchema),
794
+ },
795
+ }, async ({ harness, cwd, include_hidden }) => {
796
+ try {
797
+ const catalog = await core.listAgentModels(kindForHarness(harness), { cwd, include_hidden });
798
+ const result = agentModelsResult(harness, catalog);
799
+ return {
800
+ content: [{ type: "text", text: JSON.stringify(result) }],
801
+ structuredContent: { ...result },
802
+ };
803
+ }
804
+ catch (e) {
805
+ return fail(e);
806
+ }
807
+ });
766
808
  // 新規APIの選択軸はharness。旧4 launcher名は移行期間中の薄いaliasとして同じ実装へ流す。
767
809
  const kindForHarness = (harness) => harness === "claude-code" ? "claude" : harness === "codex-cli" ? "codex" : harness === "cursor-cli" ? "cursor" : "grok";
768
810
  const agentModelDesc = (kind) => kind === "claude"
@@ -0,0 +1,79 @@
1
+ // agent_modelsの共通の形。harnessごとの取得口・出力形式・model IDとeffortの変換は src/harnesses/ が持ち、
2
+ // ここはharness中立の型、effortの並び、形の検証だけを持つ。
3
+ import { AitermError } from "./errors.js";
4
+ // 並びはBellTeamの候補表示と同じ。未知のeffortは既知のものの後ろへ、出てきた順で置く。
5
+ const EFFORT_ORDER = ["none", "minimal", "low", "medium", "high", "xhigh", "extra-high", "max", "ultra", "ultracode"];
6
+ export function sortEfforts(efforts) {
7
+ const unique = [...new Set(efforts)];
8
+ const rank = (effort) => {
9
+ const index = EFFORT_ORDER.indexOf(effort);
10
+ return index === -1 ? EFFORT_ORDER.length : index;
11
+ };
12
+ return unique.map((effort, order) => ({ effort, order })).sort((a, b) => rank(a.effort) - rank(b.effort) || a.order - b.order)
13
+ .map(item => item.effort);
14
+ }
15
+ export function catalogUnavailable(label, detail) {
16
+ return new AitermError(`MODEL_CATALOG_UNAVAILABLE: ${label} のmodel一覧を取得できません: ${detail}`, 2);
17
+ }
18
+ export function catalogInvalid(label, detail) {
19
+ return new AitermError(`MODEL_CATALOG_INVALID: ${label} のmodel一覧の形式が不正です: ${detail}`, 2);
20
+ }
21
+ /** adapterが作った一覧を検証する。空の一覧、重複ID、空の値は形式異常として止める。 */
22
+ export function checkedCatalog(label, catalog) {
23
+ if (catalog.models.length === 0)
24
+ throw catalogInvalid(label, "利用可能なmodelがありません");
25
+ const seen = new Set();
26
+ for (const model of catalog.models) {
27
+ if (!model.id.trim())
28
+ throw catalogInvalid(label, "空のmodel IDがあります");
29
+ if (seen.has(model.id))
30
+ throw catalogInvalid(label, `model ID ${JSON.stringify(model.id)} が重複しています`);
31
+ seen.add(model.id);
32
+ if (model.efforts.some(effort => !effort.trim()))
33
+ throw catalogInvalid(label, `${model.id} に空のeffortがあります`);
34
+ if (model.default_effort !== null && !model.efforts.includes(model.default_effort)) {
35
+ throw catalogInvalid(label, `${model.id} の既定effort ${JSON.stringify(model.default_effort)} が候補にありません`);
36
+ }
37
+ }
38
+ if (catalog.default_model !== null && !seen.has(catalog.default_model)) {
39
+ throw catalogInvalid(label, `既定model ${JSON.stringify(catalog.default_model)} が一覧にありません`);
40
+ }
41
+ return { ...catalog, models: catalog.models.map(model => ({ ...model, efforts: sortEfforts(model.efforts) })) };
42
+ }
43
+ /** 公開toolの結果。harness全体のeffortsは各modelの和で、modelを省略した時や一覧に無いmodelで使える範囲の目安。 */
44
+ export function agentModelsResult(harness, catalog) {
45
+ return {
46
+ schema: "aiterm.agent-models.v1",
47
+ harness,
48
+ source: catalog.source,
49
+ harness_version: catalog.harness_version,
50
+ default_model: catalog.default_model,
51
+ efforts: sortEfforts(catalog.models.flatMap(model => model.efforts)),
52
+ adapter_efforts: catalog.adapter_efforts,
53
+ models: catalog.models,
54
+ };
55
+ }
56
+ /** 応答が見つからない時の原因調査用。本文は出さず、終了状態と長さ、stderrの末尾だけを返す。 */
57
+ export function processSummary(result) {
58
+ const stderr = (result.stderr ?? "").trim().split(/\r?\n/).slice(-3).join(" / ").slice(-300);
59
+ return `exit=${result.status ?? "null"} signal=${result.signal ?? "null"} stdout=${(result.stdout ?? "").length}bytes` +
60
+ (stderr ? ` stderr=${JSON.stringify(stderr)}` : "");
61
+ }
62
+ /** JSON Linesの出力から、条件に合う最初の行を返す。JSONでない行(起動時の案内等)は読み飛ばす。 */
63
+ export function findJsonLine(stdout, match) {
64
+ for (const line of stdout.split(/\r?\n/)) {
65
+ const text = line.trim();
66
+ if (!text.startsWith("{"))
67
+ continue;
68
+ let value;
69
+ try {
70
+ value = JSON.parse(text);
71
+ }
72
+ catch {
73
+ continue;
74
+ }
75
+ if (match(value))
76
+ return value;
77
+ }
78
+ return null;
79
+ }
@@ -200,9 +200,10 @@ export function spawnInMacGuiWhenOutsideAqua(bin, args, options) {
200
200
  if (!macOutsideAqua())
201
201
  return null;
202
202
  const timeout = options.timeout ?? 30_000;
203
- return runInMacGui([bin, ...args], options.env ?? process.env, String(options.cwd ?? process.cwd()), timeout, `launchd経由の ${path.basename(bin)} ${args.join(" ")} が${timeout}ms以内に終わりませんでした`);
203
+ const input = options.input === undefined ? undefined : typeof options.input === "string" ? options.input : Buffer.from(options.input.buffer, options.input.byteOffset, options.input.byteLength);
204
+ return runInMacGui([bin, ...args], options.env ?? process.env, String(options.cwd ?? process.cwd()), timeout, `launchd経由の ${path.basename(bin)} ${args.join(" ")} が${timeout}ms以内に終わりませんでした`, input, options.until);
204
205
  }
205
- function runInMacGui(argv, env, cwd, timeoutMs, timeoutMessage) {
206
+ function runInMacGui(argv, env, cwd, timeoutMs, timeoutMessage, input, until) {
206
207
  const uid = process.getuid?.();
207
208
  if (uid === undefined)
208
209
  return null;
@@ -217,6 +218,16 @@ function runInMacGui(argv, env, cwd, timeoutMs, timeoutMessage) {
217
218
  const label = `dev.aiterm.gui-job.${process.pid}.${path.basename(dir)}`;
218
219
  const plist = path.join(dir, "job.plist");
219
220
  try {
221
+ // launchdの仕事は標準入力を持たない。入力を渡す時はファイルに書き、同じ仕事の中でつなぐ。
222
+ // untilを渡した時は、その文字列が出力に現れるまで標準入力を開けておく(閉じると応答前に終わるCLIがある)。
223
+ if (input !== undefined) {
224
+ const inputFile = path.join(dir, "in");
225
+ fs.writeFileSync(inputFile, input, { mode: 0o600 });
226
+ argv = until === undefined
227
+ ? ["/bin/sh", "-c", 'f=$1; shift; exec "$@" <"$f"', "aiterm-gui-input", inputFile, ...argv]
228
+ : ["/bin/sh", "-c", 'f=$1; m=$2; o=$3; shift 3; { cat "$f"; until grep -qF -- "$m" "$o" 2>/dev/null; do sleep 0.05; done; } | "$@"',
229
+ "aiterm-gui-input", inputFile, until, path.join(dir, "out"), ...argv];
230
+ }
220
231
  fs.writeFileSync(plist, macGuiTmuxPlist(label, dir, argv, env, cwd));
221
232
  const boot = spawnSync("launchctl", ["bootstrap", `gui/${uid}`, plist], { encoding: "utf8", timeout: 10000 });
222
233
  // 画面にログインしていない等で gui domain が無ければ、今までどおり直接実行する。
package/docs/DESIGN.md CHANGED
@@ -236,6 +236,22 @@ Aitermはtransport、schema、turn相関だけを検証する。command/prompt
236
236
  harness所有credentialの内容・権限・linkも検査しない。command policyとcredential policyは、実行する
237
237
  shell、接続先、各harnessの公式CLIが所有する。
238
238
 
239
+ ## Model catalog
240
+
241
+ `agent_models`は、harnessが今選べるmodelとreasoning effortを、そのharness自身の一覧から返す。
242
+ BellTeam等の画面は、保存した固定の一覧ではなく、agentを実際に動かす端末のharnessが返す候補を使う。
243
+ 取得口、出力形式、model IDとeffortの変換は各harness adapterが持ち、`src/model-catalog.ts`は
244
+ harness中立の形、effortの並び、形式検証だけを持つ。stdinを渡す起動のOS差(macOSのAqua外の
245
+ launchd経由、Windowsの`.cmd`)は`src/agent-resolver.ts`/`src/tmux-runtime.ts`が持つ。
246
+
247
+ 取得はpromptもturnも送らない。Codexは公式App Serverの`model/list`、Claude Codeはstream-jsonの
248
+ 制御要求`initialize`(hook・MCP server・session保存を止める)、Grokは`grok agent stdio`の
249
+ `initialize`、Cursorは`cursor-agent models`を使う。返すmodel IDとeffortは`agent_launch`/
250
+ `agent_configure`へそのまま渡せる形にし、Cursorは`<model>-<effort>`の連結で作れる組だけを返す。
251
+ harnessの一覧に無い値をadapterが足す時(Claudeの`ultracode`)は`adapter_efforts`で出所を示す。
252
+ 取得不能と形式異常は明示errorにし、固定一覧や別harnessへfallbackしない。判断の記録は
253
+ ADR 0074(repositoryの`docs/adr/0074-agent-model-catalog.md`)。
254
+
239
255
  ## Failure and recovery
240
256
 
241
257
  入力が64KiBを超える、送信lockが残る、harnessがblocking UIにいる、model catalogが一致しない等の
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiterm-mcp",
3
- "version": "0.43.7",
3
+ "version": "0.44.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": [