aiterm-mcp 0.49.0 → 0.51.0

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,47 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.51.0] - 2026-10-04
11
+
12
+ ### 追加
13
+
14
+ - `aiterm-delivery-wake --parent <codex|claude|cursor>`。MCPの本体を使う時だけ起こす中継(`mcp-lazy`)の後ろで、眠っている本体を
15
+ 親配送の引き取りのために起こすべきかを返す(ADR 0082)。終了した持ち主がその種類の親の配送を持ったままならexit 0、
16
+ 無ければexit 1。何も出力せず、子processを起こさない。同じ判定を回す席のうち1席だけが起こす。
17
+ 中継には先行起動の判定として渡す(`MCP_LAZY_WAKE_COMMAND`)。
18
+
19
+ ### 変更
20
+
21
+ - `pty_observe`の`activity.post_startup_process_count`で、中継`mcp-lazy`の直接の子を数えない(ADR 0082)。
22
+ 中継は道具を初めて呼ばれた時に本体を自分の下に起こすので、中継ごしの席は道具を一度使うと数が0へ戻らなかった。
23
+ 見分けるのは、親の起動時のargvの先頭のbasenameが`mcp-lazy`で始まること。その下で動くprocessは今までどおり数える。
24
+
25
+ ### 修正
26
+
27
+ - Cursor親の受け取りprocess(`cursor-parent-receive`)が、親のCursorが終わった後も残り、後から完了した子の回答を引き取っていた
28
+ (aiterm-steer-delivery 0.1.12)。Cursorの背景shellは親から切り離されて動くので、席を閉じても最長24時間残る。
29
+ 引き取られた回答は`submitted`になるが、読む相手は居ない。受け取りprocessは、出力の読み手が居なくなったら引き取らずに終わる。
30
+ その配送は`sending`のまま残り、24時間で`CURSOR_PARENT_DELIVERY_UNCLAIMED`になる。本文は今までどおり記録に残る。
31
+
32
+ ## [0.50.0] - 2026-10-04
33
+
34
+ ### 変更
35
+
36
+ - 新しい端末の環境を、その端末を開いたMCP processの環境にした(ADR 0081。POSIXのtmux。Windowsのpsmuxは元からこの動き)。
37
+ 今までは、tmuxのserverを最初に起こした呼び出し元の環境が、後から開く全部の端末へ入っていた。別の呼び出し元の
38
+ `PATH`や`npm_config_prefix`、その呼び出し元だけが持つ変数が、名指ししていなくても端末に入る。
39
+ - harnessがMCP processへ渡す環境が少なければ、端末の環境もその分だけになる。Codexは既定で`HOME`・`PATH`・`SHELL`・`TERM`の
40
+ 4つしか渡さない。要る変数は`~/.codex/config.toml`の`[mcp_servers.aiterm]`に`env_vars`で名前を並べる。
41
+ - `AITERM_SESSION_ID`・`AITERM_AGENT_*`・`TMUX`・`TMUX_PANE`は継がない(今までどおり、端末ごとに付く)。
42
+ - `env_vars`は今までどおり使える。`pty_list`の`env_keys`で読めるのは、`env_vars`で名指しした値と`AITERM_SESSION_ID`だけ。
43
+ - 同じtmuxのserverを、前の版のMCP processと並んで使える。前の版が開く端末は今までどおり動く。
44
+
45
+ ### 修正
46
+
47
+ - 利用者の`~/.tmux.conf`を読んでいた。空のconfigを指す`-f`を`new-session`の引数として渡していて、tmux本体には届いていなかった。
48
+ READMEの「`-f /dev/null`で起動し、`~/.tmux.conf`を読まない」のとおりにした。
49
+ - tmux 3.2未満で端末を1つも開けなかった(`new-session -f`を知らず「unknown option」で落ちる)。3.1cで開けることを確かめた。
50
+
10
51
  ## [0.49.0] - 2026-10-03
11
52
 
12
53
  ### 追加
@@ -2009,7 +2050,9 @@ prototype (preserved under `prototype/python/` as the porting source and referen
2009
2050
  `ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
2010
2051
  provenance.
2011
2052
 
2012
- [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.49.0...HEAD
2053
+ [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.51.0...HEAD
2054
+ [0.51.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.50.0...v0.51.0
2055
+ [0.50.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.49.0...v0.50.0
2013
2056
  [0.49.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.48.0...v0.49.0
2014
2057
  [0.48.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.47.1...v0.48.0
2015
2058
  [0.47.1]: https://github.com/kitepon/aiterm-mcp/compare/v0.47.0...v0.47.1
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.49.0** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
212
+ **状態:** 開発継続中 · 現行公開版 **v0.51.0** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
213
213
 
214
214
  ### 更新と巻き戻し
215
215
 
@@ -299,10 +299,15 @@ $ aiterm-wait --session codex1 --cursor <event_cursor> # exit 0=done / 3=timeo
299
299
 
300
300
  Cursorの`model`は`gpt-5.6-luna`のようなbase model、`reasoning_effort`は`high`のように別指定する。adapterは現行`model-effort` IDを`cursor-agent models`へ照合し、起動中変更はCursor標準model pickerのparameter editorを使う。不在時は別modelへfallbackしない。
301
301
 
302
+ 新しい端末の環境は、その端末を開いたMCP processの環境で決まる(0.50.0から。ADR 0081)。tmuxのserverを
303
+ 先に起こした別の呼び出し元の値は入らない。Aitermが端末ごとに付け直す`AITERM_SESSION_ID`・`AITERM_AGENT_*`は
304
+ 継がない。harnessがMCP processへ渡す環境が少なければ、端末の環境もその分だけになる(Codexは既定で
305
+ `HOME`・`PATH`・`SHELL`・`TERM`。要る変数は`[mcp_servers.aiterm]`の`env_vars`で渡す)。
306
+
302
307
  `env_vars`は環境変数の**名前**だけを並べるallowlistであり、name/value mapではない。aitermは
303
308
  launcher起動時に現在のMCP processから各名前を読み、存在する値をshell quoteして、その1回のvendor
304
309
  起動コマンドへ入れる。未設定名は省略し、shell変数名として不正な名前はsession作成前に失敗する。
305
- 全環境の暗黙copy、backend server再起動、retry、fallbackは行わない。値はMCP tool引数には入らないが、
310
+ 名指しした値だけが`pty_list`の`env_keys`で読める。値はMCP tool引数には入らないが、
306
311
  PTYの起動コマンドとして送られ、sessionの`.lastcmd`にも保持されるため、起動先vendorと同じOS userへ
307
312
  到達する。秘密転送路ではなく、席identityやworkflow用の非secret変数だけに使う。
308
313
 
@@ -492,8 +497,9 @@ aiterm は同じ核心の洞察——端末を出会いの場にする——を
492
497
  ### セッション観測と起動準備
493
498
 
494
499
  `pty_open`の既定shellはPOSIXでbash、WindowsでPowerShell 7。通常PTYとagentの内側では
495
- `AITERM_SESSION_ID`で自分のsessionを識別できる。`env_vars`はMCP processから継承する名前の配列で、
496
- `pty_list({ env_keys: ["JOB_OWNER"] })`は指定した非秘密キーだけを`environment`へ返す。未設定値はnull。
500
+ `AITERM_SESSION_ID`で自分のsessionを識別できる。端末は開いたMCP processの環境を継ぐ。`env_vars`は、そのうち
501
+ sessionへ登録する名前の配列で、`pty_list({ env_keys: ["JOB_OWNER"] })`は登録した非秘密キーだけを`environment`へ返す。
502
+ 登録していない値と未設定値はnull。
497
503
  一覧の`aiterm.pty-list-result.v1`は`observed_at`と`sessions`を持ち、各行に`session_id`、`current_command`、
498
504
  `attached`、`width`、`height`、`harness`、`environment`を返す。従来のtextも維持する。
499
505
 
@@ -511,11 +517,20 @@ aiterm は同じ核心の洞察——端末を出会いの場にする——を
511
517
  `token_hint`は画面の直近token表示値またはnull。画面本文や生argvを解析する必要はない。
512
518
 
513
519
  agent sessionを閉じると失うものは、次の2項目で分かる。`activity.post_startup_process_count`は、`agent_launch`の起動準備が
514
- 完了した時点(初手を送る前)に居なかったprocessの数。起動直後の1分に始めた裏の作業も数える。Codex自身の補助process
515
- (`codex-code-mode-host`)は数えない。`pending_child_deliveries`は、そのsessionが親として待っていて、まだ届け終えていない
520
+ 完了した時点(初手を送る前)に居なかったprocessの数。起動直後の1分に始めた裏の作業も数える。数えないものは2つ。Codex自身の
521
+ 補助process(`codex-code-mode-host`)と、中継`mcp-lazy`の直接の子(初めて使う時に起こすMCPの本体と、先行起動の判定)。
522
+ その下で動くprocessは数える。`pending_child_deliveries`は、そのsessionが親として待っていて、まだ届け終えていない
516
523
  子の結果の数。呼び出した側が誰でも付く。どちらも、分からない時はnull(通常PTY、harnessのprocessを特定できない時。
517
524
  processの数は0.48.0以前が起動したagentも)。別端末が旧版の時は項目ごと無い。nullと項目なしは「分からない」で、0ではない。
518
525
 
526
+ MCPの本体を初めて使う時まで起こさない中継の後ろにAitermを登録すると、眠っている本体は、終了した持ち主の配送を引き取れない。
527
+ `aiterm-delivery-wake --parent <codex|claude|cursor>`は、起こす必要があるかを返す。その種類の親の配送を、終了した持ち主が
528
+ 持ったままならexit 0、そうでなければexit 1、引数の誤りはexit 2。何も出力せず、子processを起こさない。同じ判定を回す席のうち
529
+ 1席だけが起こすよう、配送に印を付ける(引き取られないまま30秒過ぎたら、次の判定が印を取り直す)。`mcp-lazy`には、本体と同じ環境で
530
+ 先行起動の判定として渡す。
531
+ `MCP_LAZY_WAKE_COMMAND='["/absolute/path/to/node","/absolute/path/to/aiterm-mcp/dist/delivery-wake-cli.js","--parent","claude"]'`
532
+ 持ち主の開始時刻はLinuxでは`/proc`で照合する。それ以外のOSでは、pidがあれば生きている持ち主として扱う。
533
+
519
534
  `agent_launch({ harness, cwd, trust_project: true })`はpromptなしでも既知のworkspace・project hooks・MCP初期同意を
520
535
  進め、入力受付とharness生存を確認して`startup.status="ready"`を返す。指定なしのpromptなし起動は`not_checked`。
521
536
  Claude Code初回起動の文字表示テーマ選択では、画面で選択済みの項目を確定して起動を続ける。
package/README.md CHANGED
@@ -223,7 +223,7 @@ collection is off by default and performs no network I/O. It ships via
223
223
  tag-triggered CI with npm provenance (OIDC Trusted Publishing); the GitHub
224
224
  Release re-registers the Official MCP Registry entry.
225
225
 
226
- **Status:** actively maintained · current public release **v0.49.0** · runs on Linux · WSL2 · macOS · native Windows (tmux on POSIX, the tmux-CLI-compatible [psmux](https://github.com/psmux/psmux) on native Windows — no WSL required) · MIT · see the [CHANGELOG](CHANGELOG.md).
226
+ **Status:** actively maintained · current public release **v0.51.0** · runs on Linux · WSL2 · macOS · native Windows (tmux on POSIX, the tmux-CLI-compatible [psmux](https://github.com/psmux/psmux) on native Windows — no WSL required) · MIT · see the [CHANGELOG](CHANGELOG.md).
227
227
 
228
228
  ### Update and rollback
229
229
 
@@ -321,11 +321,17 @@ The canonical harness choices are:
321
321
  | `grok-cli` | Grok Build CLI | Grok model selected with `model`; live catalog check |
322
322
  | `cursor-cli` | Cursor Agent CLI | GPT, Claude, Grok, or another Cursor catalog model; normal transcript completion |
323
323
 
324
+ A new terminal gets the environment of the MCP process that opened it (since 0.50.0, ADR 0081).
325
+ Values from another caller that happened to start the tmux server first no longer appear in it.
326
+ `AITERM_SESSION_ID` and `AITERM_AGENT_*` are not inherited; aiterm sets them per terminal. If a harness
327
+ passes only a few variables to its MCP process, the terminal has only those (Codex passes `HOME`, `PATH`,
328
+ `SHELL`, and `TERM` by default; forward more with `env_vars` under `[mcp_servers.aiterm]`).
329
+
324
330
  `env_vars` is an allowlist of environment-variable **names**, not a name/value map. At launch,
325
331
  aiterm reads each valid name from its current MCP process, shell-quotes present values, and places
326
332
  them on that one harness launch command. Missing names are omitted; invalid shell variable names
327
- fail before session creation. There is no implicit whole-environment copy, backend-server restart,
328
- retry, or fallback. Values do not enter the MCP tool arguments, but they are delivered through the
333
+ fail before session creation. Only the named values can be read back with `env_keys` in `pty_list`.
334
+ Values do not enter the MCP tool arguments, but they are delivered through the
329
335
  PTY launch command and retained in aiterm's per-session `.lastcmd`; the launched harness and other
330
336
  processes with access to the same OS user may read them. Use this for non-secret seat identity and
331
337
  workflow variables, not as a secret transport.
@@ -523,8 +529,8 @@ On top of that sits a productized layer a raw tmux bridge doesn't have: **token-
523
529
  ### Session observation and startup
524
530
 
525
531
  `pty_open` defaults to bash on POSIX and PowerShell 7 on Windows. Ordinary terminals and agents receive
526
- `AITERM_SESSION_ID`. Pass environment-variable names in `env_vars` to inherit ownership information from the MCP process.
527
- `pty_list({ env_keys: ["JOB_OWNER"] })` returns only the requested non-secret values in `environment`; missing values are null.
532
+ `AITERM_SESSION_ID`. A terminal inherits the environment of the MCP process that opened it. Pass names in `env_vars` to register them on the session.
533
+ `pty_list({ env_keys: ["JOB_OWNER"] })` returns only registered non-secret values in `environment`; unregistered and missing values are null.
528
534
  Its `aiterm.pty-list-result.v1` receipt contains `observed_at` and `sessions`, whose entries include `session_id`,
529
535
  `current_command`, `attached`, `width`, `height`, `harness`, and `environment`. Existing text remains available.
530
536
 
@@ -544,11 +550,22 @@ displayed token count or null. Callers do not need raw argv or pane-text parsing
544
550
 
545
551
  Two fields tell a caller what closing an agent session would lose. `activity.post_startup_process_count` is the number of
546
552
  processes in the session that did not exist when `agent_launch` finished startup (before the first prompt), so background work
547
- started in the first minute is counted too; Codex's own `codex-code-mode-host` helper is not counted. `pending_child_deliveries`
553
+ started in the first minute is counted too. Two kinds of process are not counted: Codex's own `codex-code-mode-host` helper, and
554
+ the direct children of an `mcp-lazy` relay (the MCP server it starts on first use and its wake predicate). What runs below them
555
+ is counted. `pending_child_deliveries`
548
556
  is the number of sub-agent results this session is still waiting for as a parent, whoever the caller is. Both are null when
549
557
  Aiterm cannot tell (ordinary terminals, agents launched by 0.48.0 or earlier for the process count, or an unresolved harness
550
558
  process), and may be absent when a remote host runs an older Aiterm. Treat null or absent as unknown, not as zero.
551
559
 
560
+ When Aiterm is registered behind a relay that starts the MCP server only on first use, a sleeping server cannot take over the
561
+ deliveries of an owner that has exited. `aiterm-delivery-wake --parent <codex|claude|cursor>` answers whether one needs to start:
562
+ exit 0 when an exited owner still holds a delivery for that parent kind, exit 1 otherwise, exit 2 for bad arguments. It prints
563
+ nothing, starts no child process, and marks the delivery so that only one of the seats running the same check starts its server
564
+ (the mark is retaken after 30 seconds if nobody took the delivery over). For `mcp-lazy`, pass it as the wake predicate with the
565
+ same environment as the server:
566
+ `MCP_LAZY_WAKE_COMMAND='["/absolute/path/to/node","/absolute/path/to/aiterm-mcp/dist/delivery-wake-cli.js","--parent","claude"]'`.
567
+ Owner start times are checked through `/proc` on Linux; elsewhere an existing PID is treated as a live owner.
568
+
552
569
  `agent_launch({ harness, cwd, trust_project: true })` completes known workspace, project-hook, and project-MCP startup
553
570
  consent even without a prompt, then verifies input readiness and harness liveness before returning `startup.status="ready"`.
554
571
  For Claude Code's first-run text-style menu, it confirms the item already selected on screen before continuing startup.
@@ -784,10 +801,9 @@ symlink, filter, or replace harness configuration, authentication, MCP, plugin,
784
801
  trust, memory, or history stores. Cleanup removes only aiterm-owned launch metadata and completion
785
802
  correlation files.
786
803
 
787
- The ordinary environment still comes from the persistent shell session. When a caller needs a value that
788
- belongs to the current MCP process rather than the older persistent multiplexer server, every harness
789
- accepts `env_vars: ["NAME", ...]`. Only those names are refreshed at launch; this is a narrow
790
- per-launch overlay, not a replacement environment or configuration snapshot.
804
+ The ordinary environment comes from the MCP process that opened the terminal, not from whichever caller
805
+ started the persistent multiplexer server (since 0.50.0). Every harness still accepts
806
+ `env_vars: ["NAME", ...]`; those names are placed on the launch command and registered on the session.
791
807
 
792
808
  ## License
793
809
 
package/dist/core.js CHANGED
@@ -16,9 +16,9 @@ import * as rtk from "./rtk.js";
16
16
  import { isCursorMcpClient } from "aiterm-steer-delivery";
17
17
  import { paneTokenHint } from "./harnesses/pane-tokens.js";
18
18
  import { unfinishedDeliveriesOwnedBy } from "./parent-delivery-owners.js";
19
- import { readRuntimeProcesses, processSubtree, parentProcess, processIdentity, backgroundProcesses } from "./process-runtime.js";
19
+ import { readRuntimeProcesses, processSubtree, parentProcess, processIdentity, backgroundProcesses, lazyRelayProcess } from "./process-runtime.js";
20
20
  import { AitermError, telemetryOwnedFailure, ownTelemetryFailure } from "./errors.js";
21
- import { isWin, SOCKDIR, tmuxCommand, sendPsmuxPayload, loadPtyBufferChunk, pasteBufferBaseArgs, TMUX_EMPTY_CONFIG, attachCommand, normalizePaneCommand, atomicShellMultiline, appendMarkSentinel, markShellCommand, settlePaneLog, paneCwdArgument, sessionEnvironmentLaunch, } from "./tmux-runtime.js";
21
+ import { isWin, SOCKDIR, tmuxCommand, sendPsmuxPayload, loadPtyBufferChunk, pasteBufferBaseArgs, tmuxNewSession, registeredSessionEnvironment, attachCommand, normalizePaneCommand, atomicShellMultiline, appendMarkSentinel, markShellCommand, settlePaneLog, paneCwdArgument, sessionEnvironmentLaunch, } from "./tmux-runtime.js";
22
22
  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";
23
23
  import { catalogUnavailable } from "./model-catalog.js";
24
24
  import { grokAuthPlan, grokAuthStatus, grokAuthPane, 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";
@@ -672,11 +672,10 @@ export function openSession(name, shell = "bash", envVars = []) {
672
672
  throw new AitermError(`session '${nm}' は既に存在します(list で確認)`, 2);
673
673
  }
674
674
  else {
675
- // -f は端末個人の設定ファイルを読まないための空 config。
676
675
  const environment = [...banner, ...envVars.filter(key => key !== "AITERM_SESSION_ID" && process.env[key] !== undefined)
677
676
  .map(key => `${key}=${process.env[key]}`), `AITERM_SESSION_ID=${nm}`];
678
677
  const launch = sessionEnvironmentLaunch(shell, environment);
679
- const r = tmux("new-session", "-d", "-s", nm, ...launch.args, "-f", TMUX_EMPTY_CONFIG, launch.shell);
678
+ const r = tmuxNewSession(true, nm, launch, environment.map(entry => entry.slice(0, entry.indexOf("="))));
680
679
  if (r.code === 0) {
681
680
  if (launch.register_after_start)
682
681
  for (const entry of environment) {
@@ -1078,6 +1077,18 @@ export function selectHarnessProcesses(meta, rows, subtree) {
1078
1077
  });
1079
1078
  return roots;
1080
1079
  }
1080
+ // 起動完了の控えに無いprocessの数。harnessと中継が自分のために立てるprocessは、利用者の作業ではないので数えない。
1081
+ // その下で動くprocessは数える。
1082
+ export function postStartupProcessCount(kind, activityRows, rows, startup) {
1083
+ const byPid = new Map(rows.map(row => [row.pid, row]));
1084
+ const relayChild = (row) => {
1085
+ const parent = parentProcess(row, byPid);
1086
+ return parent !== undefined && lazyRelayProcess(parent.command);
1087
+ };
1088
+ return new Set(activityRows
1089
+ .filter(row => !(kind === "codex" && codexHelperProcess(row.command)) && !relayChild(row))
1090
+ .map(row => `${row.pid}:${row.started_identity}`).filter(identity => !startup.has(identity))).size;
1091
+ }
1081
1092
  export function observeSession(name, cursor) {
1082
1093
  assertSessionName(name);
1083
1094
  const previous = cursor === undefined ? null : decodeActivityCursor(cursor, name);
@@ -1179,9 +1190,7 @@ export function observeSession(name, cursor) {
1179
1190
  background_cpu_seconds: Object.values(backgroundCpu).reduce((sum, cpu) => sum + cpu, 0),
1180
1191
  background_cpu_delta_seconds: comparable ? Object.entries(backgroundCpu).reduce((sum, [identity, cpu]) => sum + cpu - (previous.background_processes[identity] ?? 0), 0) : null,
1181
1192
  background_cpu_delta_complete: comparable ? Object.keys(previous.background_processes).every(identity => identity in backgroundCpu) : null,
1182
- post_startup_process_count: startup ? new Set(activityRows
1183
- .filter(row => !(meta?.kind === "codex" && codexHelperProcess(row.command)))
1184
- .map(row => `${row.pid}:${row.started_identity}`).filter(identity => !startup.has(identity))).size : null,
1193
+ post_startup_process_count: startup ? postStartupProcessCount(meta.kind, activityRows, rows, startup) : null,
1185
1194
  };
1186
1195
  return result;
1187
1196
  }
@@ -1199,7 +1208,13 @@ export function listSessionsResult(envKeys = []) {
1199
1208
  const [name, command, attached, width, height] = line.split("\t");
1200
1209
  assertSessionName(name);
1201
1210
  const environment = {};
1211
+ const registered = envKeys.length ? registeredSessionEnvironment(name) : null;
1202
1212
  for (const key of envKeys) {
1213
+ // sessionの表には開いたprocessの環境も入っている。返すのは名指しで登録した値だけ。
1214
+ if (registered && !registered.includes(key)) {
1215
+ environment[key] = null;
1216
+ continue;
1217
+ }
1203
1218
  const value = tmux("show-environment", "-t", name, key);
1204
1219
  if (value.code !== 0 && !/unknown variable|not found|not set/i.test(value.stderr))
1205
1220
  throw new AitermError("session環境変数を照会できません", 2);
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/env node
2
+ // aiterm-delivery-wake — 眠っているAitermのMCP本体を、親配送の引き取りのために起こすべきかを返す。
3
+ // MCPを使う時だけ起こす中継(mcp-lazy)の先行起動の判定に渡す。本体が眠っている間は、終了した持ち主の配送を
4
+ // 引き取る者が居ない(引き取りは起きている本体が5秒おきに行う)。
5
+ // exit 0=起こす / 1=眠ったままでよい / 2=引数の誤り。stdoutへは何も出さない。
6
+ // 中継の下で動くので、子processを起こさない(起こすと席のprocess数に入る)。node builtinとstate-rootだけに依存する。
7
+ import * as fs from "node:fs";
8
+ import * as path from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+ import { currentUid, runtimeStateBase } from "./state-root.js";
11
+ const DIRECTORY = {
12
+ codex: "parent-deliveries", claude: "claude-parent-deliveries", cursor: "cursor-parent-deliveries",
13
+ };
14
+ // 起こす役を引き受けた印の寿命。起こした本体は起動してすぐ引き取る。引き取られないまま過ぎたら、次の判定が引き受け直す。
15
+ const CLAIM_TTL_MS = 30_000;
16
+ const USAGE = "usage: aiterm-delivery-wake --parent <codex|claude|cursor>";
17
+ function processExists(pid) {
18
+ if (pid <= 0)
19
+ return false;
20
+ try {
21
+ process.kill(pid, 0);
22
+ return true;
23
+ }
24
+ catch (error) {
25
+ return error.code === "EPERM";
26
+ }
27
+ }
28
+ // psのlstartと同じ値を/procから作る(起動時刻+開始tick。LinuxのUSER_HZは100)。/procの無いOSはnull。
29
+ function linuxStartedAt(pid) {
30
+ try {
31
+ const stat = fs.readFileSync(`/proc/${pid}/stat`, "utf8");
32
+ const ticks = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[19]);
33
+ const boot = Number(/^btime (\d+)$/m.exec(fs.readFileSync("/proc/stat", "utf8"))?.[1]);
34
+ return Number.isFinite(ticks) && Number.isFinite(boot) ? (boot + Math.floor(ticks / 100)) * 1000 : null;
35
+ }
36
+ catch {
37
+ return null;
38
+ }
39
+ }
40
+ const DEFAULTS = { exists: processExists, started_at: linuxStartedAt, now: () => Date.now() };
41
+ function ownerGone(owner, deps) {
42
+ const value = owner;
43
+ if (!value || !Number.isSafeInteger(value.pid) || typeof value.started_identity !== "string" || typeof value.closed !== "boolean")
44
+ return false;
45
+ if (value.closed)
46
+ return true;
47
+ const pid = value.pid;
48
+ if (!deps.exists(pid))
49
+ return true;
50
+ // 同じpidが別のprocessへ再利用されていないかを、開始時刻で見る。照合できない時は生きている扱い(本体が起きた時に照合する)。
51
+ const recorded = Date.parse(value.started_identity);
52
+ const actual = deps.started_at(pid);
53
+ return Number.isFinite(recorded) && actual !== null && Math.abs(actual - recorded) > 2_000;
54
+ }
55
+ /**
56
+ * 終了した持ち主の下に、引き取られていない配送の記録があれば、起こす役を引き受けてtrueを返す。
57
+ * 同じ種類の席は全部が同じ判定を回す。印を先に作れた1席だけが起こす。
58
+ */
59
+ export function shouldWakeForDeliveries(stateRoot, parent, dependencies = {}) {
60
+ const deps = { ...DEFAULTS, ...dependencies };
61
+ let wake = false;
62
+ for (const prefix of ["", "remote-"]) {
63
+ const root = path.join(stateRoot, prefix + DIRECTORY[parent]);
64
+ const active = path.join(root, "active");
65
+ const claims = path.join(root, "wake-claims");
66
+ let owners;
67
+ try {
68
+ owners = fs.readdirSync(active, { withFileTypes: true });
69
+ }
70
+ catch {
71
+ continue;
72
+ }
73
+ const orphaned = new Set();
74
+ for (const directory of owners) {
75
+ if (!directory.isDirectory())
76
+ continue;
77
+ const ownerDir = path.join(active, directory.name);
78
+ let owner;
79
+ let records;
80
+ try {
81
+ owner = JSON.parse(fs.readFileSync(path.join(ownerDir, "owner.json"), "utf8"));
82
+ records = fs.readdirSync(ownerDir).filter(name => name !== "owner.json" && name.endsWith(".json"));
83
+ }
84
+ catch {
85
+ continue;
86
+ }
87
+ if (records.length > 0 && ownerGone(owner, deps))
88
+ orphaned.add(directory.name);
89
+ }
90
+ // 引き取りが済んだ持ち主の印を片付ける。
91
+ try {
92
+ for (const name of fs.readdirSync(claims))
93
+ if (!orphaned.has(name))
94
+ fs.rmSync(path.join(claims, name), { recursive: true, force: true });
95
+ }
96
+ catch { /* 印の置き場がまだ無い */ }
97
+ for (const name of orphaned) {
98
+ const claim = path.join(claims, name);
99
+ try {
100
+ fs.mkdirSync(claims, { recursive: true, mode: 0o700 });
101
+ fs.mkdirSync(claim, { mode: 0o700 });
102
+ wake = true;
103
+ }
104
+ catch (error) {
105
+ if (error.code !== "EEXIST")
106
+ throw error;
107
+ const now = deps.now();
108
+ if (now - fs.statSync(claim).mtimeMs < CLAIM_TTL_MS)
109
+ continue;
110
+ fs.utimesSync(claim, now / 1000, now / 1000);
111
+ wake = true;
112
+ }
113
+ }
114
+ }
115
+ return wake;
116
+ }
117
+ function main(argv) {
118
+ if (argv.length !== 2 || argv[0] !== "--parent" || !(argv[1] in DIRECTORY)) {
119
+ process.stderr.write(`${USAGE}\n`);
120
+ return 2;
121
+ }
122
+ return shouldWakeForDeliveries(path.join(runtimeStateBase(), `aiterm-mcp-${currentUid()}`), argv[1]) ? 0 : 1;
123
+ }
124
+ function invokedDirectly() {
125
+ if (!process.argv[1])
126
+ return false;
127
+ try {
128
+ return fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url));
129
+ }
130
+ catch {
131
+ return false;
132
+ }
133
+ }
134
+ if (invokedDirectly()) {
135
+ try {
136
+ process.exitCode = main(process.argv.slice(2));
137
+ }
138
+ catch (error) {
139
+ process.stderr.write(`aiterm-delivery-wake: ${error instanceof Error ? error.message : String(error)}\n`);
140
+ process.exitCode = 3;
141
+ }
142
+ }
package/dist/index.js CHANGED
@@ -253,7 +253,7 @@ registerRemoteAwareTool("pty_open", {
253
253
  inputSchema: {
254
254
  name: z.string().nullish().describe("セッション名(省略時は t1, t2... を自動採番)"),
255
255
  shell: z.string().default(DEFAULT_PTY_SHELL).describe(`起動シェル(既定 ${DEFAULT_PTY_SHELL})`),
256
- env_vars: z.array(z.string()).optional().describe("現在のMCP processからsessionへ継承する環境変数名"),
256
+ env_vars: z.array(z.string()).optional().describe("sessionへ登録する環境変数名(値は現在のMCP processから読む。pty_listのenv_keysで読める)。端末の環境は、名指ししなくても現在のMCP processの環境を継ぐ"),
257
257
  },
258
258
  }, async ({ name, shell, env_vars }) => {
259
259
  try {
@@ -1,6 +1,7 @@
1
1
  // OSのprocess表とnative PIDの所有者。argvは相関にだけ使い、公開identityにはdigestだけを載せる。
2
2
  import { spawnSync } from "node:child_process";
3
3
  import { createHash } from "node:crypto";
4
+ import path from "node:path";
4
5
  import { AitermError } from "./errors.js";
5
6
  import { isWin } from "./tmux-runtime.js";
6
7
  import { resolveWindowsPowerShell7 } from "./windows-powershell.js";
@@ -153,6 +154,12 @@ export function processSubtree(rows, rootPid) {
153
154
  }
154
155
  return rows.filter(row => selected.has(row.pid));
155
156
  }
157
+ // 中継(mcp-lazy)は、登録されたMCPの本体と先行起動の判定を、自分の直接の子として起こす。
158
+ // 見分けるのは起動した実行ファイルの名前(argvの先頭のbasenameが`mcp-lazy`で始まる)。取り決めはmcp-lazyのREADMEにある。
159
+ export function lazyRelayProcess(command) {
160
+ const first = /^(?:"([^"]+)"|(\S+))/.exec(command);
161
+ return path.posix.basename((first?.[1] ?? first?.[2] ?? "").replace(/\\/g, "/")).toLowerCase().startsWith("mcp-lazy");
162
+ }
156
163
  export function backgroundProcesses(rows, root) {
157
164
  // 起動時の足場processを除く既存契約。pane開始から60秒以上後に生成された子孫だけを集計する。
158
165
  const rootStart = Date.parse(root.started_identity);
@@ -118,11 +118,16 @@ export function tmuxSpawnEnv() {
118
118
  return env;
119
119
  }
120
120
  export function tmuxCommandWithInput(observe, input, ...args) {
121
+ return runTmux(observe, input, args);
122
+ }
123
+ // session は、新しい session を作る呼び出しの時だけ渡す client の環境(POSIX のみ)。server を起こし得るのは
124
+ // この呼び出しだけなので、空 config の指定と macOS の gui domain での起動もここに限る。
125
+ function runTmux(observe, input, args, session) {
121
126
  // maxBuffer は既定 1MiB。capture-pane(大きなスクロールバック)や多セッションの list-sessions で
122
127
  // 頭打ちになり stdout が切れる/空になる。Python の subprocess.run は無制限だったので 64MiB へ広げる。
123
128
  // Windows は tmux CLI 互換の native psmux を -L namespace 隔離で叩く(WSL 非依存)。
124
129
  let r;
125
- const spawnOpts = { encoding: "utf8", maxBuffer: 64 * 1024 * 1024, input, env: tmuxSpawnEnv() };
130
+ const spawnOpts = { encoding: "utf8", maxBuffer: 64 * 1024 * 1024, input, env: session ?? tmuxSpawnEnv() };
126
131
  if (isWin) {
127
132
  ensureWinPsmux(observe);
128
133
  r = spawnSync(psmuxBin(), ["-L", WIN_NS, ...args], spawnOpts);
@@ -130,8 +135,10 @@ export function tmuxCommandWithInput(observe, input, ...args) {
130
135
  else {
131
136
  // resolveTmux() は tmux を解決できなければ明確な AitermError を投げる(POSIX 版の事前確認)。
132
137
  const bin = resolveTmux(observe);
133
- r = (args[0] === "new-session" && macGuiServerWanted(bin) && startTmuxInMacGui(bin, args, spawnOpts.env ?? process.env))
134
- || spawnSync(bin, ["-S", SOCK, ...args], spawnOpts);
138
+ // -f は tmux 本体の引数で、server を起こす時にだけ読まれる(new-session の -f は別物で、config を指さない)。
139
+ const argv = session ? ["-f", TMUX_EMPTY_CONFIG, "-S", SOCK, ...args] : ["-S", SOCK, ...args];
140
+ r = (session && macGuiServerWanted(bin) && startTmuxInMacGui(bin, argv, session))
141
+ || spawnSync(bin, argv, spawnOpts);
135
142
  }
136
143
  // ENOBUFS(出力が 64MiB 超)を「code=1 の失敗」へ握り潰すと部分/空 stdout を正常扱いしてしまう。区別して投げる。
137
144
  // EXPECTED-FAILURE: 外部システム境界(tmux 出力過大)
@@ -191,8 +198,8 @@ export function macGuiTmuxPlist(label, dir, argv, env, cwd) {
191
198
  "",
192
199
  ].join("\n");
193
200
  }
194
- function startTmuxInMacGui(bin, args, env) {
195
- return runInMacGui([bin, "-S", SOCK, ...args], env, process.cwd(), 30_000, "launchd経由のtmux起動が30秒以内に終わりませんでした");
201
+ function startTmuxInMacGui(bin, argv, env) {
202
+ return runInMacGui([bin, ...argv], env, process.cwd(), 30_000, "launchd経由のtmux起動が30秒以内に終わりませんでした");
196
203
  }
197
204
  // 起動前の認証確認(`claude auth status`・`agent status` 等)も keychain を読むので、tmux server と同じく
198
205
  // Aqua の外では gui domain で実行する。gui domain が無ければ null を返し、呼び出し側が直接実行する。
@@ -306,21 +313,99 @@ export function pasteBufferBaseArgs() {
306
313
  }
307
314
  // new-session -f 用の空 config(端末個人の設定ファイルを読まない)。Windows は NUL デバイス。
308
315
  export const TMUX_EMPTY_CONFIG = isWin ? "NUL" : "/dev/null";
316
+ // tmux 3.2 で new-session -e と、update-environment のワイルドカードが入った。
317
+ function tmuxHasSessionEnvironmentFlags(version) {
318
+ const observed = version ?? spawnSync(resolveTmux(), ["-V"], { encoding: "utf8", timeout: 5000 }).stdout;
319
+ const match = /^tmux (\d+)\.(\d+)/.exec(observed ?? "");
320
+ if (!match)
321
+ throw new AitermError("tmuxの環境変数機能を判定できません", 2);
322
+ return Number(match[1]) > 3 || (Number(match[1]) === 3 && Number(match[2]) >= 2);
323
+ }
309
324
  export function sessionEnvironmentLaunch(shell, environment, version) {
310
- let modern = isWin;
311
- if (!isWin) {
312
- const observed = version ?? spawnSync(resolveTmux(), ["-V"], { encoding: "utf8", timeout: 5000 }).stdout;
313
- const match = /^tmux (\d+)\.(\d+)/.exec(observed ?? "");
314
- if (!match)
315
- throw new AitermError("tmuxの環境変数機能を判定できません", 2);
316
- modern = Number(match[1]) > 3 || (Number(match[1]) === 3 && Number(match[2]) >= 2);
317
- }
325
+ const modern = isWin || tmuxHasSessionEnvironmentFlags(version);
318
326
  if (modern)
319
327
  return { shell, args: environment.flatMap(entry => ["-e", entry]), register_after_start: false };
320
328
  // tmux <3.2はnew-session -eを持たない。子への継承とsession台帳への登録を製品内で完結する。
321
329
  const quote = (value) => `'${value.replace(/'/g, "'\\''")}'`;
322
330
  return { shell: `exec /usr/bin/env ${environment.map(quote).join(" ")} ${quote(shell)}`, args: [], register_after_start: true };
323
331
  }
332
+ // Aiterm が端末ごとに付け直す名札と、呼んだ側の pane を指す値。新しい端末へは継がせない。
333
+ const SESSION_OWN_ENV = /^(?:AITERM_SESSION_ID|AITERM_AGENT_[A-Z0-9_]+|TMUX|TMUX_PANE)$/;
334
+ const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
335
+ export function sessionCallerEnvironment(base = tmuxSpawnEnv() ?? process.env) {
336
+ const env = {};
337
+ for (const [key, value] of Object.entries(base)) {
338
+ if (value !== undefined && !SESSION_OWN_ENV.test(key))
339
+ env[key] = value;
340
+ }
341
+ return env;
342
+ }
343
+ // update-environment へ渡す並び。3.2 以上は "*" で client の環境を丸ごと写す。それより前は名前を並べる。
344
+ // client に無い名前は、その session で「消す」印になる。
345
+ export function updateEnvironmentPatterns(caller, others, wildcard) {
346
+ const names = caller.filter(key => ENV_NAME.test(key));
347
+ const known = new Set(names);
348
+ const rest = [...new Set(others)].filter(key => ENV_NAME.test(key) && !known.has(key));
349
+ return wildcard ? ["*", ...rest] : [...names, ...rest];
350
+ }
351
+ // `NAME=value` と、消す印の `-NAME` から名前を取り出す。server が無ければ null。
352
+ function tmuxEnvironmentNames(...scope) {
353
+ const r = runTmux(false, undefined, ["show-environment", ...scope]);
354
+ if (r.code !== 0)
355
+ return null;
356
+ return r.stdout.split("\n").map(line => line.replace(/^-/, "").split("=", 1)[0]).filter(key => ENV_NAME.test(key));
357
+ }
358
+ // session の表には開いた process の環境が丸ごと入る。名指しで登録した名前は、session の option に控える。
359
+ const REGISTERED_ENV_OPTION = "@aiterm_env_keys";
360
+ // 控えが無ければ null(Windows と、この仕組みより前の版が開いた session)。
361
+ export function registeredSessionEnvironment(name) {
362
+ if (isWin)
363
+ return null;
364
+ const r = runTmux(false, undefined, ["show-options", "-t", `=${name}:`, "-v", REGISTERED_ENV_OPTION]);
365
+ return r.code === 0 && r.stdout.trim() ? r.stdout.trim().split(" ") : null;
366
+ }
367
+ // tmux は、server を起こした client の環境を共通環境として全 session へ配る。何もしないと、最初に端末を
368
+ // 開いた呼び出し元の値が、後から別の呼び出し元が開く端末へ入る。新しい端末の環境を「開いた process の環境」に
369
+ // するため、new-session の間だけ update-environment を差し替えて client の環境を session へ写し、共通環境に
370
+ // しか無い変数は session で消す(psmux は元から端末ごとに呼び出し元の環境を継ぐ)。共通環境そのものは
371
+ // 変えない。同じ server を使う旧版の process が、開く端末の環境をそこから受け取っている。
372
+ export function tmuxNewSession(observe, name, launch, registered = []) {
373
+ if (isWin) {
374
+ return runTmux(observe, undefined, ["new-session", "-d", "-s", name, ...launch.args, "-f", TMUX_EMPTY_CONFIG, launch.shell]);
375
+ }
376
+ const env = sessionCallerEnvironment();
377
+ const wildcard = tmuxHasSessionEnvironmentFlags();
378
+ // tmux は起動時に PWD を共通環境へ足す。
379
+ const others = new Set(["PWD"]);
380
+ for (let attempt = 0;; attempt++) {
381
+ for (const key of tmuxEnvironmentNames("-g") ?? [])
382
+ others.add(key);
383
+ const r = runTmux(observe, undefined, [
384
+ "set-option", "-g", "update-environment", updateEnvironmentPatterns(Object.keys(env), [...others], wildcard).join(" "), ";",
385
+ "new-session", "-d", "-s", name, ...launch.args, launch.shell, ";",
386
+ "set-option", "-t", `=${name}:`, REGISTERED_ENV_OPTION, registered.join(" "), ";",
387
+ "set-option", "-gu", "update-environment",
388
+ ], env);
389
+ if (r.code !== 0) {
390
+ // 途中の命令が失敗すると、後続の命令は実行されない。差し替えた設定を戻す。
391
+ runTmux(false, undefined, ["set-option", "-gu", "update-environment"]);
392
+ return r;
393
+ }
394
+ // 共通環境の名前が、どれも session 側で上書きか消去になっていることを確かめる。別の呼び出し元が同時に
395
+ // server を起こした時は、消す対象を取り違えている。
396
+ const shared = tmuxEnvironmentNames("-g");
397
+ const own = new Set(tmuxEnvironmentNames("-t", `=${name}`) ?? []);
398
+ const uncovered = shared?.filter(key => !own.has(key));
399
+ if (uncovered?.length === 0)
400
+ return r;
401
+ runTmux(false, undefined, ["kill-session", "-t", `=${name}`]);
402
+ if (!uncovered || attempt >= 2) {
403
+ return { code: 1, stdout: "", stderr: "共通環境の変数をsessionから外せなかったため、sessionを破棄しました" };
404
+ }
405
+ for (const key of uncovered)
406
+ others.add(key);
407
+ }
408
+ }
324
409
  // 人が同じ session を覗く/介入するための attach コマンド。
325
410
  // Windows は native psmux(tmux CLI 互換)を -L namespace で叩く。
326
411
  export function attachCommand(name) {
package/docs/DESIGN.md CHANGED
@@ -41,6 +41,12 @@ sentinelの方言は端末runtimeが実効shellから決める。SSH先の現在
41
41
  psmuxが出力した余分な環境値を返さない。通常PTYとagentへ`AITERM_SESSION_ID`を注入し、
42
42
  `env_vars`の継承とsessionへの登録もAitermが所有する。古いtmuxは子へのenv注入とsession登録を使う。
43
43
 
44
+ 新しい端末の環境は、その端末を開いたprocessの環境にする(ADR 0081)。tmuxはserverを起こしたclientの環境を
45
+ 全sessionへ配るので、`new-session`の間だけ`update-environment`を差し替えてclientの環境をsessionへ写し、
46
+ 共通環境にしか無い変数はsessionで消す。`AITERM_SESSION_ID`・`AITERM_AGENT_*`・`TMUX`・`TMUX_PANE`は継がない。
47
+ sessionの表には環境が丸ごと入るので、名指しで登録した名前をsessionのoption `@aiterm_env_keys`に控え、
48
+ `env_keys`はその名前だけを返す。psmuxは元から端末ごとに呼び出し元の環境を継ぐ。
49
+
44
50
  `pty_observe`は存在、pane/harnessの生存、画面状態と理由、native process identityを分ける。
45
51
  PIDは開始識別子・argv digestと組にし、paneとharnessを同一視しない。特定できないidentityはnull。
46
52
  同じlaunchに属するnpm shimとnative本体は、中間の非候補processも含めた祖先関係から一つの起動として扱う。
@@ -48,6 +54,8 @@ PIDは開始識別子・argv digestと組にし、paneとharnessを同一視し
48
54
  POSIXの停止状態はOSのprocess表から取得し、SIGSTOP中は残画面より優先して`blocked/harness_stopped`を返す。
49
55
  sessionを閉じると失うものは数で返す(ADR 0080)。`activity.post_startup_process_count`は起動完了の時点に居なかったprocessの数、
50
56
  `pending_child_deliveries`はそのsessionが親として待つ未配送の数。数えられない時はnullで、0と区別する。
57
+ harnessと中継が自分のために立てるprocessは数えない(Codexの`codex-code-mode-host`、中継`mcp-lazy`の直接の子。ADR 0082)。その下は数える。
58
+ 中継の後ろで眠っている本体を配送の引き取りのために起こすかは、`aiterm-delivery-wake`が返す(ADR 0082)。
51
59
  Claude Codeの入力待ちは、起動時の見出しが取得範囲から流れ出た後も、入力欄の形(`❯`行の上下の罫線)で読む。
52
60
  画面本文とargv本文は返さず、活動cursorには画面digestとprocess別CPUだけを持たせる。
53
61
  初回とpane再作成後の差分はnull。区間中にprocessが消えた時は観測できたCPU増分だけを返し、
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiterm-mcp",
3
- "version": "0.49.0",
3
+ "version": "0.51.0",
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": [
@@ -43,6 +43,7 @@
43
43
  "aiterm-setup": "dist/setup-cli.js",
44
44
  "aiterm-runtime-errors": "dist/runtime-errors-cli.js",
45
45
  "aiterm-wait": "dist/aiterm-wait-cli.js",
46
+ "aiterm-delivery-wake": "dist/delivery-wake-cli.js",
46
47
  "aiterm-update": "dist/update-cli.js"
47
48
  },
48
49
  "files": [
@@ -70,7 +71,7 @@
70
71
  },
71
72
  "dependencies": {
72
73
  "@modelcontextprotocol/sdk": "^1.29.0",
73
- "aiterm-steer-delivery": "^0.1.10",
74
+ "aiterm-steer-delivery": "^0.1.12",
74
75
  "ws": "8.21.3",
75
76
  "zod": "^4.4.3"
76
77
  },