aiterm-mcp 0.58.2 → 0.58.4

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,51 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.58.4] - 2026-10-12
11
+
12
+ ### 修正
13
+
14
+ - AitermのMCP processが、互いの生き死にを確かめるためにprocessを起こし続け、端末を重くしていた件を直す(ADR 0112)。
15
+ 親への配送の回収は、MCP process 1個につき2本、5秒おきに回り、ほかの持ち主の開始時刻を60秒おきに照合していた
16
+ (WindowsはPowerShell 7とWMI、ほかは`ps`)。照合の時刻が持ち主ごとにずれるので、回数がMCP processの数の掛け算で増えていた。
17
+ Windowsの端末(process 1312個、AitermのMCP process 23個)で、Aitermだけで20秒に78回のPowerShellが起き、
18
+ WMIの側のprocessがCPUを中央2.8コア使っていた。
19
+ - 持ち主は、自分の保存場所へ「生きている印」(`active/<持ち主>/alive`の時刻)を15秒おきに付ける。
20
+ 印が60秒以内の持ち主は、OSへ聞かずに生きていると読む。pidを使い回した別のprocessは、この印を書かない。
21
+ - 印の無い持ち主(0.58.3までの版)と、印が古い持ち主だけ、今までどおり開始時刻で照合する。照合する回は全員をまとめ、1本が1分に1回まで。
22
+ - Windowsで1個のprocessの開始時刻を読む時は、WMIを使わず、.NETのprocessの読みを使う(1個1.5ミリ秒。値は今までと同じ)。
23
+ - Windowsの実物で、MCP processを6個・150秒動かした時のPowerShellの起動は、0.58.3が80回、この版は0回。
24
+ - `owner.json`の形、保存場所の名前、配送の記録と返りは変わらない。前の版のMCP processが残っている間、その分の照合は続く。
25
+ - 共通部品`aiterm-steer-delivery`を0.4.5へ上げる。AitermのClaude Codeの親への配送で、受信hookが走るたびに端末の全processの一覧を
26
+ 取っていたのが、確かめるprocessだけを読む形になる。
27
+
28
+ ## [0.58.3] - 2026-10-12
29
+
30
+ ### 修正
31
+
32
+ - `agent_configure`が、Claude Codeの席のmodel/effortを替える時に、端末の`~/.claude/settings.json`の既定を書き換えていた件を直す(ADR 0111)。
33
+ 今までは`/model <名前>`と`/effort <値>`を打っていた。Claude Code(2.1.293・2.1.296で見た)は、この2つで選んだ値を
34
+ 「新しい会話の既定」として保存する(`model`と、`modelSettings.<機種>.effortLevel`)。1つの席を替えただけで、その端末でこの後に
35
+ 引数なしで立つ全部のClaude Codeの既定が変わり、利用者が自分で決めた既定も上書きしていた。
36
+ - `/model`を引数なしで開いた選ぶ画面を読み、行とeffortを合わせて、`s`(use this session only)で選ぶ。Enter(set as default)は押さない。
37
+ - 会話がcacheされている席で出る「Switch model?」の確かめは、1番(Yes, switch to …)が選ばれている時だけ進める。
38
+ - `model`は、catalogのID(`claude-opus-5-5`)、別名(`opus`)、表示名(`Opus 5.5`)のどれでも受ける。
39
+ - 選べない時は、選ぶ画面をEscで閉じて、何も変えずに断る。`s`の選び方が無い版では、保存する選び方へ落ちずに断る
40
+ (`CLAUDE_MODEL_SESSION_ONLY_UNAVAILABLE`)。選ぶ画面に無いmodelは`CLAUDE_MODEL_NOT_IN_PICKER`(文に選べる機種を並べる)、
41
+ 行はあるが選べないmodel(席のClaude Codeが古い時の「Update Claude Code to use this model」)は`CLAUDE_MODEL_NOT_SELECTABLE`、
42
+ そのmodelで選べないeffortは`CLAUDE_EFFORT_NOT_SELECTABLE`。今までは、Claude Codeが受け付けない値でも成功で返っていた。
43
+ - `reasoning_effort`が`low`/`medium`/`high`/`xhigh`/`max`/`ultracode`のどれでもない時は、打つ前に断る。
44
+ - 引数と返りの形(`aiterm.agent-configure-result.v1`)、Codex・Grok・Cursorの席の動きは変わらない。
45
+ - 「この会話だけ」の替えなので、席を閉じて立て直すと端末の既定へ戻る。立て直す時は`agent_launch`へ同じ値を渡す。
46
+ - 選ぶ画面に無いmodel(`opus[1m]`のような名前、古い機種のID)は、動いている席では替えられなくなる。今までは`/model <名前>`で通っていた
47
+ (端末の既定へ保存されていた)。席を閉じて、`agent_launch`の`model`で渡して立て直す。
48
+
49
+ ### 追加
50
+
51
+ - `pty_list`の構造の返り(`aiterm.pty-list-result.v1`)の席ごとの項目に、harness側の会話ID`vendor_session_id`を足す。
52
+ Claude CodeとGrokは起動時に`--session-id`で渡した値(席の中で会話を移した後は追わない)、CodexとCursorは掴んだ後の値。
53
+ 通常PTYと、まだ分からない席は`null`。文字の返りには前から載っていた。`remote`の先が0.58.2以前の時は、項目そのものが無い。
54
+
10
55
  ## [0.58.2] - 2026-10-11
11
56
 
12
57
  ### 修正
@@ -2497,7 +2542,9 @@ prototype (preserved under `prototype/python/` as the porting source and referen
2497
2542
  `ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
2498
2543
  provenance.
2499
2544
 
2500
- [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.58.2...HEAD
2545
+ [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.58.4...HEAD
2546
+ [0.58.4]: https://github.com/kitepon/aiterm-mcp/compare/v0.58.3...v0.58.4
2547
+ [0.58.3]: https://github.com/kitepon/aiterm-mcp/compare/v0.58.2...v0.58.3
2501
2548
  [0.58.2]: https://github.com/kitepon/aiterm-mcp/compare/v0.58.1...v0.58.2
2502
2549
  [0.58.1]: https://github.com/kitepon/aiterm-mcp/compare/v0.58.0...v0.58.1
2503
2550
  [0.58.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.57.4...v0.58.0
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.58.2** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
212
+ **状態:** 開発継続中 · 現行公開版 **v0.58.4** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
213
213
 
214
214
  ### 更新と巻き戻し
215
215
 
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.58.2** · 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.58.4** · 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
 
@@ -622,7 +622,7 @@ continue to use `claude_approval`.
622
622
  | `pty_read` | Read output, token-reduced (incremental by default) | `session_id`, `wait`, `until`, `until_regex`, `timeout`, `screen`, `full`, `lines`, `line_range`, `raw`, `rtk`, `agent_transcript`, `operation_id` |
623
623
  | `pty_key` | Send a control key | `session_id`, `key` (`C-c`/`Enter`/`Up`…) |
624
624
  | `pty_close` | Close idempotently; return `closed` / `already_closed` | `session_id` |
625
- | `pty_list` | Text and structured session list, with explicitly requested non-secret environment values | `env_keys?` |
625
+ | `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?` |
626
626
  | `pty_observe` | Pane/harness liveness, native process identity, state, and activity | `session_id`, `cursor?` |
627
627
  | `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?` |
628
628
  | `agent_auth` | 公式CLIの認証を開始・確認・取消し、公式URL・device code・入力待ちを返す | `harness`, `action`, `session_id?`, `cwd?`, `env_vars?`, `relogin?` |
@@ -664,6 +664,8 @@ enables collection on that host.
664
664
 
665
665
  `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.
666
666
 
667
+ 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`を渡す。
668
+
667
669
  `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と同じ標準対応。
668
670
 
669
671
  `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`すれば元の資格情報が残る(無効な資格情報で確認)。
package/dist/core.js CHANGED
@@ -23,7 +23,7 @@ import { sleep, currentUid, runtimeStateBase, safeStatSize, readFileRange, write
23
23
  import { catalogUnavailable } from "./model-catalog.js";
24
24
  import { grokAuthPlan, grokAuthStatus, grokAuthPane, realGrokHome, resolveAndValidateGrokAuth, assertGrokModelAvailable, grokModelChoices, grokEventsTranscript, latestGrokCompletion, grokTurnSettled, observeGrokDone, buildGrokAgentCmd, grokLaunchNote, grokEnvTokens, grokTuiReady, grokTuiBusy, grokPaneObservation, grokRateLimitDialog, grokPromptAwaitingAccept, grokUnacceptedPromptRestored, grokStartupAction, grokLaunchBlockingDialog, assertGrokSandboxNotRejected, GROK_COMPOSER_MARKER_RE, grokFooterHasConfiguration, grokTranscriptText, createGrokAgentMetadata, } from "./harnesses/grok.js";
25
25
  import { codexAuthPlan, codexAuthStatus, codexAuthPane, bindCodexTranscriptSession, latestCodexCompletion, codexTurnSettled, codexRunningTurn, observeCodexDone, buildCodexAgentCmd, codexLaunchNote, codexTuiReady, codexPaneObservation, codexTuiBusy, codexHelperProcess, codexRateLimitModelSwitchDialog, codexApprovalDialog, codexStartupAction, CODEX_COMPOSER_MARKER_RE, codexModelChoice, codexEffortChoice, codexMoreReasoningChoice, codexTranscriptText, createCodexAgentMetadata, codexModelChoices, } from "./harnesses/codex.js";
26
- import { claudeAuthPlan, claudeAuthStatus, claudeAuthPane, 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 { claudeAuthPlan, claudeAuthStatus, claudeAuthPane, OPERATION_ID_RE, CLAUDE_RESULT_MAX_BYTES, CLAUDE_EFFORTS, agentManagedClaudeSettingsPath, agentClaudeResultPath, agentClaudeOperationPath, agentClaudeApprovalReceiptPath, agentClaudeDispatchReceiptPath, validateOperationId, readClaudeResultText, assertClaudeAuthenticationReady, claudeModelChoices, buildClaudeAgentCmd, claudeLaunchNote, claudeTuiReady, claudePaneObservation, claudeUsageLimit, claudeModelPicker, claudeModelPickerRow, claudeModelSwitchConfirm, claudeModelSwitchResult, claudeStartupAction, claudeLoginMethodMenu, CLAUDE_COMPOSER_MARKER_RE, createClaudeAgentMetadata, claudeSessionTranscriptPath, claudeApiErrorFromLine, claudeApiErrorAfter, } from "./harnesses/claude.js";
27
27
  import { cursorAuthPlan, cursorAuthStatus, cursorAuthPane, 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";
28
28
  import { readInterimWords, recordInterimBoundary } from "./interim-words.js";
29
29
  import { resolveAgentBin, resolveThroughlineBin, runThroughlineHandoffContext, isWindowsNativeExecutable, agentBinForPaneShell, resolveWinPaneShell } from "./agent-resolver.js";
@@ -1338,7 +1338,8 @@ export function listSessionsResult(envKeys = []) {
1338
1338
  }
1339
1339
  const meta = tryLoadAgentMetadata(name);
1340
1340
  sessions.push({ session_id: name, current_command: normalizePaneCommand(command), attached: Number(attached) > 0,
1341
- width: Number(width), height: Number(height), harness: meta ? agentHarness(meta.kind) : null, environment });
1341
+ width: Number(width), height: Number(height), harness: meta ? agentHarness(meta.kind) : null,
1342
+ vendor_session_id: meta?.vendor_session_id ?? null, environment });
1342
1343
  }
1343
1344
  return { schema: "aiterm.pty-list-result.v1", observed_at: new Date().toISOString(), sessions };
1344
1345
  }
@@ -4119,6 +4120,166 @@ export async function listAgentModels(kind, options = {}) {
4119
4120
  case "cursor": return cursorModelChoices(bin, cwd);
4120
4121
  }
4121
4122
  }
4123
+ const CLAUDE_MODEL_PICKER_TIMEOUT_MS = 8_000;
4124
+ const CLAUDE_MODEL_PICKER_KEY_TIMEOUT_MS = 1_500;
4125
+ const CLAUDE_MODEL_SWITCH_TIMEOUT_MS = 15_000;
4126
+ const CLAUDE_EFFORT_ORDER = ["low", "medium", "high", "xhigh", "max"];
4127
+ /**
4128
+ * Claude Codeの席のmodel/effortを、この会話だけで替える(ADR 0111)。
4129
+ * `/model <名前>` と `/effort <値>` は、選んだ値を端末の ~/.claude/settings.json へ新しい会話の既定として保存する
4130
+ * (2.1.296で確かめた)。引数なしの `/model` の選ぶ画面で s を押すと保存されないので、画面を1歩ずつ読んでそちらを打つ。
4131
+ * 保存しない選び方が画面に無い時、行やeffortを選べない時は、Escで閉じて何も変えずに断る。
4132
+ */
4133
+ async function configureClaudeForSessionOnly(name, meta, model, effort) {
4134
+ const screen = () => captureScreen(name, AGENT_TUI_READY_LINES);
4135
+ const until = async (read, timeoutMs) => {
4136
+ const deadline = performance.now() + timeoutMs;
4137
+ do {
4138
+ const got = read();
4139
+ if (got)
4140
+ return got;
4141
+ await sleep(100);
4142
+ } while (performance.now() < deadline);
4143
+ return null;
4144
+ };
4145
+ // Escは、選ぶ画面か確かめの画面が出ている間だけ打つ(入力欄へ戻った後のEscは、ほかの操作を取り消してしまう)。
4146
+ const refuse = async (code, reason) => {
4147
+ for (let i = 0; i < 3; i++) {
4148
+ const now = screen();
4149
+ if (!claudeModelPicker(now) && !claudeModelSwitchConfirm(now))
4150
+ break;
4151
+ sendMenuChoice(name, "Escape");
4152
+ await sleep(300);
4153
+ }
4154
+ throw new AitermError(`${code} vendor=claude session=${name}\n${reason}。modelとreasoning_effortは変えていません。`, 2);
4155
+ };
4156
+ const picker = () => claudeModelPicker(screen());
4157
+ const cursorOf = (p) => p?.rows.find((row) => row.cursor) ?? null;
4158
+ // 1回押して、画面の今の行かeffortが変わるまで待つ。変わらなければnull(端で止まった・キーが落ちた)。
4159
+ // キーが落ちる事があるので、変わらなければ1度だけ押し直す。
4160
+ const press = async (key, changed) => {
4161
+ for (let attempt = 0; attempt < 2; attempt++) {
4162
+ sendMenuChoice(name, key);
4163
+ const next = await until(() => { const p = picker(); return p && changed(p) ? p : null; }, CLAUDE_MODEL_PICKER_KEY_TIMEOUT_MS);
4164
+ if (next)
4165
+ return next;
4166
+ }
4167
+ return null;
4168
+ };
4169
+ await sendAgentPromptText(name, "/model");
4170
+ let now = await until(picker, CLAUDE_MODEL_PICKER_TIMEOUT_MS);
4171
+ if (!now) {
4172
+ // 読めない形の画面が開いたままだと、席が次の文を受け取れない。入力待ちでも動作中でもない時だけ、Escを1度打って閉じる。
4173
+ let state = claudePaneObservation(screen());
4174
+ if (state.state === "unknown") {
4175
+ sendMenuChoice(name, "Escape");
4176
+ await sleep(500);
4177
+ state = claudePaneObservation(screen());
4178
+ }
4179
+ throw new AitermError(`CLAUDE_MODEL_PICKER_NOT_SHOWN vendor=claude session=${name} state=${state.state}/${state.reason}\n` +
4180
+ "Claude Codeの /model の選ぶ画面を読めません(Claude Codeの版で画面の形が変わった見込み)。modelとreasoning_effortは変えていません。", 2);
4181
+ }
4182
+ if (!now.session_only) {
4183
+ return refuse("CLAUDE_MODEL_SESSION_ONLY_UNAVAILABLE", "このClaude Codeの /model の選ぶ画面に、この会話だけで使う選び方(s to use this session only)がありません。" +
4184
+ "端末の既定を書き換える選び方は使いません");
4185
+ }
4186
+ let targetIndex = null;
4187
+ if (model) {
4188
+ // 見えていない行は、下へ送りながら集める(一周するか、端で止まるまで)。
4189
+ const known = new Map(now.rows.map((row) => [row.index, row]));
4190
+ const start = cursorOf(now);
4191
+ if (!start)
4192
+ return refuse("CLAUDE_MODEL_PICKER_UNREADABLE", "選ぶ画面の今の行を読めません");
4193
+ let at = start.index;
4194
+ const seen = new Set([at]);
4195
+ for (let i = 0; i < 80; i++) {
4196
+ const before = at;
4197
+ const next = await press("Down", (p) => { const c = cursorOf(p); return !!c && c.index !== before; });
4198
+ if (!next)
4199
+ break;
4200
+ now = next;
4201
+ for (const row of next.rows)
4202
+ known.set(row.index, row);
4203
+ at = cursorOf(next).index;
4204
+ if (seen.has(at))
4205
+ break;
4206
+ seen.add(at);
4207
+ }
4208
+ const target = claudeModelPickerRow([...known.values()], model);
4209
+ if (!target) {
4210
+ const names = [...known.values()].sort((a, b) => a.index - b.index).map((row) => row.label).join(", ");
4211
+ return refuse("CLAUDE_MODEL_NOT_IN_PICKER", `Claude Codeの /model の選ぶ画面に ${model} がありません(選べる機種: ${names})`);
4212
+ }
4213
+ for (let i = 0; i < 80 && at !== target.index; i++) {
4214
+ const before = at;
4215
+ const next = await press(target.index > at ? "Down" : "Up", (p) => { const c = cursorOf(p); return !!c && c.index !== before; });
4216
+ if (!next)
4217
+ break;
4218
+ now = next;
4219
+ at = cursorOf(next).index;
4220
+ }
4221
+ if (at !== target.index)
4222
+ return refuse("CLAUDE_MODEL_PICKER_UNREADABLE", `選ぶ画面で ${target.label} の行へ動けません`);
4223
+ // 起きた後にClaude Codeが更新された席では、「Update Claude Code to use this model」の行が出る。その行にはEnterもsも無い。
4224
+ if (!now.session_only) {
4225
+ return refuse("CLAUDE_MODEL_NOT_SELECTABLE", `${target.label} は、この席のClaude Codeでは選べません(選ぶ画面のその行に、選ぶためのキーが出ていません。席のClaude Codeが古い見込み)`);
4226
+ }
4227
+ targetIndex = target.index;
4228
+ }
4229
+ if (effort) {
4230
+ const wanted = CLAUDE_EFFORT_ORDER.indexOf(effort);
4231
+ const row = cursorOf(now)?.label ?? model ?? "このmodel";
4232
+ // そのmodelに無い段は飛ばされて行き来になるので、回数で打ち切る。
4233
+ for (let i = 0; i < CLAUDE_EFFORT_ORDER.length + 3 && now.effort !== effort; i++) {
4234
+ const current = now.effort;
4235
+ const from = current === null ? -1 : CLAUDE_EFFORT_ORDER.indexOf(current);
4236
+ if (from < 0)
4237
+ return refuse("CLAUDE_EFFORT_NOT_SELECTABLE", `${row} では reasoning_effort を選べません`);
4238
+ const next = await press(wanted > from ? "Right" : "Left", (p) => p.effort !== current);
4239
+ if (!next)
4240
+ break;
4241
+ now = next;
4242
+ }
4243
+ if (now.effort !== effort)
4244
+ return refuse("CLAUDE_EFFORT_NOT_SELECTABLE", `${row} では reasoning_effort=${effort} を選べません`);
4245
+ }
4246
+ // 打つ直前に、もう1度読んで確かめる。
4247
+ const last = picker();
4248
+ const lastRow = cursorOf(last);
4249
+ if (!last || !lastRow || !last.session_only
4250
+ || (targetIndex !== null && lastRow.index !== targetIndex)
4251
+ || (effort && last.effort !== effort)) {
4252
+ return refuse("CLAUDE_MODEL_PICKER_UNREADABLE", "選ぶ直前の画面が、選んだ行・effortと合いません");
4253
+ }
4254
+ sendMenuChoice(name, "s");
4255
+ let confirmed = false;
4256
+ const result = await until(() => {
4257
+ const text = screen();
4258
+ const confirm = claudeModelSwitchConfirm(text);
4259
+ if (confirm === "yes_selected" && !confirmed) {
4260
+ // 会話が今のmodelでcacheされている時の確かめ。1番(Yes, switch)が選ばれている時だけ進める。
4261
+ confirmed = true;
4262
+ sendMenuChoice(name, "Enter");
4263
+ return null;
4264
+ }
4265
+ if (confirm || claudeModelPicker(text))
4266
+ return null;
4267
+ return claudeModelSwitchResult(text);
4268
+ }, CLAUDE_MODEL_SWITCH_TIMEOUT_MS);
4269
+ if (!result)
4270
+ return refuse("CLAUDE_MODEL_SWITCH_UNCONFIRMED", "Claude Codeの切り替えの知らせを確認できません");
4271
+ if (result.scope === "saved_as_default") {
4272
+ throw new AitermError(`CLAUDE_MODEL_SAVED_AS_DEFAULT vendor=claude session=${name}\n` +
4273
+ `Claude Codeが、選んだ値を端末の既定として保存したと返しました(${result.line})。~/.claude/settings.json を確かめてください。`, 2);
4274
+ }
4275
+ if (result.scope !== "session_only") {
4276
+ throw new AitermError(`CLAUDE_MODEL_SWITCH_REFUSED vendor=claude session=${name}\n` +
4277
+ `Claude Codeがmodelを切り替えませんでした(${result.line})。modelとreasoning_effortは変えていません。`, 2);
4278
+ }
4279
+ const ready = await waitAgentTuiReady(name, meta, AGENT_TUI_READY_TIMEOUT_MS);
4280
+ if (!ready.ready)
4281
+ throw new AitermError("Claudeのmodel変更完了を確認できません", 2);
4282
+ }
4122
4283
  export async function configureAgent(name, opts) {
4123
4284
  assertSessionName(name);
4124
4285
  const model = opts.model?.trim() || null;
@@ -4138,13 +4299,14 @@ export async function configureAgent(name, opts) {
4138
4299
  throw new AitermError(`agent session '${name}' は入力待ちではありません`, 2);
4139
4300
  }
4140
4301
  if (meta.kind === "claude") {
4141
- if (model) {
4142
- await sendAgentPromptText(name, `/model ${model}`);
4143
- const modelReady = await waitAgentTuiReady(name, meta, AGENT_TUI_READY_TIMEOUT_MS);
4144
- if (!modelReady.ready)
4145
- throw new AitermError(`Claudeのmodel変更完了を確認できません`, 2);
4146
- }
4147
- if (effort) {
4302
+ // ultracodeはClaude Codeが自分で「this session only」と返すsession設定で、選ぶ画面には出ない。
4303
+ const level = effort && CLAUDE_EFFORTS.has(effort) ? effort : null;
4304
+ if (effort && !level && effort !== "ultracode") {
4305
+ throw new AitermError("Claude Code の reasoning_effort は low/medium/high/xhigh/max/ultracode のいずれかです。設定は変えていません。", 2);
4306
+ }
4307
+ if (model || level)
4308
+ await configureClaudeForSessionOnly(name, meta, model, level);
4309
+ if (effort && !level) {
4148
4310
  await sendAgentPromptText(name, `/effort ${effort}`);
4149
4311
  const effortReady = await waitAgentTuiReady(name, meta, AGENT_TUI_READY_TIMEOUT_MS);
4150
4312
  if (!effortReady.ready)
@@ -373,6 +373,97 @@ export function claudeUsageLimit(screen) {
373
373
  }
374
374
  return null;
375
375
  }
376
+ function lastLineIndex(lines, re) {
377
+ for (let i = lines.length - 1; i >= 0; i--)
378
+ if (re.test(lines[i]))
379
+ return i;
380
+ return -1;
381
+ }
382
+ export function claudeModelPicker(screen) {
383
+ const lines = screen.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "").split(/\r?\n/u);
384
+ const head = lastLineIndex(lines, /^\s*Select model\s*$/u);
385
+ if (head < 0)
386
+ return null;
387
+ const body = lines.slice(head + 1);
388
+ const foot = body.findIndex((line) => /\bEsc to cancel\b/u.test(line));
389
+ if (foot < 0)
390
+ return null;
391
+ const rows = [];
392
+ let effort = null;
393
+ for (const line of body.slice(0, foot)) {
394
+ const row = /^\s*([❯↓↑]\s+)?(\d+)\.\s+(.*?)\s*$/u.exec(line);
395
+ if (row) {
396
+ const name = row[3].split(/\s{2,}/u)[0];
397
+ rows.push({
398
+ index: Number(row[2]),
399
+ label: name.replace(/\s*✔$/u, ""),
400
+ cursor: row[1]?.includes("❯") === true,
401
+ current: /✔$/u.test(name),
402
+ });
403
+ continue;
404
+ }
405
+ const level = /(?:^|\s)(low|medium|high|xhigh|max) effort\b/iu.exec(line);
406
+ if (level)
407
+ effort = level[1].toLowerCase();
408
+ }
409
+ if (rows.length === 0)
410
+ return null;
411
+ return { rows, effort, session_only: /\bs to use this session only\b/u.test(body[foot]) };
412
+ }
413
+ /**
414
+ * 指定されたmodel(catalogのID・別名・表示名)が、選ぶ画面のどの行かを決める。
415
+ * `claude-opus-5-5` は「Opus 5.5」、別名の `opus` はその系統で番号が一番若い行(catalogの並びで最新)。
416
+ */
417
+ export function claudeModelPickerRow(rows, model) {
418
+ const want = model.trim().toLowerCase();
419
+ const sorted = [...rows].sort((a, b) => a.index - b.index);
420
+ const id = /^claude-([a-z]+)-(\d+)(?:-(\d{1,2}))?(?:-\d{8})?$/u.exec(want);
421
+ const name = id ? `${id[1]} ${id[2]}${id[3] ? `.${id[3]}` : ""}` : want;
422
+ return sorted.find((row) => row.label.toLowerCase() === name)
423
+ ?? (want === "default" ? sorted.find((row) => /^default\b/iu.test(row.label)) : undefined)
424
+ ?? (/^[a-z]+$/u.test(want) ? sorted.find((row) => row.label.toLowerCase().startsWith(`${want} `) && /^\S+ \d/u.test(row.label)) : undefined)
425
+ ?? null;
426
+ }
427
+ // 会話が今のmodelでcacheされている時だけ、sの後に出る確かめ(2.1.293・2.1.296)。
428
+ // Switch model?
429
+ // Your next response will be slower and use more tokens
430
+ // ❯ 1. Yes, switch to Opus 5.5
431
+ // 2. No, go back
432
+ export function claudeModelSwitchConfirm(screen) {
433
+ const lines = screen.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "").split(/\r?\n/u);
434
+ const head = lastLineIndex(lines, /^\s*Switch model\?\s*$/u);
435
+ if (head < 0)
436
+ return null;
437
+ const body = lines.slice(head + 1);
438
+ if (!body.some((line) => /^\s*(?:❯\s*)?1\.\s*Yes, switch to\b/u.test(line)))
439
+ return null;
440
+ return body.some((line) => /^\s*❯\s*1\.\s*Yes, switch to\b/u.test(line)) ? "yes_selected" : "other_selected";
441
+ }
442
+ export function claudeModelSwitchResult(screen) {
443
+ const lines = screen.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "").split(/\r?\n/u);
444
+ const command = lastLineIndex(lines, /^\s*❯\s*\/model\s*$/u);
445
+ if (command < 0)
446
+ return null;
447
+ const start = lines.findIndex((line, i) => i > command && /^\s*⎿\s+\S/u.test(line));
448
+ if (start < 0)
449
+ return null;
450
+ const parts = [lines[start].replace(/^\s*⎿\s+/u, "").trim()];
451
+ for (const line of lines.slice(start + 1)) {
452
+ if (!/^\s{3,}\S/u.test(line) || /^\s*⎿/u.test(line))
453
+ break;
454
+ parts.push(line.trim());
455
+ }
456
+ const line = parts.join(" ");
457
+ if (/\bblocked by a PreModelSwitch hook\b/u.test(line))
458
+ return { scope: "blocked", line };
459
+ if (/^Kept model as\b/u.test(line))
460
+ return { scope: "kept", line };
461
+ if (!/^(?:Set model to|Model set to)\b/u.test(line))
462
+ return null;
463
+ if (/\bsaved as your default\b/u.test(line))
464
+ return { scope: "saved_as_default", line };
465
+ return /\bfor this session only\b/u.test(line) ? { scope: "session_only", line } : null;
466
+ }
376
467
  // Claude Code 2.1.282 は選択肢へ「1.」などの番号を付ける。番号の有無のどちらも受ける。
377
468
  export function claudeLoginMethodMenu(screen) {
378
469
  const tail = screen.split("\n").slice(-32).join("\n");
package/dist/index.js CHANGED
@@ -618,6 +618,9 @@ registerRemoteAwareTool("pty_list", {
618
618
  session_id: z.string(), current_command: z.string(), attached: z.boolean(),
619
619
  width: z.number(), height: z.number(),
620
620
  harness: z.enum(["claude-code", "codex-cli", "grok-cli", "cursor-cli"]).nullable(),
621
+ // 0.58.3より前の現地(remote)は返さないので、無い事も受ける。
622
+ vendor_session_id: z.string().nullable().optional().describe("agent sessionのharness側の会話ID。Claude CodeとGrokは起動時に--session-idで渡した値(席の中で会話を移した後は追わない)、" +
623
+ "CodexとCursorは分かった後の値。通常PTYと、まだ分からない席はnull"),
621
624
  environment: z.record(z.string(), z.string().nullable()),
622
625
  })),
623
626
  },
@@ -833,7 +836,9 @@ registerRemoteAwareTool("agent_auth", {
833
836
  });
834
837
  registerRemoteAwareTool("agent_configure", {
835
838
  description: "起動済みのClaude/Codex/Grok/Cursor agent sessionを再起動せず、会話contextを保ったままmodel/reasoning effortを変更する。" +
836
- "各harnessのCLI標準model操作を使う。Cursorのreasoning_effort変更はmodelと同時指定する。",
839
+ "各harnessのCLI標準model操作を使う。Cursorのreasoning_effort変更はmodelと同時指定する。" +
840
+ "Claude Codeの席では /model の選ぶ画面の「この会話だけ」(s)で替え、端末の ~/.claude/settings.json の既定は書き換えない。" +
841
+ "その選び方が無い版、選ぶ画面に無いmodel、そのmodelで選べないeffortは、何も変えずに断る。",
837
842
  inputSchema: {
838
843
  session_id: z.string().regex(/^[A-Za-z0-9_-]{1,64}$/),
839
844
  model: z.string().min(1).nullish().describe("変更後のmodel。省略時はmodelを変更しない"),
@@ -64,6 +64,22 @@ async function submitParentAnswer(parent, deliveryId, text) {
64
64
  const OWNER_IDENTITY_RECHECK_MS = 60_000;
65
65
  // owner.jsonの無い保存場所を、作っている途中の持ち主として扱う時間。
66
66
  const OWNER_SETUP_GRACE_MS = 60_000;
67
+ // 持ち主が自分の保存場所へ付ける「生きている印」(ADR 0112)。中身は無く、最後に書いた時刻(mtime)だけを使う。
68
+ // 印が新しい持ち主は、OSへ聞かずに生きていると読む。pidを使い回した別のprocessは、この印を書かない。
69
+ // 印の無い持ち主(0.58.3までの版)と、印が古い持ち主だけ、今までどおり開始時刻で照合する。
70
+ // 名前は.jsonで終わらせない(回収は、保存場所の.jsonを配送の記録として引き取る)。
71
+ const OWNER_HEARTBEAT_FILE = "alive";
72
+ const OWNER_HEARTBEAT_MS = 15_000;
73
+ // 持ち主のprocessが同期の処理で塞がる間(最長で十数秒)も印を新しいと読めるよう、書く間隔の4倍にする。
74
+ const OWNER_HEARTBEAT_FRESH_MS = 60_000;
75
+ function ownerHeartbeatAt(directory) {
76
+ try {
77
+ return fs.statSync(path.join(directory, OWNER_HEARTBEAT_FILE)).mtimeMs;
78
+ }
79
+ catch {
80
+ return null;
81
+ }
82
+ }
67
83
  function processExists(pid) {
68
84
  // 0以下はprocess groupを指す。持ち主のpidにはならない。
69
85
  if (pid <= 0)
@@ -129,10 +145,11 @@ export class ParentDeliveryManager {
129
145
  verifiedOwners = new Map();
130
146
  serviceError = null;
131
147
  closing = false;
148
+ lastHeartbeat = Number.NEGATIVE_INFINITY;
132
149
  constructor(options = {}) {
133
150
  this.deps = { observe: observeAnywhere, answer: answerAnywhere, submit: submitParentAnswer,
134
151
  verify: verifyParent, processes: readProcessIdentities, exists: processExists,
135
- now: () => performance.now(), ...options.dependencies };
152
+ now: () => performance.now(), wall: () => Date.now(), ...options.dependencies };
136
153
  const stateRoot = ensureStateRoot();
137
154
  // 別端末の子の記録はremote-で始まる保存場所へ分け、boundary.remoteを知らない旧版のreaderへ渡さない。
138
155
  const prefix = options.remote ? "remote-" : "";
@@ -414,14 +431,40 @@ export class ParentDeliveryManager {
414
431
  this.recovering = this.recoverOrphans().finally(() => { this.recovering = null; });
415
432
  return this.recovering;
416
433
  }
434
+ /** 自分の保存場所の「生きている印」を新しくする。書けない回は次の回へ回す(印が古くなれば、相手は開始時刻で照合する)。 */
435
+ heartbeat() {
436
+ const wall = this.deps.wall();
437
+ if (this.closing || Math.abs(wall - this.lastHeartbeat) < OWNER_HEARTBEAT_MS)
438
+ return;
439
+ const file = path.join(this.ownerDir, OWNER_HEARTBEAT_FILE);
440
+ try {
441
+ const at = new Date(wall);
442
+ try {
443
+ fs.utimesSync(file, at, at);
444
+ }
445
+ catch (error) {
446
+ if (error.code !== "ENOENT")
447
+ throw error;
448
+ fs.writeFileSync(file, "", { mode: 0o600 });
449
+ fs.utimesSync(file, at, at);
450
+ }
451
+ this.lastHeartbeat = wall;
452
+ }
453
+ catch { /* 次の回 */ }
454
+ }
417
455
  async recoverOrphans() {
456
+ this.heartbeat();
418
457
  // 生存を確かめる相手は、closeしていない他の持ち主だけ。居なければOSへ問い合わせない。
419
- // この処理は5秒おきに全MCP processで回る。全processの一覧を取ると、processの多い端末で負荷になる。
458
+ // この処理は5秒おきに全MCP processで回る。processを起こして確かめると、MCP processの数の掛け算で端末を重くする
459
+ // (Windowsで、23個のMCP processがPowerShellを毎秒4回起こしていた。2026-10-12)。
420
460
  const directories = fs.readdirSync(this.active, { withFileTypes: true })
421
461
  .filter((directory) => directory.isDirectory() && path.join(this.active, directory.name) !== this.ownerDir);
422
462
  const now = this.deps.now();
463
+ const wall = this.deps.wall();
423
464
  const fate = new Map();
424
465
  const unverified = [];
466
+ // 前の照合を覚えている持ち主。誰かを照合する回に、まとめて照合し直す。
467
+ const remembered = [];
425
468
  for (const directory of directories) {
426
469
  let owner;
427
470
  try {
@@ -437,14 +480,23 @@ export class ParentDeliveryManager {
437
480
  fate.set(directory.name, "gone");
438
481
  continue;
439
482
  }
483
+ const beat = ownerHeartbeatAt(path.join(this.active, directory.name));
484
+ if (beat !== null && Math.abs(wall - beat) < OWNER_HEARTBEAT_FRESH_MS) {
485
+ fate.set(directory.name, "alive");
486
+ continue;
487
+ }
440
488
  const verified = this.verifiedOwners.get(directory.name);
441
489
  if (verified && verified.pid === pid && verified.started_identity === owner.started_identity
442
- && now - verified.at < OWNER_IDENTITY_RECHECK_MS)
490
+ && now - verified.at < OWNER_IDENTITY_RECHECK_MS) {
443
491
  fate.set(directory.name, verified.alive ? "alive" : "unverified");
492
+ remembered.push({ name: directory.name, pid, started_identity: owner.started_identity });
493
+ }
444
494
  else
445
495
  unverified.push({ name: directory.name, pid, started_identity: owner.started_identity });
446
496
  }
447
497
  if (unverified.length > 0) {
498
+ // 覚えた時刻が持ち主ごとにずれると、1分の間に何度もprocessを起こす。照合する回は、全員を1回で照合し直して時刻を揃える。
499
+ unverified.push(...remembered);
448
500
  const processes = this.deps.processes([...new Set(unverified.map((entry) => entry.pid))]);
449
501
  for (const entry of unverified) {
450
502
  const row = processes.find((candidate) => candidate.pid === entry.pid);
@@ -101,17 +101,31 @@ export function readRuntimeProcesses() {
101
101
  };
102
102
  });
103
103
  }
104
+ /** そのpidのprocessが今あるか。OSへ直接聞き、processを起こさない。権限が無くて触れないprocessも「ある」と読む。 */
105
+ function pidExists(pid) {
106
+ if (pid <= 0)
107
+ return false;
108
+ try {
109
+ process.kill(pid, 0);
110
+ return true;
111
+ }
112
+ catch (error) {
113
+ return error.code === "EPERM";
114
+ }
115
+ }
104
116
  /**
105
117
  * 指定したpidの開始時刻だけを引く。readRuntimeProcessesと同じ開始時刻を返し、存在しないpidは結果に含めない。
106
118
  * process表にpidはあるが開始時刻を読めない時は、started_identityをnullで返す(pidが無いのとは区別する)。
107
119
  * 全processのargvを読む一覧取得は、processの多い端末で重い(定期実行から呼ばない)。
108
120
  */
109
121
  export function readProcessIdentities(pids) {
110
- const wanted = [...new Set(pids)];
122
+ if (pids.some(pid => !Number.isSafeInteger(pid) || pid < 0))
123
+ throw new AitermError("process照会のpidが不正です", 2);
124
+ // 居ないpidは、OSへ直接聞いて先に除く。全部居なければ、processを起こさない。
125
+ // macOSのpsは、範囲の外のpidが1つ混ざると、居るprocessの分も出さずに誤りで終わる。
126
+ const wanted = [...new Set(pids)].filter(pidExists);
111
127
  if (wanted.length === 0)
112
128
  return [];
113
- if (wanted.some(pid => !Number.isSafeInteger(pid) || pid < 0))
114
- throw new AitermError("process照会のpidが不正です", 2);
115
129
  if (!isWin) {
116
130
  const result = spawnSync(POSIX_PS, [...POSIX_IDENTITY_ARGS, wanted.join(",")], {
117
131
  encoding: "utf8", env: { ...process.env, LC_ALL: "C" }, timeout: 10000,
@@ -129,11 +143,25 @@ export function readProcessIdentities(pids) {
129
143
  }
130
144
  // CommandLineの読めないprocess(別の権限のserviceなど)も返す。pidが使い回された先を「別のprocess」と確かめるのに要る。
131
145
  // 開始時刻を読めない行も、pidがprocess表にある印として返す。
146
+ // WMI(Win32_Process)は使わない。絞っても1回に0.1〜0.3秒かかり、WMIの側のprocess(wmiprvse)のCPUを使う
147
+ // (process 1300個のWindowsで測った 2026-10-12。ADR 0112)。.NETのprocessの読み(OpenProcessとGetProcessTimes)は1個1.5ミリ秒。
148
+ // 開始時刻は同じ値になる(WMIはマイクロ秒で切り捨てた値を返し、どちらも1ミリ秒で切り捨てて使う。888個のprocessで食い違いは0)。
149
+ // 権限が無くて読めないprocessだけ、WMIへ聞き直す。
132
150
  const script = [
133
151
  ...WINDOWS_PROBE_HEADER,
134
- `$rows=@(Get-CimInstance Win32_Process -Filter "${wanted.map(pid => `ProcessId=${pid}`).join(" OR ")}" | ForEach-Object {`,
135
- "[ordered]@{ pid=[int]$_.ProcessId; started_identity=$(if ($null -ne $_.CreationDate) { $_.CreationDate.ToUniversalTime().ToString('o') } else { $null }) }",
136
- "})",
152
+ "$rows=@()",
153
+ `foreach ($id in @(${wanted.join(",")})) {`,
154
+ " try { $p=[Diagnostics.Process]::GetProcessById($id) } catch [ArgumentException] { continue }",
155
+ " $started=$null",
156
+ " try { $started=$p.StartTime.ToUniversalTime().ToString('o') }",
157
+ " catch [InvalidOperationException] { continue }",
158
+ " catch {",
159
+ " $w=Get-CimInstance Win32_Process -Filter \"ProcessId=$id\" -Property CreationDate",
160
+ " if ($null -eq $w) { continue }",
161
+ " if ($null -ne $w.CreationDate) { $started=$w.CreationDate.ToUniversalTime().ToString('o') }",
162
+ " }",
163
+ " $rows += [ordered]@{ pid=[int]$id; started_identity=$started }",
164
+ "}",
137
165
  "ConvertTo-Json -Compress -InputObject $rows",
138
166
  ].join("\n");
139
167
  const result = spawnSync(resolveWindowsPowerShell7(), [...WINDOWS_PROBE_ARGS, Buffer.from(script, "utf16le").toString("base64")], {
package/docs/DESIGN.md CHANGED
@@ -37,7 +37,8 @@ UTF-8のBase64を復元したscriptblockのdot-sourceを使う。変数と作業
37
37
  sentinelの方言は端末runtimeが実効shellから決める。SSH先の現在の標準PowerShell promptを
38
38
  検出した場合は、Aiterm hostのOSに関係なくPowerShell構文を使う。過去のpromptでは切り替えない。
39
39
 
40
- `pty_list`はtextと構造化したsession一覧を返す。`env_keys`は帰属等の非秘密キーの明示照会だけで、
40
+ `pty_list`はtextと構造化したsession一覧を返す。agent sessionには、harness側の会話ID(`vendor_session_id`。Claude CodeとGrokは
41
+ 起動時に`--session-id`で渡した値、CodexとCursorは掴んだ後の値)を載せる。`env_keys`は帰属等の非秘密キーの明示照会だけで、
41
42
  psmuxが出力した余分な環境値を返さない。通常PTYとagentへ`AITERM_SESSION_ID`を注入し、
42
43
  `env_vars`の継承とsessionへの登録もAitermが所有する。古いtmuxは子へのenv注入とsession登録を使う。
43
44
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiterm-mcp",
3
- "version": "0.58.2",
3
+ "version": "0.58.4",
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": [
@@ -72,7 +72,7 @@
72
72
  },
73
73
  "dependencies": {
74
74
  "@modelcontextprotocol/sdk": "^1.29.0",
75
- "aiterm-steer-delivery": "^0.4.3",
75
+ "aiterm-steer-delivery": "^0.4.5",
76
76
  "ws": "8.21.3",
77
77
  "zod": "^4.4.3"
78
78
  },