aiterm-mcp 0.55.4 → 0.56.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,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.56.1] - 2026-10-07
11
+
12
+ ### 修正
13
+
14
+ - `aiterm-parent-delivery`が、呼ぶ側のPATHにnodeの場所が無い環境でも、nodeで動くCodexの起動役(npm版の`codex`。1行目が
15
+ `#!/usr/bin/env node`)を起こせるようにする。0.56.0は、そういう環境(素のsshの環境、製品の常駐process、アプリ配下のprocess)から
16
+ 呼ぶと、`codex verify`が`CODEX_RECEIVER_TRANSPORT_FAILED`(exit=127)で断っていた。本文は送られていなかった。
17
+ - 命令が、自分を動かしているnodeの場所を、起こすCodexのPATHの頭へ足す。既にPATHにあれば何も変えない。
18
+ - 引数・返りの形は変わらない。Desktop同梱のCodex(macOS・Windows)は元から当たらない。
19
+
20
+ ## [0.56.0] - 2026-10-07
21
+
22
+ ### 追加
23
+
24
+ - ほかの製品が、Codexの親への回答をAitermの親配送へ頼む命令`aiterm-parent-delivery`を足す(ADR 0098)。製品はCodex用のhookを登録せず、
25
+ Aitermが登録したhookと公式キューで届く。決裁箱(Approval Box)が専用hookを捨てるための入口。
26
+ - `codex verify`・`codex submit`・`codex state`・`provider`。結果は1行のJSON。失敗は`{ok:false, code, message, outcome_unknown}`とexit 1。
27
+ - 設定・受付・実際に届いた事を分けて返す。`verify`の`steer`は設定(`enabled`=hookが登録・承認済みで親がhookの導入後に起きている、
28
+ `disabled`=公式キューだけ)。`submit`は公式キューの受付。`state`は`hook:"emitted"`と`turn_id`(hookがその番へ入れた)、`queued`(今も
29
+ 公式キューにあるか)。
30
+ - 同じ配送idの2回目は`PARENT_DELIVERY_DUPLICATE`で断る。対象はCodexの親だけ。
31
+ - 実物の確かめ(公式のCodex、一時HOME): 別のprocessから頼んで、動いている番へは同じ番へ、止まっている会話は公式キューが起こして届く。
32
+ hookを入れていない環境では`steer:"disabled"`と返り、番の後に届く。
33
+ - `aiterm-setup`が、命令の場所を`~/.config/aiterm-mcp/delivery-provider.json`へ残す。残せなければ成功にしない(`delivery_provider_failed`)。
34
+ - `dist/setup-integrations.js`に`ensureCodexParentSteer(home, options)`を足す。`aiterm-setup`を通らない導入(コンテナの起動時など)が、
35
+ Codexの親配送のhookを登録する入口。homeを引数で受け、その下だけを書く。動いているCodexは要らない。登録済みで承認済みなら何も書かない。
36
+ 失敗は投げずに`{status:"failed", reason_code}`で返す。`writeDeliveryProvider(home, registration)`も同じ所から出す。
37
+ - 今までの動きは変えていない。Aiterm自身の親配送、hookの登録の形、`aiterm-setup --codex-steer`はそのまま。
38
+
10
39
  ## [0.55.4] - 2026-10-06
11
40
 
12
41
  ### 変更
@@ -2314,7 +2343,9 @@ prototype (preserved under `prototype/python/` as the porting source and referen
2314
2343
  `ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
2315
2344
  provenance.
2316
2345
 
2317
- [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.55.4...HEAD
2346
+ [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.56.1...HEAD
2347
+ [0.56.1]: https://github.com/kitepon/aiterm-mcp/compare/v0.56.0...v0.56.1
2348
+ [0.56.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.55.4...v0.56.0
2318
2349
  [0.55.4]: https://github.com/kitepon/aiterm-mcp/compare/v0.55.3...v0.55.4
2319
2350
  [0.55.3]: https://github.com/kitepon/aiterm-mcp/compare/v0.55.2...v0.55.3
2320
2351
  [0.55.2]: https://github.com/kitepon/aiterm-mcp/compare/v0.55.1...v0.55.2
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.55.4** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
212
+ **状態:** 開発継続中 · 現行公開版 **v0.56.1** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
213
213
 
214
214
  ### 更新と巻き戻し
215
215
 
@@ -528,6 +528,15 @@ agent sessionを閉じると失うものは、次の2項目で分かる。`activ
528
528
  子の結果の数。呼び出した側が誰でも付く。どちらも、分からない時はnull(通常PTY、harnessのprocessを特定できない時。
529
529
  processの数は0.48.0以前が起動したagentも)。別端末が旧版の時は項目ごと無い。nullと項目なしは「分からない」で、0ではない。
530
530
 
531
+ ほかの製品が、Codexの親へ回答を届ける時は、製品のhookを登録せずに`aiterm-parent-delivery`でAitermへ頼める。Aitermが登録したhookと
532
+ 公式キューで届く。`codex verify --thread <uuid>`は親を確かめ、`steer`で設定を返す(`enabled`=hookが登録・承認済みで親がhookの導入後に
533
+ 起きている。`disabled`=公式キューだけで、ターンが終わってから届く)。`codex submit --thread <uuid> --delivery <uuid> --text-file <file|->`は
534
+ 本文を一度だけ渡し、公式キューの受付IDを返す。`codex state --thread <uuid> --delivery <uuid>`は届き方の事実を返す(`hook:"emitted"`と
535
+ `turn_id`=hookがそのターンへ本文を入れた。`queued`=今も公式キューにあるか)。結果は1行のJSONで、失敗は
536
+ `{ok:false, code, message, outcome_unknown}`とexit 1。同じ配送IDの2回目は`PARENT_DELIVERY_DUPLICATE`で断る。対象はCodexの親だけ。
537
+ 命令の場所は`aiterm-setup`が`~/.config/aiterm-mcp/delivery-provider.json`へ残す。Nodeの製品は`aiterm-steer-delivery`(0.3.0以上)の
538
+ `submitCodexParentAnswerViaAiterm`などから呼べる。
539
+
531
540
  MCPの本体を初めて使う時まで起こさない中継の後ろにAitermを登録すると、眠っている本体は、終了した持ち主の配送を引き取れない。
532
541
  `aiterm-delivery-wake --parent <codex|claude|cursor>`は、起こす必要があるかを返す。その種類の親の配送を、終了した持ち主が
533
542
  持ったままならexit 0、そうでなければexit 1、引数の誤りはexit 2。何も出力せず、子processを起こさない。同じ判定を回す席のうち
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.55.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).
226
+ **Status:** actively maintained · current public release **v0.56.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).
227
227
 
228
228
  ### Update and rollback
229
229
 
@@ -563,6 +563,16 @@ is the number of sub-agent results this session is still waiting for as a parent
563
563
  Aiterm cannot tell (ordinary terminals, agents launched by 0.48.0 or earlier for the process count, or an unresolved harness
564
564
  process), and may be absent when a remote host runs an older Aiterm. Treat null or absent as unknown, not as zero.
565
565
 
566
+ Another product that needs to deliver an answer to a Codex parent can ask Aiterm with `aiterm-parent-delivery` instead of registering its own hooks.
567
+ Aiterm's registered hooks and the official queue deliver it. `codex verify --thread <uuid>` checks the parent and reports configuration in `steer`
568
+ (`enabled`: the hooks are registered and trusted and the parent started after they were installed; `disabled`: official queue only, so the text arrives
569
+ after the turn ends). `codex submit --thread <uuid> --delivery <uuid> --text-file <file|->` hands the text over once and returns the queue's acceptance id.
570
+ `codex state --thread <uuid> --delivery <uuid>` reports how it arrived (`hook:"emitted"` with `turn_id`: the hook put the text into that turn; `queued`:
571
+ whether it is still in the official queue). Each result is one JSON line; failures are `{ok:false, code, message, outcome_unknown}` with exit 1, and a
572
+ reused delivery id is refused with `PARENT_DELIVERY_DUPLICATE`. Only Codex parents are supported. `aiterm-setup` records the command's location in
573
+ `~/.config/aiterm-mcp/delivery-provider.json`. Node products can call it through `aiterm-steer-delivery` (0.3.0 or later), for example
574
+ `submitCodexParentAnswerViaAiterm`.
575
+
566
576
  When Aiterm is registered behind a relay that starts the MCP server only on first use, a sleeping server cannot take over the
567
577
  deliveries of an owner that has exited. `aiterm-delivery-wake --parent <codex|claude|cursor>` answers whether one needs to start:
568
578
  exit 0 when an exited owner still holds a delivery for that parent kind, exit 1 otherwise, exit 2 for bad arguments. It prints
@@ -28,6 +28,20 @@ export async function verifyCodexParent(parent, runtime) {
28
28
  }
29
29
  await steer.verifyCodexParent(AITERM_PROFILE, parent, runtime);
30
30
  }
31
+ /**
32
+ * 親を確かめ、渡した文が同じ番へ入るかどうかも返す。ほかの製品がAitermへ配送を頼む入口(aiterm-parent-delivery)が使う。
33
+ * 確かめる中身は`verifyCodexParent`と同じ。hookが有効なのに未承認・親が導入前から動いている時は、今までどおり理由つきで断る。
34
+ */
35
+ export async function checkCodexParent(parent, runtime) {
36
+ const socket = relaySocket(runtime);
37
+ if (socket) {
38
+ await withCodexRelay(socket, request => verifyLoadedParent(request, parent.thread_id), runtime?.timeout_ms);
39
+ return { thread: { thread_id: parent.thread_id, cwd: null, source: null }, steer: "enabled" };
40
+ }
41
+ const thread = await steer.verifyCodexParent(AITERM_PROFILE, parent, runtime);
42
+ const config = runtime ? (runtime.hook_directory ? readCodexHookConfig(runtime.hook_directory) : null) : readCodexHookConfig();
43
+ return { thread, steer: config?.enabled ? "enabled" : "disabled" };
44
+ }
31
45
  export async function submitCodexParentAnswer(parent, deliveryId, text, runtime) {
32
46
  const socket = relaySocket(runtime);
33
47
  if (socket) {
@@ -0,0 +1,37 @@
1
+ // ほかの製品が、Aitermの親配送の入口(aiterm-parent-delivery)の場所を知るための記録(ADR 0098)。
2
+ // 製品のprocess(MCP serverや常駐process)は、PATHにnpmの置き場が無い環境で起きる事がある(macOSのアプリ配下など)。
3
+ // setupが、nodeと命令の絶対pathをHOMEの下へ残す。製品は読むだけで、書かない。
4
+ import * as fs from "node:fs";
5
+ import * as path from "node:path";
6
+ import { homedir } from "node:os";
7
+ import { createRequire } from "node:module";
8
+ import { isDeepStrictEqual } from "node:util";
9
+ import { writeJson0600 } from "aiterm-steer-delivery";
10
+ export const DELIVERY_PROVIDER_SCHEMA = "aiterm.delivery-provider.v1";
11
+ const pkg = createRequire(import.meta.url)("../package.json");
12
+ export function deliveryProviderFile(home = process.env.HOME ?? homedir()) {
13
+ return path.join(home, ".config", "aiterm-mcp", "delivery-provider.json");
14
+ }
15
+ /**
16
+ * 登録(nodeの絶対pathとdist/index.js)から、同じ導入のaiterm-parent-deliveryの場所を記録する。変わりが無ければ書かない。
17
+ * MCPの本体の起動時には呼ばない(試験用に別の場所から起こした本体が、導入済みの記録を書き換えるため)。
18
+ */
19
+ export function writeDeliveryProvider(home, registration) {
20
+ const entry = registration.args[0];
21
+ if (!path.isAbsolute(home) || !path.isAbsolute(registration.command) || typeof entry !== "string" || !path.isAbsolute(entry)) {
22
+ throw new Error("delivery providerの記録には、homeと登録の絶対pathが要ります");
23
+ }
24
+ const cli = path.join(path.dirname(entry), "parent-delivery-cli.js");
25
+ if (!fs.existsSync(cli))
26
+ throw new Error("aiterm-parent-deliveryの入口が、登録と同じ導入の中にありません");
27
+ const next = { schema: DELIVERY_PROVIDER_SCHEMA, version: pkg.version, node: registration.command, cli };
28
+ const file = deliveryProviderFile(home);
29
+ try {
30
+ if (isDeepStrictEqual(JSON.parse(fs.readFileSync(file, "utf8")), next))
31
+ return "unchanged";
32
+ }
33
+ catch { /* 無い・読めない時は書く */ }
34
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
35
+ writeJson0600(file, next);
36
+ return "configured";
37
+ }
@@ -0,0 +1,208 @@
1
+ #!/usr/bin/env node
2
+ // aiterm-parent-delivery — ほかの製品が、確定した回答をAitermの親配送へ頼む入口(ADR 0098)。
3
+ // Aitermが登録したhookと公式キューで渡す。頼む製品は自分のhookを登録せず、Aitermの中のfileも書かない。
4
+ // 結果はstdoutへ1行のJSONで返す。成功は {ok:true, schema, …}、失敗は {ok:false, schema, code, message, outcome_unknown} と exit 1。
5
+ // outcome_unknown:true は「受け付けたかどうか確定できない」。その時は送り直さない。
6
+ //
7
+ // aiterm-parent-delivery provider
8
+ // aiterm-parent-delivery codex verify --thread <uuid> [--codex-home <dir>]
9
+ // aiterm-parent-delivery codex submit --thread <uuid> --delivery <uuid> --text-file <file|-> [--codex-home <dir>]
10
+ // aiterm-parent-delivery codex state --thread <uuid> --delivery <uuid> [--codex-home <dir>]
11
+ //
12
+ // 「設定が整っている」「受け付けた」「実際に届いた」は別の事として返す。
13
+ // verify の steer:"enabled" は設定の話(hookが登録・承認済みで、親がhookの導入後に起きている)。届いた事は言わない。
14
+ // submit の queued_submission_id は、公式キューが受け付けた事。
15
+ // state は届き方の事実を返す。hook:"emitted" と turn_id は、Aitermのhookがその番へ本文を入れた事。queued は、今も公式キューに残っているか。
16
+ //
17
+ // 対象はCodexの親だけ。親の指定は、CodexがMCP要求へ付けるthreadIdと、そのCodexのCODEX_HOME。
18
+ import * as fs from "node:fs";
19
+ import * as path from "node:path";
20
+ import { createRequire } from "node:module";
21
+ import { fileURLToPath } from "node:url";
22
+ import { realCodexHome } from "aiterm-steer-delivery";
23
+ import { checkCodexParent, submitCodexParentAnswer, withCodexReceiver } from "./codex-parent-receiver.js";
24
+ import { codexHookDeliveryState, codexHookDirectory, codexInputDirectory } from "./codex-hook-state.js";
25
+ export const PARENT_DELIVERY_SCHEMA = "aiterm.parent-delivery.v1";
26
+ const pkg = createRequire(import.meta.url)("../package.json");
27
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
28
+ const USAGE = "usage: aiterm-parent-delivery provider | codex <verify|submit|state> --thread <uuid> [--delivery <uuid>] [--text-file <file|->] [--codex-home <dir>]";
29
+ class UsageError extends Error {
30
+ }
31
+ /** 同じ配送idの本文が、もう配送の記録にある。送り直しで同じ回答が2回届く事を止める。 */
32
+ class DuplicateError extends Error {
33
+ }
34
+ /** この配送idの所有記録(送る途中・hookが取り出した後)が、Aitermのhookの置き場に残っているか。hookを入れていない環境には記録が無い。 */
35
+ function deliveryRecorded(target, delivery) {
36
+ const root = codexHookDirectory();
37
+ if (!fs.existsSync(path.join(root, "inputs")) || !fs.existsSync(target.codex_home))
38
+ return false;
39
+ const directory = codexInputDirectory(root, target.codex_home, target.thread_id);
40
+ return ["pending", "claims"].some(kind => fs.existsSync(path.join(directory, kind, `${delivery}.json`)));
41
+ }
42
+ function option(args, name) {
43
+ const index = args.indexOf(name);
44
+ if (index < 0)
45
+ return undefined;
46
+ const value = args[index + 1];
47
+ if (value === undefined)
48
+ throw new UsageError(`${name} に値がありません。${USAGE}`);
49
+ return value;
50
+ }
51
+ function uuid(args, name) {
52
+ const value = option(args, name);
53
+ if (value === undefined)
54
+ throw new UsageError(`${name} は必須です。${USAGE}`);
55
+ if (!UUID_RE.test(value))
56
+ throw new UsageError(`${name} はUUIDで指定してください。${USAGE}`);
57
+ return value;
58
+ }
59
+ function parent(args) {
60
+ const home = option(args, "--codex-home");
61
+ if (home !== undefined && !path.isAbsolute(home))
62
+ throw new UsageError(`--codex-home は絶対pathで指定してください。${USAGE}`);
63
+ return { thread_id: uuid(args, "--thread"), codex_home: path.resolve(home ?? realCodexHome()) };
64
+ }
65
+ async function readText(source) {
66
+ if (source !== "-")
67
+ return fs.readFileSync(source, "utf8");
68
+ let text = "";
69
+ process.stdin.setEncoding("utf8");
70
+ for await (const chunk of process.stdin)
71
+ text += chunk;
72
+ return text;
73
+ }
74
+ /** 届き方の事実を読む。何も書き換えない。 */
75
+ async function deliveryState(target, delivery) {
76
+ const state = codexHookDeliveryState(target.codex_home, target.thread_id, delivery);
77
+ let hook = null;
78
+ let turn = null;
79
+ const root = codexHookDirectory();
80
+ if (fs.existsSync(path.join(root, "inputs")) && fs.existsSync(target.codex_home)) {
81
+ const directory = codexInputDirectory(root, target.codex_home, target.thread_id);
82
+ let claim = null;
83
+ try {
84
+ claim = JSON.parse(fs.readFileSync(path.join(directory, "claims", `${delivery}.json`), "utf8"));
85
+ }
86
+ catch (error) {
87
+ if (error.code !== "ENOENT")
88
+ throw error;
89
+ }
90
+ if (claim?.state === "emitted") {
91
+ hook = "emitted";
92
+ turn = typeof claim.turn_id === "string" ? claim.turn_id : null;
93
+ }
94
+ else if (claim?.state === "not_in_queue")
95
+ hook = "not_in_queue";
96
+ else if (state)
97
+ hook = state;
98
+ // 所有権のlinkだけを作った段階(まだ公式キューから取り出していない)は、受け付けた後と同じ。
99
+ else if (claim !== null || fs.existsSync(path.join(directory, "pending", `${delivery}.json`)))
100
+ hook = "pending";
101
+ }
102
+ try {
103
+ const queued = await withCodexReceiver(target, async (request) => {
104
+ let cursor = null;
105
+ do {
106
+ const page = await request("thread/queue/list", { threadId: target.thread_id, cursor, limit: 100 });
107
+ if (!Array.isArray(page?.data))
108
+ throw new Error("queue list invalid");
109
+ if (page.data.some((entry) => entry?.clientUserMessageId === delivery))
110
+ return true;
111
+ cursor = typeof page.nextCursor === "string" ? page.nextCursor : null;
112
+ } while (cursor);
113
+ return false;
114
+ });
115
+ return { state, hook, turn_id: turn, queued };
116
+ }
117
+ catch (error) {
118
+ const code = error?.delivery_code;
119
+ return { state, hook, turn_id: turn, queued: null, queue_error: typeof code === "string" ? code : "CODEX_QUEUE_UNREADABLE" };
120
+ }
121
+ }
122
+ /**
123
+ * 起こすCodexが、この命令を動かしているnodeを見つけられるPATHを返す。
124
+ * npm版のCodexは`#!/usr/bin/env node`の起動役で、呼ぶ側のPATHにnodeの場所が無いと起きない(exit 127)。製品の常駐processや
125
+ * アプリ配下のprocessは、PATHが細い事がある(2026-10-07、素のsshの環境からの呼び出しで起きた)。
126
+ * nodeの場所がPATHに無い時だけ頭へ足す。既にあれば、並びも中身も変えない。
127
+ */
128
+ export function pathWithNode(env, node = process.execPath, platform = process.platform) {
129
+ const windows = platform === "win32";
130
+ // Windowsの環境変数名は大文字小文字を区別しない(`Path`で入っている事が多い)。
131
+ const key = Object.keys(env).find(name => windows ? name.toLowerCase() === "path" : name === "PATH") ?? "PATH";
132
+ const delimiter = windows ? ";" : ":";
133
+ const directory = (windows ? path.win32 : path.posix).dirname(node);
134
+ const current = env[key] ?? "";
135
+ const listed = current.split(delimiter).some(part => windows ? part.toLowerCase() === directory.toLowerCase() : part === directory);
136
+ return { key, value: listed ? current : current ? `${directory}${delimiter}${current}` : directory };
137
+ }
138
+ /** この命令の場所と版。製品側の入口(aiterm-steer-delivery)が、呼べる相手かどうかを確かめるのに使う。 */
139
+ export function providerDescription() {
140
+ return { version: pkg.version, node: process.execPath, cli: fileURLToPath(import.meta.url) };
141
+ }
142
+ export async function runParentDelivery(argv) {
143
+ const [kind, command, ...args] = argv;
144
+ if (kind === "provider" && command === undefined)
145
+ return providerDescription();
146
+ if (kind !== "codex")
147
+ throw new UsageError(`対応する親は codex だけです。${USAGE}`);
148
+ switch (command) {
149
+ case "verify": {
150
+ const checked = await checkCodexParent(parent(args));
151
+ return { verified: true, thread: checked.thread, steer: checked.steer };
152
+ }
153
+ case "submit": {
154
+ const target = parent(args);
155
+ const delivery = uuid(args, "--delivery");
156
+ const source = option(args, "--text-file");
157
+ if (source === undefined)
158
+ throw new UsageError(`--text-file は必須です。${USAGE}`);
159
+ const text = await readText(source);
160
+ if (text.length === 0)
161
+ throw new UsageError("本文が空です。文字列は送っていません。");
162
+ // 配送idは本文ごとに新しいUUID。同じidをもう一度受けると、同じ回答が2回届く。
163
+ if (deliveryRecorded(target, delivery))
164
+ throw new DuplicateError("同じ配送idの本文は、もう受け付けています。文字列は送っていません。");
165
+ return await submitCodexParentAnswer(target, delivery, text);
166
+ }
167
+ case "state": {
168
+ const target = parent(args);
169
+ return await deliveryState(target, uuid(args, "--delivery"));
170
+ }
171
+ default: throw new UsageError(`未対応のcommandです: ${String(command)}。${USAGE}`);
172
+ }
173
+ }
174
+ function isDirectExecution() {
175
+ const entry = process.argv[1];
176
+ if (!entry)
177
+ return false;
178
+ try {
179
+ const a = fs.realpathSync(entry);
180
+ const b = fs.realpathSync(fileURLToPath(import.meta.url));
181
+ return a === b || (process.platform === "win32" && a.toLowerCase() === b.toLowerCase());
182
+ }
183
+ catch {
184
+ return false;
185
+ }
186
+ }
187
+ if (isDirectExecution()) {
188
+ try {
189
+ const found = pathWithNode(process.env);
190
+ process.env[found.key] = found.value;
191
+ process.stdout.write(JSON.stringify({ ok: true, schema: PARENT_DELIVERY_SCHEMA, ...(await runParentDelivery(process.argv.slice(2))) }) + "\n");
192
+ }
193
+ catch (error) {
194
+ // 配送の誤り(delivery_code)と使い方の誤りは、文面が呼び出し元向けに決めてあるのでそのまま返す。
195
+ // それ以外の例外は、中身(path・本文)を出さずに固定の文面へ落とす。
196
+ const value = error;
197
+ const delivery = typeof value?.delivery_code === "string" ? value.delivery_code : null;
198
+ const usage = error instanceof UsageError;
199
+ const duplicate = error instanceof DuplicateError;
200
+ process.stdout.write(JSON.stringify({
201
+ ok: false, schema: PARENT_DELIVERY_SCHEMA,
202
+ code: delivery ?? (usage ? "PARENT_DELIVERY_USAGE" : duplicate ? "PARENT_DELIVERY_DUPLICATE" : "PARENT_DELIVERY_FAILED"),
203
+ message: delivery || usage || duplicate ? String(value.message) : "aiterm-parent-delivery: operation failed",
204
+ outcome_unknown: value?.outcome_unknown === true,
205
+ }) + "\n");
206
+ process.exitCode = 1;
207
+ }
208
+ }
@@ -1,7 +1,8 @@
1
1
  // Steerの正規導入。公式hookを登録・承認してから、旧中継の起動差し替えを解除する。本体はaiterm-steer-delivery。
2
+ import * as path from "node:path";
2
3
  import { fileURLToPath } from "node:url";
3
4
  import * as steer from "aiterm-steer-delivery";
4
- import { readRelayConfig } from "./codex-relay-config.js";
5
+ import { readRelayConfig, relayConfigDirectory } from "./codex-relay-config.js";
5
6
  import { configureCodexSteer as configureLegacyRelay } from "./setup-codex-relay.js";
6
7
  import { readCodexHookConfig } from "./codex-hook-state.js";
7
8
  import { AITERM_PROFILE } from "./steer-profile.js";
@@ -20,3 +21,41 @@ export async function configureCodexSteer(action = "status", overrides = {}) {
20
21
  ...overrides,
21
22
  });
22
23
  }
24
+ /**
25
+ * aiterm-setupを通らない導入(コンテナの起動時など)が、Codexの親配送のhookを登録する入口(ADR 0098)。
26
+ * - `home`を引数で受け、その下(`.codex`と`.config/aiterm-mcp/codex-parent-hooks`)だけを書く。
27
+ * - 動いているCodexは要らない。Codexの実行ファイルを一時的に起こし、登録の読戻しと承認を公式の口(hooks/list・config/batchWrite)で行う。
28
+ * - 登録済みで、公式の読戻しでも承認済みなら、何も書かない(`changed:false`)。登録が外れていた・commandが変わった時だけ書き直す。
29
+ * - 失敗は投げない。`{status:"failed", reason_code}`で返す(書き換える前の`hooks.json`は`.aiterm-backup`に残る)。
30
+ * 登録より前から動いているCodexは、起き直すまで同じ番への配送を受けられない(`restart_required`)。
31
+ */
32
+ export async function ensureCodexParentSteer(home, options = {}) {
33
+ try {
34
+ if (!path.isAbsolute(home))
35
+ throw new steer.SetupError("codex_steer_home_invalid", "homeは絶対pathで指定してください");
36
+ const directory = path.join(home, ".config", "aiterm-mcp", "codex-parent-hooks");
37
+ const codexHome = options.codex_home ?? path.join(home, ".codex");
38
+ const hook = fileURLToPath(new URL("./codex-parent-hook.js", import.meta.url));
39
+ const overrides = {
40
+ directory, codex_home: codexHome,
41
+ legacy: () => readRelayConfig(relayConfigDirectory(home)),
42
+ verify: (config, approve) => steer.verifyCodexHookRegistration(AITERM_PROFILE, config, approve, directory),
43
+ ...(options.binary ? { findBinary: () => options.binary } : {}),
44
+ ...options.runtime,
45
+ };
46
+ const previous = readCodexHookConfig(directory);
47
+ const expected = steer.codexHookCommand(steer.setupNodeExecutable(process.execPath), hook, directory);
48
+ if (previous?.enabled && previous.command === expected && path.resolve(previous.codex_home) === path.resolve(codexHome)) {
49
+ // 登録の読戻しが通れば書かない。通らない時(ほかの導入がhooks.jsonを書き直して登録が外れた等)は、下で登録し直す。
50
+ const status = await configureCodexSteer("status", overrides).catch(() => null);
51
+ if (status && (status.status === "ready" || status.status === "restart_required"))
52
+ return { ...status, changed: false };
53
+ }
54
+ return { ...await configureCodexSteer("enable", overrides), changed: true };
55
+ }
56
+ catch (error) {
57
+ const value = error;
58
+ const reason = typeof value?.delivery_code === "string" ? value.delivery_code : typeof value?.code === "string" ? value.code : "codex_steer_setup_failed";
59
+ return { status: "failed", reason_code: reason, changed: false };
60
+ }
61
+ }
@@ -9,6 +9,9 @@ import { lazyRelayExecutable } from "./lazy-relay.js";
9
9
  import { SetupError, runSetupCommand } from "./setup-platform.js";
10
10
  import { AITERM_PROFILE } from "./steer-profile.js";
11
11
  export { powershellInvocation } from "./setup-platform.js";
12
+ // aiterm-setupを通らない導入(コンテナの起動時)が、ClaudeとCursorのhookと同じ並びで呼べるように、ここからも出す。
13
+ export { ensureCodexParentSteer } from "./setup-codex-hooks.js";
14
+ export { writeDeliveryProvider, deliveryProviderFile } from "./delivery-provider.js";
12
15
  const record = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
13
16
  /**
14
17
  * 既にある登録が、今から書く登録と同じ本体を指しているか。
package/dist/setup.js CHANGED
@@ -9,6 +9,7 @@ import { CallToolResultSchema } from "@modelcontextprotocol/sdk/types.js";
9
9
  import { prepareBackend, runSetupCommand, SetupError } from "./setup-platform.js";
10
10
  import { configureIntegrations, configureParentHooks } from "./setup-integrations.js";
11
11
  import { configureCodexSteer, codexSteerSelected } from "./setup-codex-hooks.js";
12
+ import { writeDeliveryProvider } from "./delivery-provider.js";
12
13
  import { setupNodeExecutable } from "./setup-node.js";
13
14
  /** 実行中のNodeに付属するnpmの既定のglobal root。npmのprefixを環境変数や設定で別の場所へ向けていても変わらない。 */
14
15
  export function nodeDefaultGlobalRoot(execPath = process.execPath, platform = process.platform) {
@@ -95,6 +96,9 @@ export async function runSetup(options = {}) {
95
96
  else
96
97
  result.status = "ready";
97
98
  if (result.status === "ready") {
99
+ // ほかの製品がAitermの親配送を頼む入口(aiterm-parent-delivery)の場所を残す。
100
+ stage = "delivery_provider";
101
+ (options.provider ?? writeDeliveryProvider)(process.env.HOME ?? homedir(), registration);
98
102
  stage = "codex_steer";
99
103
  result.codex_steer = await (options.steer ?? configureCodexSteer)(options.codex_steer ?? (codexSteerSelected() ? "enable" : "status"));
100
104
  if (["failed", "unsupported", "restart_required"].includes(result.codex_steer.status)) {
package/docs/DESIGN.md CHANGED
@@ -68,6 +68,10 @@ harnessと中継が自分のために立てるprocessは数えない(Codexの`
68
68
  harnessが同じ親の下に同じargvで立て直した起動時のprocessも数えない(ADR 0083)。
69
69
  Aitermがprocess表を引くために起こしたprocess(`ps`、WindowsのPowerShellと付いて立つconsole host)も数えない(ADR 0084)。
70
70
  中継の後ろで眠っている本体を配送の引き取りのために起こすかは、`aiterm-delivery-wake`が返す(ADR 0082)。
71
+ ほかの製品がCodexの親へ回答を届ける時は、製品のhookを登録せず、`aiterm-parent-delivery`でAitermの親配送へ頼む(ADR 0098)。
72
+ Aitermの登録済みhookと公式キューで届け、設定(`steer`)・受付(`queued_submission_id`)・実際に届いた事(`hook:"emitted"`と`turn_id`)を分けて返す。
73
+ 命令の場所は`aiterm-setup`が`~/.config/aiterm-mcp/delivery-provider.json`へ残す。`aiterm-setup`を通らない導入は、
74
+ `dist/setup-integrations.js`の`ensureCodexParentSteer(home)`でCodexのhookを登録する。
71
75
  Claude Codeの入力待ちは、起動時の見出しが取得範囲から流れ出た後も、入力欄の形(`❯`行の上下の罫線)で読む。
72
76
  画面本文とargv本文は返さず、活動cursorには画面digestとprocess別CPUだけを持たせる。
73
77
  初回とpane再作成後の差分はnull。区間中にprocessが消えた時は観測できたCPU増分だけを返し、
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiterm-mcp",
3
- "version": "0.55.4",
3
+ "version": "0.56.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": [
@@ -44,6 +44,7 @@
44
44
  "aiterm-runtime-errors": "dist/runtime-errors-cli.js",
45
45
  "aiterm-wait": "dist/aiterm-wait-cli.js",
46
46
  "aiterm-delivery-wake": "dist/delivery-wake-cli.js",
47
+ "aiterm-parent-delivery": "dist/parent-delivery-cli.js",
47
48
  "aiterm-update": "dist/update-cli.js"
48
49
  },
49
50
  "files": [
@@ -71,7 +72,7 @@
71
72
  },
72
73
  "dependencies": {
73
74
  "@modelcontextprotocol/sdk": "^1.29.0",
74
- "aiterm-steer-delivery": "^0.2.3",
75
+ "aiterm-steer-delivery": "^0.3.0",
75
76
  "ws": "8.21.3",
76
77
  "zod": "^4.4.3"
77
78
  },