aiterm-mcp 0.37.0 → 0.37.2

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,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.37.2] - 2026-09-13
11
+
12
+ ### 修正
13
+
14
+ - Codex Steerの引数、通信中継、設定の有効化・復元をmacOSとWindowsで共通化した。OS差は起動API・接続・権限・設定保存に限定する。
15
+ - Windowsで公式Codexの直接の親がDesktopではなくなり、他アプリの公式CLI検出を妨げていた構成を修正した。公式CodexをDesktopの直接の子、中継を公式Codexの子として起動する。
16
+ - Windowsだけの既存launcher共有を廃止し、Macと同じくAitermのlauncherへ設定して解除時に元へ戻す。旧共有設定も次のenableで移行する。
17
+
18
+ ## [0.37.1] - 2026-09-13
19
+
20
+ ### 修正
21
+
22
+ - WindowsのCodexでhook確認画面の見出しが画面上部へ出た場合も認識し、`trust_project: true`による起動準備を完了する。
23
+ - npmのshell shimとnative Codexの間にNode processがある場合、同じ起動を複数候補と誤判定して停止する問題を修正した。
24
+
10
25
  ## [0.37.0] - 2026-09-13
11
26
 
12
27
  ### 追加
@@ -1664,7 +1679,9 @@ prototype (preserved under `prototype/python/` as the porting source and referen
1664
1679
  `ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
1665
1680
  provenance.
1666
1681
 
1667
- [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.37.0...HEAD
1682
+ [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.37.2...HEAD
1683
+ [0.37.2]: https://github.com/kitepon/aiterm-mcp/compare/v0.37.1...v0.37.2
1684
+ [0.37.1]: https://github.com/kitepon/aiterm-mcp/compare/v0.37.0...v0.37.1
1668
1685
  [0.37.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.36.0...v0.37.0
1669
1686
  [0.36.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.35.1...v0.36.0
1670
1687
  [0.35.1]: https://github.com/kitepon/aiterm-mcp/compare/v0.35.0...v0.35.1
package/README.ja.md CHANGED
@@ -57,7 +57,7 @@ aiterm-setup --json --codex-steer enable
57
57
 
58
58
  公式Codex Desktopに同梱されたApp Serverを使い、実行中の親には同じターンへ回答を送り、
59
59
  終了後に届いた回答でも同じタスクを自動再開します。App Serverの改造・再ビルド・別配布は不要です。
60
- Steerの初回設定後は`restart_required`(終了コード3)を返します。Codexを完全終了して再起動し、
60
+ 新しいlauncherを設定した場合は`restart_required`(終了コード3)を返します。Codexを完全終了して再起動し、
61
61
  `aiterm-setup --codex-steer status`で`ready`を確認してください。接続できない時にキューへ自動退避しません。
62
62
 
63
63
  選択と復元情報は`~/.config/aiterm-mcp/codex-relay/`へ保存します。macOSはユーザーLaunchAgent、
@@ -66,8 +66,9 @@ Windowsはユーザー環境変数`CODEX_CLI_PATH`で起動設定を維持しま
66
66
  SteerはmacOS・Windowsの公式Codex Desktopと同梱CLI 0.154以上に対応します。Steer有効時の宛先は中継で起動したDesktopに限ります。
67
67
  Windowsでは公式Desktopが展開した実行ファイルを照合し、認証付きのローカル接続へ中継します。
68
68
  Aitermのpackageがlauncher・中継・設定・親への配送を含み、gpt-connectorやdotagentsの導入は不要です。
69
- 既存の互換launcherがある場合は、稼働中のDesktop接続を確認した時だけ共有し、その起動設定を変更しません。
70
- 互換性を確認できない設定は理由付きで停止します。WindowsのDesktop更新後はsetupを再実行してください。
69
+ 両OSで公式CodexをDesktopの直接の子として保ち、中継は公式Codexの子として動きます。
70
+ 有効化時は元の起動設定を保存してAitermのlauncherを設定し、解除時に元へ戻します。選択後に他から変更された値は上書きしません。
71
+ WindowsのDesktop更新後はsetupを再実行してください。
71
72
  LinuxのSteer付き導入は理由付き`unsupported`を返します。Aiterm単品は従来どおり利用できます。
72
73
 
73
74
  cloneもビルドも不要。どのクライアントでも公開パッケージを次のコマンドで起動する:
@@ -195,7 +196,7 @@ runtime-error store は canonical dotagents config の `collection.enabled: true
195
196
  場合だけ収集し、既定OFF、network送信は行いません。tag起点CIのnpm provenance(OIDC Trusted
196
197
  Publishing)で公開し、GitHub Release が Official MCP Registry を再登録します。
197
198
 
198
- **状態:** 開発継続中 · 現行公開版 **v0.37.0** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
199
+ **状態:** 開発継続中 · 現行公開版 **v0.37.2** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
199
200
 
200
201
  ### 更新と巻き戻し
201
202
 
@@ -475,6 +476,7 @@ aiterm は同じ核心の洞察——端末を出会いの場にする——を
475
476
 
476
477
  `agent_launch({ harness, cwd, trust_project: true })`はpromptなしでも既知のworkspace・project hooks・MCP初期同意を
477
478
  進め、入力受付とharness生存を確認して`startup.status="ready"`を返す。指定なしのpromptなし起動は`not_checked`。
479
+ WindowsのCodexもhook確認を認識し、npm shim経由の起動を一つのharnessとして識別する。
478
480
  初手の`initial_prompt.status`は`not_requested`/`not_sent`/`submitted_unconfirmed`/`started`を区別する。
479
481
  未送信・未確認の失敗もsession付きstructuredContentを保持する。未確認のpromptを再送せず、返ったcursorで観測する。
480
482
 
package/README.md CHANGED
@@ -57,7 +57,7 @@ aiterm-setup --json --codex-steer enable
57
57
 
58
58
  公式Codex Desktopに同梱されたApp Serverを使い、実行中の親には同じターンへ回答を送り、
59
59
  終了後に届いた回答でも同じタスクを自動再開します。App Serverの改造・再ビルド・別配布は不要です。
60
- Steerの初回設定後は`restart_required`(終了コード3)を返します。Codexを完全終了して再起動し、
60
+ 新しいlauncherを設定した場合は`restart_required`(終了コード3)を返します。Codexを完全終了して再起動し、
61
61
  `aiterm-setup --codex-steer status`で`ready`を確認してください。接続できない時にキューへ自動退避しません。
62
62
 
63
63
  選択と復元情報は`~/.config/aiterm-mcp/codex-relay/`へ保存します。macOSはユーザーLaunchAgent、
@@ -66,8 +66,9 @@ Windowsはユーザー環境変数`CODEX_CLI_PATH`で起動設定を維持しま
66
66
  SteerはmacOS・Windowsの公式Codex Desktopと同梱CLI 0.154以上に対応します。Steer有効時の宛先は中継で起動したDesktopに限ります。
67
67
  Windowsでは公式Desktopが展開した実行ファイルを照合し、認証付きのローカル接続へ中継します。
68
68
  Aitermのpackageがlauncher・中継・設定・親への配送を含み、gpt-connectorやdotagentsの導入は不要です。
69
- 既存の互換launcherがある場合は、稼働中のDesktop接続を確認した時だけ共有し、その起動設定を変更しません。
70
- 互換性を確認できない設定は理由付きで停止します。WindowsのDesktop更新後はsetupを再実行してください。
69
+ 両OSで公式CodexをDesktopの直接の子として保ち、中継は公式Codexの子として動きます。
70
+ 有効化時は元の起動設定を保存してAitermのlauncherを設定し、解除時に元へ戻します。選択後に他から変更された値は上書きしません。
71
+ WindowsのDesktop更新後はsetupを再実行してください。
71
72
  LinuxのSteer付き導入は理由付き`unsupported`を返します。Aiterm単品は従来どおり利用できます。
72
73
 
73
74
  No clone or build is required. Each client launches the published package with:
@@ -209,7 +210,7 @@ collection is off by default and performs no network I/O. It ships via
209
210
  tag-triggered CI with npm provenance (OIDC Trusted Publishing); the GitHub
210
211
  Release re-registers the Official MCP Registry entry.
211
212
 
212
- **Status:** actively maintained · current public release **v0.37.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).
213
+ **Status:** actively maintained · current public release **v0.37.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).
213
214
 
214
215
  ### Update and rollback
215
216
 
@@ -508,6 +509,7 @@ displayed token count or null. Callers do not need raw argv or pane-text parsing
508
509
  consent even without a prompt, then verifies input readiness and harness liveness before returning `startup.status="ready"`.
509
510
  A prompt-free launch without this option retains `startup.status="not_checked"`. `initial_prompt.status` distinguishes
510
511
  `not_requested`, `not_sent`, `submitted_unconfirmed`, and `started`. Failure responses retain structured session information.
512
+ WindowsのCodexもhook確認を認識し、npm shim経由の起動を一つのharnessとして識別する。
511
513
  Do not resend an unconfirmed prompt; observe or wait using its returned cursor.
512
514
 
513
515
  For a live Codex approval, inspect with `agent_approval({ action: "inspect", session_id })`, review `prompt` and `choices`,
@@ -0,0 +1,36 @@
1
+ // transportを切り替える条件と引数の扱いは全OSで共通にする。
2
+ export function codexServerArguments(args) {
3
+ let server = false;
4
+ const result = [];
5
+ for (let i = 0; i < args.length; i++) {
6
+ const arg = args[i];
7
+ if (["-c", "--config", "--enable", "--disable"].includes(arg)) {
8
+ result.push(arg, args[++i]);
9
+ continue;
10
+ }
11
+ if (/^(?:--config=|--enable=|--disable=|-c.)/.test(arg)) {
12
+ result.push(arg);
13
+ continue;
14
+ }
15
+ if (!server) {
16
+ if (arg !== "app-server")
17
+ return null;
18
+ server = true;
19
+ result.push(arg);
20
+ continue;
21
+ }
22
+ if (["proxy", "daemon", "start", "stop", "status", "generate-ts", "generate-json-schema", "--help", "-h", "--version", "-V"].includes(arg))
23
+ return null;
24
+ if (arg === "--stdio" || arg === "--listen=stdio://")
25
+ continue;
26
+ if (arg === "--listen") {
27
+ if (args[++i] !== "stdio://")
28
+ throw new Error("stdio以外の接続指定は変更できません");
29
+ continue;
30
+ }
31
+ if (arg.startsWith("--listen=") || arg.startsWith("--ws-"))
32
+ throw new Error("既存の接続・認証指定は変更できません");
33
+ result.push(arg);
34
+ }
35
+ return server ? result : null;
36
+ }
@@ -1,63 +1,26 @@
1
- // POSIXのexecで公式CLIのPIDとDesktopからの親子関係を維持する。
2
- const quote = (text) => `'${text.replace(/'/g, `'"'"'`)}'`;
1
+ // POSIXのexecで公式CLIとDesktopの直接の親子関係を維持する。
2
+ import { codexServerArguments } from "./codex-relay-arguments.js";
3
+ import { prepareRelayDirectory } from "./codex-relay-config.js";
4
+ const quote = (text) => "'" + text.replace(/'/g, "'\"'\"'") + "'";
3
5
  export function codexRelayLauncher(options) {
4
- return `#!/bin/sh
5
- binary=${quote(options.binary)}
6
- node=${quote(options.node)}
7
- relay=${quote(options.relay)}
8
- root=${quote(options.socket_root)}
9
- mode=root
10
- value=no
11
- for arg do
12
- if [ "$value" = yes ]; then value=no; continue; fi
13
- case "$arg" in
14
- -c|--config|--enable|--disable|--listen) value=yes; continue ;;
15
- esac
16
- if [ "$mode" = root ]; then
17
- case "$arg" in
18
- --config=*|--enable=*|--disable=*|-c?*) ;;
19
- app-server) mode=server ;;
20
- *) mode=other; break ;;
21
- esac
22
- else
23
- case "$arg" in
24
- proxy|start|stop|status|generate-ts|generate-json-schema|--help|-h|--version|-V) mode=other; break ;;
25
- esac
26
- fi
27
- done
28
- if [ "$mode" != server ]; then exec "$binary" "$@"; fi
29
-
30
- # 引数を一巡させ、stdio指定だけを公式Unix受付へ変更する。
31
- remaining=$#
32
- value=no
33
- while [ "$remaining" -gt 0 ]; do
34
- arg=$1
35
- shift
36
- remaining=$((remaining - 1))
37
- if [ "$value" = yes ]; then
38
- set -- "$@" "$arg"
39
- value=no
40
- continue
41
- fi
42
- case "$arg" in
43
- -c|--config|--enable|--disable) value=yes; set -- "$@" "$arg" ;;
44
- --stdio) ;;
45
- --listen)
46
- if [ "$remaining" -eq 0 ] || [ "$1" != stdio:// ]; then
47
- echo 'aiterm-relay: stdio以外の接続指定は変更できません' >&2
48
- exit 2
49
- fi
50
- shift
51
- remaining=$((remaining - 1)) ;;
52
- --listen=stdio://) ;;
53
- --listen=*) echo 'aiterm-relay: stdio以外の接続指定は変更できません' >&2; exit 2 ;;
54
- *) set -- "$@" "$arg" ;;
55
- esac
56
- done
57
- "$node" "$relay" --prepare "$root" || exit $?
58
- socket="$root/$$.sock"
59
- exec 3<&0
60
- "$node" "$relay" "$socket" "$$" <&3 3<&- &
61
- exec "$binary" "$@" --listen "unix://$socket" </dev/null >/dev/null 3<&-
62
- `;
6
+ return [
7
+ "#!/bin/sh",
8
+ "command=$(" + [options.node, options.relay, "--prepare", options.socket_root, options.binary, options.node, options.relay].map(quote).join(" ") + ' "$$" "$@") || exit $?',
9
+ 'exec /bin/sh -c "$command"',
10
+ "",
11
+ ].join("\n");
12
+ }
13
+ export function preparePosixRelayLaunch(root, binary, node, relay, pid, args) {
14
+ const serverArgs = codexServerArguments(args);
15
+ if (!serverArgs)
16
+ return ["exec", binary, ...args].map((arg, i) => i === 0 ? arg : quote(arg)).join(" ");
17
+ if (!/^[1-9][0-9]*$/.test(pid) || Buffer.byteLength(root + "/9999999999.sock") >= 104)
18
+ throw new Error("socketのpathまたは親processの指定が不正です");
19
+ prepareRelayDirectory(root);
20
+ const socket = root + "/" + pid + ".sock";
21
+ return [
22
+ "exec 3<&0",
23
+ [node, relay, socket, pid].map(quote).join(" ") + " <&3 3<&- &",
24
+ "exec " + [binary, ...serverArgs, "--listen", "unix://" + socket].map(quote).join(" ") + " </dev/null >/dev/null 3<&-",
25
+ ].join("\n");
63
26
  }
@@ -0,0 +1,44 @@
1
+ import { readRelayConfig } from "./codex-relay-config.js";
2
+ import { SetupError } from "./setup-platform.js";
3
+ // 設定・競合・再実行・復元・実効確認の順序は全OSで同じにする。
4
+ export async function configureRelay(action, runtime) {
5
+ const previous = readRelayConfig(runtime.directory);
6
+ if (action === "status") {
7
+ if (!previous?.enabled)
8
+ return { status: "disabled" };
9
+ if (runtime.getGui("CODEX_CLI_PATH") !== previous.launcher)
10
+ return { status: "failed", reason_code: "codex_steer_configuration_changed" };
11
+ return await runtime.live(previous) ? { status: "ready" } : { status: "restart_required", reason_code: "codex_restart_required" };
12
+ }
13
+ if (action === "disable") {
14
+ if (!previous?.enabled)
15
+ return { status: "disabled" };
16
+ if (![previous.launcher, previous.previous_cli_path].includes(runtime.getGui("CODEX_CLI_PATH")))
17
+ throw new SetupError("codex_steer_configuration_changed", "起動設定が他から変更されています。所有外の値は上書きしません");
18
+ runtime.unpersist(previous.launcher);
19
+ runtime.setGui("CODEX_CLI_PATH", previous.previous_cli_path);
20
+ if (runtime.getGui("CODEX_CLI_PATH") !== previous.previous_cli_path)
21
+ throw new SetupError("codex_steer_readback_failed", "元の起動設定を確認できません");
22
+ runtime.save(runtime.directory, { ...previous, enabled: false });
23
+ return { status: "restart_required", reason_code: "codex_restart_required" };
24
+ }
25
+ for (const key of ["CODEX_APP_SERVER_WS_URL", "CODEX_APP_SERVER_USE_LOCAL_DAEMON", "CODEX_APP_SERVER_FORCE_CLI"]) {
26
+ if (runtime.getGui(key))
27
+ throw new SetupError("codex_steer_configuration_conflict", `${key}が設定されています。既存の接続設定は上書きしません`);
28
+ }
29
+ const binary = runtime.findBinary();
30
+ const current = runtime.getGui("CODEX_CLI_PATH");
31
+ if (previous?.enabled && ![previous.launcher, previous.previous_cli_path].includes(current))
32
+ throw new SetupError("codex_steer_configuration_changed", "起動設定が他から変更されています");
33
+ runtime.prepare(runtime.directory);
34
+ const launcher = await runtime.build(binary);
35
+ const config = { schema: "aiterm.codex-relay.v1", enabled: true, binary, node: runtime.node,
36
+ launcher, socket_root: runtime.socket_root, previous_cli_path: previous?.enabled ? previous.previous_cli_path : current };
37
+ // 起動設定の変更前に復元値を保存する。読戻し不一致を成功扱いしない。
38
+ runtime.save(runtime.directory, config);
39
+ runtime.persist(launcher);
40
+ runtime.setGui("CODEX_CLI_PATH", launcher);
41
+ if (runtime.getGui("CODEX_CLI_PATH") !== launcher)
42
+ throw new SetupError("codex_steer_readback_failed", "Steerの起動設定を確認できません");
43
+ return await runtime.live(config) ? { status: "ready" } : { status: "restart_required", reason_code: "codex_restart_required" };
44
+ }
@@ -0,0 +1,81 @@
1
+ // Desktopと公式App Serverを結ぶ共通のJSONL中継。OS adapterは接続先とprocess終了だけを供給する。
2
+ import { createInterface } from "node:readline";
3
+ import { once } from "node:events";
4
+ import { setTimeout as delay } from "node:timers/promises";
5
+ export async function relayCodexStdio(adapter) {
6
+ let socket;
7
+ let ending = false;
8
+ let failure;
9
+ let stopping;
10
+ const stop = () => stopping ??= adapter.stop();
11
+ const input = createInterface({ input: process.stdin, crlfDelay: Infinity });
12
+ // 接続準備中に届いたinitializeとEOFも同じ順序で扱う。
13
+ const lines = input[Symbol.asyncIterator]();
14
+ const end = () => { ending = true; void stop().catch(error => { failure = error; }); socket?.terminate(); input.close(); process.stdin.destroy(); };
15
+ const outputFailed = (error) => { failure = error; end(); };
16
+ process.stdout.once("error", outputFailed);
17
+ const drain = () => socket?.resume();
18
+ process.stdout.on("drain", drain);
19
+ try {
20
+ const deadline = Date.now() + 15_000;
21
+ while (!ending) {
22
+ if (!adapter.alive())
23
+ throw new Error("公式App Serverが起動中に終了しました");
24
+ socket = adapter.socket();
25
+ const candidate = socket;
26
+ let opened = false;
27
+ candidate.once("open", () => { opened = true; });
28
+ // upgradeと最初のframeが同じ受信単位でも、openのawaitより先に受け付ける。
29
+ candidate.on("message", (data, binary) => {
30
+ if (binary) {
31
+ failure = new Error("公式App Serverから予期しないバイナリ応答を受信しました");
32
+ end();
33
+ return;
34
+ }
35
+ if (!process.stdout.write(data.toString() + "\n"))
36
+ candidate.pause();
37
+ });
38
+ candidate.on("error", error => { if (opened) {
39
+ failure = error;
40
+ end();
41
+ } });
42
+ candidate.on("close", () => {
43
+ if (opened && !ending) {
44
+ failure = new Error("公式App Serverとの接続が終了しました");
45
+ end();
46
+ }
47
+ });
48
+ try {
49
+ await once(socket, "open");
50
+ break;
51
+ }
52
+ catch (error) {
53
+ socket.on("error", () => { });
54
+ socket.terminate();
55
+ if (ending)
56
+ break;
57
+ // 公式受付の作成だけを待つ。送信済みRPCは再送しない。
58
+ if (!["ENOENT", "ECONNREFUSED"].includes(error.code ?? "") || Date.now() >= deadline)
59
+ throw error;
60
+ await delay(25);
61
+ }
62
+ }
63
+ if (ending) {
64
+ if (failure)
65
+ throw failure;
66
+ return;
67
+ }
68
+ const connected = socket;
69
+ adapter.connected?.();
70
+ for await (const line of lines)
71
+ await new Promise((resolve, reject) => connected.send(line, error => error ? reject(error) : resolve()));
72
+ if (failure)
73
+ throw failure;
74
+ }
75
+ finally {
76
+ end();
77
+ await stop();
78
+ process.stdout.off("error", outputFailed);
79
+ process.stdout.off("drain", drain);
80
+ }
81
+ }
@@ -1,18 +1,18 @@
1
1
  #!/usr/bin/env node
2
- // DesktopのJSONLと公式App ServerのUnix WebSocketの中継。RPCの内容は変更しない。
2
+ // Unix socketとPOSIX signalのadapter。中継するJSONLと終了契約はWindowsと共通。
3
3
  import { createConnection } from "node:net";
4
- import { createInterface } from "node:readline";
5
4
  import { setTimeout as delay } from "node:timers/promises";
6
- import { once } from "node:events";
7
5
  import { fileURLToPath } from "node:url";
8
6
  import WebSocket from "ws";
9
- import { prepareRelayDirectory } from "./codex-relay-config.js";
7
+ import { preparePosixRelayLaunch } from "./codex-relay-launcher.js";
8
+ import { relayCodexStdio } from "./codex-relay-stdio.js";
10
9
  export async function runCodexStdioRelay(socketPath, serverPid) {
11
- let socket;
12
- let ending = false;
13
- let stopping;
14
- function stopServer() {
15
- return stopping ??= (async () => {
10
+ await relayCodexStdio({
11
+ alive: () => process.ppid === serverPid,
12
+ socket: () => new WebSocket("ws://localhost/rpc", {
13
+ createConnection: () => createConnection(socketPath), handshakeTimeout: 10_000, perMessageDeflate: false,
14
+ }),
15
+ stop: async () => {
16
16
  // 公式Unix受付は1回目でturn完了待ち、2回目で終了する。
17
17
  for (let i = 0; i < 2; i++) {
18
18
  if (process.ppid !== serverPid)
@@ -28,93 +28,26 @@ export async function runCodexStdioRelay(socketPath, serverPid) {
28
28
  if (i === 0)
29
29
  await delay(100);
30
30
  }
31
- })();
32
- }
33
- async function connectDuringStartup() {
34
- const deadline = Date.now() + 15_000;
35
- while (true) {
36
- if (process.ppid !== serverPid)
37
- throw new Error("公式App Serverが起動中に終了しました");
38
- const candidate = new WebSocket("ws://localhost/rpc", {
39
- createConnection: () => createConnection(socketPath), handshakeTimeout: 10_000, perMessageDeflate: false,
40
- });
41
- try {
42
- await once(candidate, "open");
43
- return candidate;
44
- }
45
- catch (error) {
46
- candidate.on("error", () => { });
47
- candidate.terminate();
48
- // exec直後のsocket作成だけを待つ。送信済みRPCは再送しない。
49
- if (!["ENOENT", "ECONNREFUSED"].includes(error.code ?? "") || Date.now() >= deadline)
50
- throw error;
51
- await delay(25);
52
- }
53
- }
54
- }
55
- try {
56
- socket = await connectDuringStartup();
57
- const connected = socket;
58
- connected.on("message", (data, binary) => {
59
- if (binary) {
60
- process.stderr.write("aiterm-relay: 予期しないバイナリ応答\n");
61
- process.exitCode = 1;
62
- void stopServer();
63
- connected.terminate();
64
- return;
65
- }
66
- if (!process.stdout.write(`${data.toString()}\n`))
67
- connected.pause();
68
- });
69
- process.stdout.on("drain", () => connected.resume());
70
- connected.on("error", () => {
71
- process.stderr.write("aiterm-relay: 通信に失敗しました\n");
72
- process.exitCode = 1;
73
- void stopServer();
74
- });
75
- connected.on("close", () => {
76
- if (!ending) {
77
- process.stderr.write("aiterm-relay: 公式App Serverとの接続が終了しました\n");
78
- process.exitCode = 1;
79
- }
80
- process.stdin.destroy();
81
- });
82
- process.stdout.on("error", () => { ending = true; void stopServer(); connected.terminate(); process.stdin.destroy(); });
83
- const lines = createInterface({ input: process.stdin, crlfDelay: Infinity });
84
- for await (const line of lines) {
85
- await new Promise((resolve, reject) => connected.send(line, error => error ? reject(error) : resolve()));
86
- }
87
- ending = true;
88
- await stopServer();
89
- connected.terminate();
90
- }
91
- catch (error) {
92
- process.stderr.write(`aiterm-relay: 中継失敗: ${error instanceof Error ? error.message : String(error)}\n`);
93
- process.exitCode = 1;
94
- await stopServer();
95
- socket?.terminate();
96
- process.stdin.destroy();
97
- }
31
+ },
32
+ });
98
33
  }
99
34
  if (process.argv[1] === fileURLToPath(import.meta.url)) {
100
- const [socket, pid] = process.argv.slice(2);
101
- if (socket === "--prepare") {
102
- try {
35
+ const [socket, pid, ...args] = process.argv.slice(2);
36
+ try {
37
+ if (socket === "--prepare") {
103
38
  if (process.platform === "win32")
104
- throw new Error("WindowsのAF_UNIX中継は未対応です");
105
- if (!pid || Buffer.byteLength(`${pid}/9999999999.sock`) >= 104)
106
- throw new Error("socketのpathが長すぎます");
107
- prepareRelayDirectory(pid);
108
- }
109
- catch (error) {
110
- process.stderr.write(`aiterm-relay: ${error instanceof Error ? error.message : String(error)}\n`);
111
- process.exitCode = 2;
39
+ throw new Error("POSIX launcherはこのOSで利用できません");
40
+ if (!pid || args.length < 4)
41
+ throw new Error("中継の起動指定がありません");
42
+ process.stdout.write(preparePosixRelayLaunch(pid, args[0], args[1], args[2], args[3], args.slice(4)));
112
43
  }
44
+ else if (!socket || !/^[1-9][0-9]*$/.test(pid ?? "") || Number(pid) !== process.ppid)
45
+ throw new Error("親processの指定が不正です");
46
+ else
47
+ await runCodexStdioRelay(socket, Number(pid));
113
48
  }
114
- else if (!socket || !/^\d+$/.test(pid ?? "") || Number(pid) <= 1 || Number(pid) !== process.ppid) {
115
- process.stderr.write("aiterm-relay: 親processの指定が不正です\n");
116
- process.exitCode = 2;
49
+ catch (error) {
50
+ process.stderr.write("aiterm-relay: " + (error instanceof Error ? error.message : String(error)) + "\n");
51
+ process.exitCode = 1;
117
52
  }
118
- else
119
- await runCodexStdioRelay(socket, Number(pid));
120
53
  }
package/dist/core.js CHANGED
@@ -1051,7 +1051,7 @@ function decodeActivityCursor(cursor, name) {
1051
1051
  throw new AitermError("活動観測cursorが対象sessionの形式と一致しません", 2);
1052
1052
  return value;
1053
1053
  }
1054
- function selectHarnessProcesses(meta, rows, subtree) {
1054
+ export function selectHarnessProcesses(meta, rows, subtree) {
1055
1055
  const matches = (row) => {
1056
1056
  const first = /^(?:"([^"]+)"|(\S+))/.exec(row.command);
1057
1057
  const executable = path.posix.basename((first?.[1] ?? first?.[2] ?? "").replace(/\\/g, "/"))
@@ -1065,7 +1065,20 @@ function selectHarnessProcesses(meta, rows, subtree) {
1065
1065
  // WindowsのMSYS execでnative親子関係が切れる場合も、launch固有引数で相関する。
1066
1066
  const correlated = rows.filter(row => matches(row) && row.command.includes(meta.launch_id));
1067
1067
  const candidates = correlated.length ? correlated : subtree.filter(matches);
1068
- const roots = candidates.filter(row => !candidates.some(parent => row.parent_pid === parent.pid));
1068
+ const byPid = new Map(rows.map(row => [row.pid, row]));
1069
+ const candidatePids = new Set(candidates.map(row => row.pid));
1070
+ // npm shimとnative本体の間にNodeなどの非候補processがいても、同じ起動の祖先を辿る。
1071
+ const roots = candidates.filter(row => {
1072
+ const seen = new Set();
1073
+ let parent = byPid.get(row.parent_pid);
1074
+ while (parent && !seen.has(parent.pid)) {
1075
+ if (candidatePids.has(parent.pid))
1076
+ return false;
1077
+ seen.add(parent.pid);
1078
+ parent = byPid.get(parent.parent_pid);
1079
+ }
1080
+ return true;
1081
+ });
1069
1082
  return roots;
1070
1083
  }
1071
1084
  export function observeSession(name, cursor) {
@@ -386,7 +386,7 @@ export function codexTuiReady(screen) {
386
386
  return codexFrontend && /(^|\n)[ \t]*[›>](?![ \t]*\d+\.)/.test(screen);
387
387
  }
388
388
  function currentCodexDialog(screen) {
389
- const footers = [...screen.matchAll(/Press enter to confirm or esc to cancel|enter to submit\s*\|\s*esc to cancel/gi)];
389
+ const footers = [...screen.matchAll(/Press enter to confirm or esc to (?:cancel|go back)|enter to submit\s*\|\s*esc to cancel/gi)];
390
390
  // 過去の完了footerより前の質問・選択番号を、現在のdialogへ持ち込まない。
391
391
  const previousFooter = footers.at(-2);
392
392
  const current = previousFooter ? screen.slice(previousFooter.index + previousFooter[0].length) : screen;
@@ -400,7 +400,7 @@ export function codexPaneObservation(screen) {
400
400
  const tail = screen.split("\n").slice(-24).join("\n");
401
401
  // 現在のmodal footerがある時だけ、折返しで上へ出た質問を画面全体から探す。
402
402
  const lastComposer = [...tail.matchAll(/(?:^|\n)[ \t]*[›>](?![ \t]*\d+\.)/g)].at(-1)?.index ?? -1;
403
- const lastDialog = [...tail.matchAll(/Press enter to confirm or esc to cancel|enter to submit\s*\|\s*esc to cancel|Would you like to run the following command\?|Allow the [^\n]+ MCP server to run tool|Hooks need review|Do you trust the contents of this directory|Update available!/gi)].at(-1)?.index ?? -1;
403
+ const lastDialog = [...tail.matchAll(/Press enter to confirm or esc to (?:cancel|go back)|enter to submit\s*\|\s*esc to cancel|Would you like to run the following command\?|Allow the [^\n]+ MCP server to run tool|Hooks need review|Do you trust the contents of this directory|Update available!/gi)].at(-1)?.index ?? -1;
404
404
  const modal = lastDialog > lastComposer;
405
405
  if (!modal) {
406
406
  if (/esc to interrupt/i.test(tail))