aiterm-mcp 0.37.1 → 0.37.3

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,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.37.3] - 2026-09-13
11
+
12
+ ### 修正
13
+
14
+ - WindowsのMSIX仮想AppDataにAitermがある場合、Codex Desktopの中継起動が`MODULE_NOT_FOUND`で失敗する問題を修正した。起動済みの中継が自身の実体パスを後続processへ渡す。
15
+
16
+ ## [0.37.2] - 2026-09-13
17
+
18
+ ### 修正
19
+
20
+ - Codex Steerの引数、通信中継、設定の有効化・復元をmacOSとWindowsで共通化した。OS差は起動API・接続・権限・設定保存に限定する。
21
+ - Windowsで公式Codexの直接の親がDesktopではなくなり、他アプリの公式CLI検出を妨げていた構成を修正した。公式CodexをDesktopの直接の子、中継を公式Codexの子として起動する。
22
+ - Windowsだけの既存launcher共有を廃止し、Macと同じくAitermのlauncherへ設定して解除時に元へ戻す。旧共有設定も次のenableで移行する。
23
+
10
24
  ## [0.37.1] - 2026-09-13
11
25
 
12
26
  ### 修正
@@ -1671,7 +1685,9 @@ prototype (preserved under `prototype/python/` as the porting source and referen
1671
1685
  `ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
1672
1686
  provenance.
1673
1687
 
1674
- [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.37.1...HEAD
1688
+ [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.37.3...HEAD
1689
+ [0.37.3]: https://github.com/kitepon/aiterm-mcp/compare/v0.37.2...v0.37.3
1690
+ [0.37.2]: https://github.com/kitepon/aiterm-mcp/compare/v0.37.1...v0.37.2
1675
1691
  [0.37.1]: https://github.com/kitepon/aiterm-mcp/compare/v0.37.0...v0.37.1
1676
1692
  [0.37.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.36.0...v0.37.0
1677
1693
  [0.36.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.35.1...v0.36.0
package/README.ja.md CHANGED
@@ -65,9 +65,11 @@ Windowsはユーザー環境変数`CODEX_CLI_PATH`で起動設定を維持しま
65
65
  解除・旧版への巻き戻し前は`aiterm-setup --codex-steer disable`を実行してCodexを再起動してください。
66
66
  SteerはmacOS・Windowsの公式Codex Desktopと同梱CLI 0.154以上に対応します。Steer有効時の宛先は中継で起動したDesktopに限ります。
67
67
  Windowsでは公式Desktopが展開した実行ファイルを照合し、認証付きのローカル接続へ中継します。
68
+ Windowsの仮想AppDataからの起動にも対応し、中継へ実際の保存先を引き継ぎます。
68
69
  Aitermのpackageがlauncher・中継・設定・親への配送を含み、gpt-connectorやdotagentsの導入は不要です。
69
- 既存の互換launcherがある場合は、稼働中のDesktop接続を確認した時だけ共有し、その起動設定を変更しません。
70
- 互換性を確認できない設定は理由付きで停止します。WindowsのDesktop更新後はsetupを再実行してください。
70
+ 両OSで公式CodexをDesktopの直接の子として保ち、中継は公式Codexの子として動きます。
71
+ 有効化時は元の起動設定を保存してAitermのlauncherを設定し、解除時に元へ戻します。選択後に他から変更された値は上書きしません。
72
+ WindowsのDesktop更新後はsetupを再実行してください。
71
73
  LinuxのSteer付き導入は理由付き`unsupported`を返します。Aiterm単品は従来どおり利用できます。
72
74
 
73
75
  cloneもビルドも不要。どのクライアントでも公開パッケージを次のコマンドで起動する:
@@ -195,7 +197,7 @@ runtime-error store は canonical dotagents config の `collection.enabled: true
195
197
  場合だけ収集し、既定OFF、network送信は行いません。tag起点CIのnpm provenance(OIDC Trusted
196
198
  Publishing)で公開し、GitHub Release が Official MCP Registry を再登録します。
197
199
 
198
- **状態:** 開発継続中 · 現行公開版 **v0.37.1** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
200
+ **状態:** 開発継続中 · 現行公開版 **v0.37.3** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
199
201
 
200
202
  ### 更新と巻き戻し
201
203
 
package/README.md CHANGED
@@ -65,9 +65,11 @@ Windowsはユーザー環境変数`CODEX_CLI_PATH`で起動設定を維持しま
65
65
  解除・旧版への巻き戻し前は`aiterm-setup --codex-steer disable`を実行してCodexを再起動してください。
66
66
  SteerはmacOS・Windowsの公式Codex Desktopと同梱CLI 0.154以上に対応します。Steer有効時の宛先は中継で起動したDesktopに限ります。
67
67
  Windowsでは公式Desktopが展開した実行ファイルを照合し、認証付きのローカル接続へ中継します。
68
+ Windowsの仮想AppDataからの起動にも対応し、中継へ実際の保存先を引き継ぎます。
68
69
  Aitermのpackageがlauncher・中継・設定・親への配送を含み、gpt-connectorやdotagentsの導入は不要です。
69
- 既存の互換launcherがある場合は、稼働中のDesktop接続を確認した時だけ共有し、その起動設定を変更しません。
70
- 互換性を確認できない設定は理由付きで停止します。WindowsのDesktop更新後はsetupを再実行してください。
70
+ 両OSで公式CodexをDesktopの直接の子として保ち、中継は公式Codexの子として動きます。
71
+ 有効化時は元の起動設定を保存してAitermのlauncherを設定し、解除時に元へ戻します。選択後に他から変更された値は上書きしません。
72
+ WindowsのDesktop更新後はsetupを再実行してください。
71
73
  LinuxのSteer付き導入は理由付き`unsupported`を返します。Aiterm単品は従来どおり利用できます。
72
74
 
73
75
  No clone or build is required. Each client launches the published package with:
@@ -209,7 +211,7 @@ collection is off by default and performs no network I/O. It ships via
209
211
  tag-triggered CI with npm provenance (OIDC Trusted Publishing); the GitHub
210
212
  Release re-registers the Official MCP Registry entry.
211
213
 
212
- **Status:** actively maintained · current public release **v0.37.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).
214
+ **Status:** actively maintained · current public release **v0.37.3** · 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
215
 
214
216
  ### Update and rollback
215
217
 
@@ -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
  }
@@ -7,12 +7,13 @@ import { fileURLToPath } from "node:url";
7
7
  import { randomUUID } from "node:crypto";
8
8
  import { SetupError } from "./setup-platform.js";
9
9
  import { CodexDeliveryError } from "./codex-delivery-error.js";
10
- import { readRelayConfig, relayConfigDirectory, prepareRelayDirectory } from "./codex-relay-config.js";
10
+ import { relayConfigDirectory, prepareRelayDirectory } from "./codex-relay-config.js";
11
11
  import { codexRelayLauncher } from "./codex-relay-launcher.js";
12
12
  import { withCodexRelay } from "./codex-relay-client.js";
13
13
  import { readRuntimeProcesses } from "./process-runtime.js";
14
14
  import { installRelayLogin, removeRelayLogin } from "./codex-relay-login.js";
15
- import { configureWindowsCodexSteer } from "./windows-codex-setup.js";
15
+ import { windowsCodexRuntime } from "./windows-codex-setup.js";
16
+ import { configureRelay } from "./codex-relay-setup.js";
16
17
  function command(executable, args) {
17
18
  const result = spawnSync(executable, args, { encoding: "utf8", timeout: 15_000 });
18
19
  if (result.error || result.status !== 0)
@@ -123,65 +124,31 @@ function writeConfig(directory, config) {
123
124
  }
124
125
  export async function configureCodexSteer(action = "status", overrides = {}) {
125
126
  if ((overrides.platform ?? process.platform) === "win32")
126
- return configureWindowsCodexSteer(action, overrides);
127
+ return configureRelay(action, windowsCodexRuntime(overrides));
127
128
  const runtime = {
128
129
  platform: process.platform, directory: relayConfigDirectory(), socket_root: `/tmp/aiterm-codex-${process.getuid?.() ?? 0}`,
129
130
  node: process.execPath, relay: fileURLToPath(new URL("./codex-stdio-relay.js", import.meta.url)),
130
131
  findBinary: findDesktopBinary, getGui,
131
132
  setGui: (key, value) => { command("/bin/launchctl", value === null ? ["unsetenv", key] : ["setenv", key, value]); },
132
133
  persist: installRelayLogin, unpersist: removeRelayLogin,
133
- verify: verifyRelayLauncher, live: liveRelay, ...overrides,
134
+ verify: verifyRelayLauncher, live: liveRelay, prepare: prepareRelayDirectory, save: writeConfig,
135
+ build: async (binary) => {
136
+ const launcher = path.join(runtime.directory, "codex");
137
+ const candidate = path.join(runtime.directory, `codex-${randomUUID()}`);
138
+ fs.writeFileSync(candidate, codexRelayLauncher({ binary, node: runtime.node, relay: runtime.relay, socket_root: runtime.socket_root }), { mode: 0o700, flag: "wx" });
139
+ try {
140
+ await runtime.verify(candidate);
141
+ fs.renameSync(candidate, launcher);
142
+ }
143
+ finally {
144
+ if (fs.existsSync(candidate))
145
+ fs.unlinkSync(candidate);
146
+ }
147
+ return launcher;
148
+ }, ...overrides,
134
149
  };
135
150
  if (runtime.platform !== "darwin") {
136
151
  return action === "enable" ? { status: "unsupported", reason_code: "codex_steer_platform_unsupported" } : { status: "disabled" };
137
152
  }
138
- const previous = readRelayConfig(runtime.directory);
139
- if (action === "status") {
140
- if (!previous?.enabled)
141
- return { status: "disabled" };
142
- if (runtime.getGui("CODEX_CLI_PATH") !== previous.launcher)
143
- return { status: "failed", reason_code: "codex_steer_configuration_changed" };
144
- return await runtime.live(previous) ? { status: "ready" } : { status: "restart_required", reason_code: "codex_restart_required" };
145
- }
146
- if (action === "disable") {
147
- if (!previous?.enabled)
148
- return { status: "disabled" };
149
- if (![previous.launcher, previous.previous_cli_path].includes(runtime.getGui("CODEX_CLI_PATH")))
150
- throw new SetupError("codex_steer_configuration_changed", "起動設定が他から変更されています。所有外の値は上書きしません");
151
- runtime.unpersist(previous.launcher);
152
- runtime.setGui("CODEX_CLI_PATH", previous.previous_cli_path);
153
- if (runtime.getGui("CODEX_CLI_PATH") !== previous.previous_cli_path)
154
- throw new SetupError("codex_steer_readback_failed", "元の起動設定を確認できません");
155
- writeConfig(runtime.directory, { ...previous, enabled: false });
156
- return { status: "restart_required", reason_code: "codex_restart_required" };
157
- }
158
- for (const key of ["CODEX_APP_SERVER_WS_URL", "CODEX_APP_SERVER_USE_LOCAL_DAEMON", "CODEX_APP_SERVER_FORCE_CLI"]) {
159
- if (runtime.getGui(key))
160
- throw new SetupError("codex_steer_configuration_conflict", `${key}が設定されています。既存の接続設定は上書きしません`);
161
- }
162
- const binary = runtime.findBinary();
163
- const current = runtime.getGui("CODEX_CLI_PATH");
164
- if (previous?.enabled && ![previous.launcher, previous.previous_cli_path].includes(current))
165
- throw new SetupError("codex_steer_configuration_changed", "起動設定が他から変更されています");
166
- prepareRelayDirectory(runtime.directory);
167
- const launcher = path.join(runtime.directory, "codex");
168
- const config = { schema: "aiterm.codex-relay.v1", enabled: true, binary, node: runtime.node,
169
- launcher, socket_root: runtime.socket_root, previous_cli_path: previous?.enabled ? previous.previous_cli_path : current };
170
- const candidate = path.join(runtime.directory, `codex-${randomUUID()}`);
171
- fs.writeFileSync(candidate, codexRelayLauncher({ ...config, relay: runtime.relay }), { mode: 0o700, flag: "wx" });
172
- try {
173
- await runtime.verify(candidate);
174
- fs.renameSync(candidate, launcher);
175
- }
176
- finally {
177
- if (fs.existsSync(candidate))
178
- fs.unlinkSync(candidate);
179
- }
180
- // 起動設定の変更前に復元値を保存する。読戻し不一致を成功扱いしない。
181
- writeConfig(runtime.directory, config);
182
- runtime.persist(launcher);
183
- runtime.setGui("CODEX_CLI_PATH", launcher);
184
- if (runtime.getGui("CODEX_CLI_PATH") !== launcher)
185
- throw new SetupError("codex_steer_readback_failed", "Steerの起動設定を確認できません");
186
- return await runtime.live(config) ? { status: "ready" } : { status: "restart_required", reason_code: "codex_restart_required" };
153
+ return configureRelay(action, runtime);
187
154
  }
@@ -76,33 +76,25 @@ export function windowsSocketConnection(file) {
76
76
  invalid("Steer認証情報が不正です。");
77
77
  return { url: record.endpoint, options: { headers: { Authorization: `Bearer ${token}` } } };
78
78
  }
79
- /** 登録済みlauncherとDesktopの子孫にある、対応する公式接続だけを共有できる。 */
80
- export function windowsDesktopRelay(launcher, rows = readWindowsProcesses()) {
79
+ /** 公式CLIの直接の親がDesktopであり、選択したlauncherの接続先を使っていることを確認する。 */
80
+ export function windowsDesktopRelay(launcher, rows = readWindowsProcesses(), root = join(dirname(launcher), "sessions")) {
81
81
  const parents = new Map(rows.map(row => [row.pid, row]));
82
82
  for (const server of rows) {
83
83
  if (basename(server.executable).toLowerCase() !== "codex.exe" || !server.command.includes("--ws-token-file"))
84
84
  continue;
85
- let row = parents.get(server.parent_pid);
86
- let matchedLauncher = false;
87
- let matchedDesktop = false;
88
- const seen = new Set();
89
- while (row && !seen.has(row.pid)) {
90
- seen.add(row.pid);
91
- if (row.executable.toLowerCase() === launcher.toLowerCase())
92
- matchedLauncher = true;
93
- if (/\\WindowsApps\\OpenAI\.Codex_[^\\]+\\app\\(?:ChatGPT|Codex)\.exe$/i.test(row.executable)) {
94
- matchedDesktop = true;
95
- break;
96
- }
97
- row = parents.get(row.parent_pid);
98
- }
99
- if (!matchedLauncher || !matchedDesktop)
85
+ const desktop = parents.get(server.parent_pid);
86
+ if (!desktop || !/\\WindowsApps\\OpenAI\.Codex_[^\\]+\\app\\(?:ChatGPT|Codex)\.exe$/i.test(desktop.executable))
87
+ continue;
88
+ if (!rows.some(row => row.parent_pid === desktop.pid && row.executable.toLowerCase() === launcher.toLowerCase()))
100
89
  continue;
101
90
  const token = /(?:^|\s)--ws-token-file\s+(?:"([^"]+)"|(\S+))/.exec(server.command);
102
91
  const tokenFile = token?.[1] ?? token?.[2];
103
92
  if (!tokenFile || !isAbsolute(tokenFile) || basename(tokenFile) !== "token")
104
93
  continue;
105
94
  const file = join(dirname(tokenFile), "connection.json");
95
+ const location = relative(root, file);
96
+ if (isAbsolute(location) || location === ".." || location.startsWith("..\\") || location.startsWith("../"))
97
+ continue;
106
98
  if (readWindowsRelay(file, rows).serverPid !== server.pid)
107
99
  invalid("Steer接続記録と親Codexが一致しません。");
108
100
  return file;