aiterm-mcp 0.37.1 → 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,14 @@ 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
+
10
18
  ## [0.37.1] - 2026-09-13
11
19
 
12
20
  ### 修正
@@ -1671,7 +1679,8 @@ prototype (preserved under `prototype/python/` as the porting source and referen
1671
1679
  `ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
1672
1680
  provenance.
1673
1681
 
1674
- [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.37.1...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
1675
1684
  [0.37.1]: https://github.com/kitepon/aiterm-mcp/compare/v0.37.0...v0.37.1
1676
1685
  [0.37.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.36.0...v0.37.0
1677
1686
  [0.36.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.35.1...v0.36.0
package/README.ja.md CHANGED
@@ -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.1** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
199
+ **状態:** 開発継続中 · 現行公開版 **v0.37.2** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
199
200
 
200
201
  ### 更新と巻き戻し
201
202
 
package/README.md CHANGED
@@ -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.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).
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
 
@@ -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;
@@ -0,0 +1,61 @@
1
+ import { windowsNativeProcessSource } from "./windows-codex-native.js";
2
+ // POSIXのexecに対応するWindows adapter。公式CLIを起動元の直接の子、中継を公式CLIの子として起動する。
3
+ export function windowsLauncherSource(node, relay, binary, root) {
4
+ const literal = (value) => JSON.stringify(value);
5
+ return `using System;
6
+ using System.Diagnostics;
7
+ using System.Text;
8
+ using System.IO;
9
+ using System.ComponentModel;
10
+ using System.Runtime.InteropServices;
11
+ using System.Runtime.Serialization;
12
+ using System.Runtime.Serialization.Json;
13
+ ${windowsNativeProcessSource}
14
+ [DataContract] public sealed class LaunchPlan {
15
+ [DataMember] public string directory;
16
+ [DataMember] public string endpoint;
17
+ [DataMember] public string[] arguments;
18
+ }
19
+ public static class AitermLauncher {
20
+ static LaunchPlan Prepare(string[] args) {
21
+ var arguments=new StringBuilder();
22
+ foreach(var value in new string[] { ${[relay, "--prepare", root].map(literal).join(", ")} }) arguments.Append(AitermNativeProcess.Quote(value)).Append(' ');
23
+ foreach(var value in args) arguments.Append(AitermNativeProcess.Quote(value)).Append(' ');
24
+ var start=new ProcessStartInfo(${literal(node)},arguments.ToString());
25
+ start.UseShellExecute=false; start.CreateNoWindow=true;
26
+ start.RedirectStandardInput=true; start.RedirectStandardOutput=true; start.RedirectStandardError=true;
27
+ start.StandardOutputEncoding=new UTF8Encoding(false);
28
+ using(var child=Process.Start(start)) {
29
+ child.StandardInput.Close();
30
+ var output=child.StandardOutput.ReadToEndAsync();
31
+ var error=child.StandardError.BaseStream.CopyToAsync(Console.OpenStandardError());
32
+ child.WaitForExit(); System.Threading.Tasks.Task.WaitAll(output,error);
33
+ if(child.ExitCode!=0) throw new InvalidOperationException("中継の起動準備に失敗しました");
34
+ using(var stream=new MemoryStream(Encoding.UTF8.GetBytes(output.Result)))
35
+ return (LaunchPlan)new DataContractJsonSerializer(typeof(LaunchPlan)).ReadObject(stream);
36
+ }
37
+ }
38
+ public static int Main(string[] args) {
39
+ LaunchPlan plan=null;
40
+ try {
41
+ Console.OutputEncoding=new UTF8Encoding(false);
42
+ plan=Prepare(args);
43
+ using(var lifetime=new AitermNativeProcess.Lifetime())
44
+ using(var server=AitermNativeProcess.Start(AitermNativeProcess.ParentId(),${literal(binary)},plan.arguments,plan.directory!=null,lifetime)) {
45
+ if(plan.directory==null) return server.Wait();
46
+ // 中継は公式CLIのjobを継承する。launcherは終了監視だけを担い、JSON-RPCを通さない。
47
+ using(var bridge=AitermNativeProcess.Start(server.Pid,${literal(node)},new string[] {
48
+ ${literal(relay)},"--serve",${literal(binary)},plan.directory,plan.endpoint,server.Pid.ToString()
49
+ },false,null)) {
50
+ int code=bridge.Wait();
51
+ lifetime.Dispose();
52
+ server.Wait();
53
+ return code;
54
+ }
55
+ }
56
+ } catch(Exception error) { Console.Error.WriteLine("aiterm-mcp: Windows起動失敗: "+error.Message); return 1; }
57
+ finally { if(plan!=null && plan.directory!=null && Directory.Exists(plan.directory)) Directory.Delete(plan.directory,true); }
58
+ }
59
+ }
60
+ `;
61
+ }
@@ -0,0 +1,118 @@
1
+ // Windowsの標準process作成APIで、公式CLIの直接の親と必要なstdioだけを引き継ぐ。
2
+ // launcherと公式CLIのPIDは異なる。Macのexecと同じPIDだとは扱わない。
3
+ export const windowsNativeProcessSource = String.raw `
4
+ public static class AitermNativeProcess {
5
+ [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)] struct StartupInfo {
6
+ public int cb; public string reserved,desktop,title;
7
+ public int x,y,xSize,ySize,xChars,yChars,fill,flags; public short show,reservedSize;
8
+ public IntPtr reservedBytes,input,output,error;
9
+ }
10
+ [StructLayout(LayoutKind.Sequential)] struct StartupInfoEx { public StartupInfo startup; public IntPtr attributes; }
11
+ [StructLayout(LayoutKind.Sequential)] struct ProcessInfo { public IntPtr process,thread; public int pid,tid; }
12
+ [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)] struct ProcessEntry {
13
+ public uint size,usage,pid; public UIntPtr heap; public uint module,threads,parent; public int priority; public uint flags;
14
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst=260)] public string executable;
15
+ }
16
+ [StructLayout(LayoutKind.Sequential)] struct JobLimits {
17
+ public long processTime,jobTime; public uint flags; public UIntPtr minWorking,maxWorking;
18
+ public uint activeProcesses; public UIntPtr affinity; public uint priority,scheduling;
19
+ }
20
+ [StructLayout(LayoutKind.Sequential)] struct JobInfo {
21
+ public JobLimits limits; public ulong reads,writes,others,readBytes,writeBytes,otherBytes;
22
+ public UIntPtr processMemory,jobMemory,peakProcessMemory,peakJobMemory;
23
+ }
24
+ [DllImport("kernel32.dll",SetLastError=true)] static extern IntPtr CreateToolhelp32Snapshot(uint flags,uint pid);
25
+ [DllImport("kernel32.dll",CharSet=CharSet.Unicode,SetLastError=true)] static extern bool Process32FirstW(IntPtr snapshot,ref ProcessEntry entry);
26
+ [DllImport("kernel32.dll",CharSet=CharSet.Unicode,SetLastError=true)] static extern bool Process32NextW(IntPtr snapshot,ref ProcessEntry entry);
27
+ [DllImport("kernel32.dll",SetLastError=true)] static extern IntPtr OpenProcess(uint access,bool inherit,int pid);
28
+ [DllImport("kernel32.dll")] static extern IntPtr GetCurrentProcess();
29
+ [DllImport("kernel32.dll")] static extern IntPtr GetStdHandle(int kind);
30
+ [DllImport("kernel32.dll",CharSet=CharSet.Unicode,SetLastError=true)] static extern IntPtr CreateFileW(string path,uint access,uint share,IntPtr security,uint creation,uint flags,IntPtr template);
31
+ [DllImport("kernel32.dll",SetLastError=true)] static extern bool DuplicateHandle(IntPtr source,IntPtr handle,IntPtr target,out IntPtr duplicate,uint access,bool inherit,uint options);
32
+ [DllImport("kernel32.dll",SetLastError=true)] static extern bool InitializeProcThreadAttributeList(IntPtr list,int count,int flags,ref IntPtr size);
33
+ [DllImport("kernel32.dll",SetLastError=true)] static extern bool UpdateProcThreadAttribute(IntPtr list,uint flags,IntPtr attribute,IntPtr value,IntPtr size,IntPtr previous,IntPtr returned);
34
+ [DllImport("kernel32.dll")] static extern void DeleteProcThreadAttributeList(IntPtr list);
35
+ [DllImport("kernel32.dll",CharSet=CharSet.Unicode,SetLastError=true)] static extern bool CreateProcessW(string app,StringBuilder command,IntPtr processSecurity,IntPtr threadSecurity,bool inherit,uint flags,IntPtr environment,string cwd,ref StartupInfoEx startup,out ProcessInfo info);
36
+ [DllImport("kernel32.dll",SetLastError=true)] static extern IntPtr CreateJobObjectW(IntPtr security,IntPtr name);
37
+ [DllImport("kernel32.dll",SetLastError=true)] static extern bool SetInformationJobObject(IntPtr job,int kind,ref JobInfo info,uint size);
38
+ [DllImport("kernel32.dll",SetLastError=true)] static extern bool AssignProcessToJobObject(IntPtr job,IntPtr process);
39
+ [DllImport("kernel32.dll",SetLastError=true)] static extern uint ResumeThread(IntPtr thread);
40
+ [DllImport("kernel32.dll")] static extern bool TerminateProcess(IntPtr process,uint code);
41
+ [DllImport("kernel32.dll")] static extern bool CloseHandle(IntPtr handle);
42
+ [DllImport("kernel32.dll")] static extern uint WaitForSingleObject(IntPtr handle,uint time);
43
+ [DllImport("kernel32.dll",SetLastError=true)] static extern bool GetExitCodeProcess(IntPtr process,out uint exitCode);
44
+ static void Check(bool ok) { if (!ok) throw new Win32Exception(Marshal.GetLastWin32Error()); }
45
+ public static int ParentId() {
46
+ IntPtr snapshot=CreateToolhelp32Snapshot(2,0); Check(snapshot!=new IntPtr(-1));
47
+ try {
48
+ var entry=new ProcessEntry(); entry.size=(uint)Marshal.SizeOf(entry);
49
+ Check(Process32FirstW(snapshot,ref entry)); int current=Process.GetCurrentProcess().Id;
50
+ do { if(entry.pid==current) return (int)entry.parent; } while(Process32NextW(snapshot,ref entry));
51
+ throw new InvalidOperationException("launcherの親processが見つかりません");
52
+ } finally { CloseHandle(snapshot); }
53
+ }
54
+ public static string Quote(string text) {
55
+ if(text.Length>0 && text.IndexOfAny(new char[] {' ','\t','\n','\v','"'})<0) return text;
56
+ var result=new StringBuilder().Append('"'); int slashes=0;
57
+ foreach(char c in text) {
58
+ if(c=='\\') { slashes++; continue; }
59
+ if(c=='"') { result.Append('\\',slashes*2+1); result.Append(c); }
60
+ else { result.Append('\\',slashes); result.Append(c); }
61
+ slashes=0;
62
+ }
63
+ result.Append('\\',slashes*2); return result.Append('"').ToString();
64
+ }
65
+ public sealed class Child : IDisposable {
66
+ internal IntPtr handle; public readonly int Pid;
67
+ internal Child(IntPtr process,int pid) { handle=process; Pid=pid; }
68
+ public int Wait() { WaitForSingleObject(handle,0xffffffff); uint code; Check(GetExitCodeProcess(handle,out code)); return (int)code; }
69
+ public void Dispose() { if(handle!=IntPtr.Zero) { CloseHandle(handle); handle=IntPtr.Zero; } }
70
+ }
71
+ public sealed class Lifetime : IDisposable {
72
+ internal IntPtr handle;
73
+ public Lifetime() {
74
+ handle=CreateJobObjectW(IntPtr.Zero,IntPtr.Zero); Check(handle!=IntPtr.Zero);
75
+ var info=new JobInfo(); info.limits.flags=0x2000;
76
+ try { Check(SetInformationJobObject(handle,9,ref info,(uint)Marshal.SizeOf(info))); }
77
+ catch { Dispose(); throw; }
78
+ }
79
+ public void Dispose() { if(handle!=IntPtr.Zero) { CloseHandle(handle); handle=IntPtr.Zero; } }
80
+ }
81
+ public static Child Start(int parentId,string binary,string[] args,bool quiet,Lifetime lifetime) {
82
+ IntPtr parent=IntPtr.Zero,list=IntPtr.Zero,parentValue=IntPtr.Zero,handleList=IntPtr.Zero,nul=IntPtr.Zero;
83
+ var remote=new IntPtr[3]; var info=new ProcessInfo(); bool initialized=false;
84
+ try {
85
+ parent=OpenProcess(0x80|0x40,false,parentId); Check(parent!=IntPtr.Zero);
86
+ var current=GetCurrentProcess();
87
+ if(quiet) { nul=CreateFileW("NUL",0xc0000000,3,IntPtr.Zero,3,0,IntPtr.Zero); Check(nul!=new IntPtr(-1)); }
88
+ // 指定した親へ必要な3 handleだけを一時複製し、child作成直後に親から閉じる。
89
+ for(int i=0;i<3;i++) Check(DuplicateHandle(current,quiet && i<2 ? nul : GetStdHandle(-10-i),parent,out remote[i],0,true,2));
90
+ IntPtr size=IntPtr.Zero; InitializeProcThreadAttributeList(IntPtr.Zero,2,0,ref size);
91
+ list=Marshal.AllocHGlobal(size); Check(InitializeProcThreadAttributeList(list,2,0,ref size)); initialized=true;
92
+ parentValue=Marshal.AllocHGlobal(IntPtr.Size); Marshal.WriteIntPtr(parentValue,parent);
93
+ Check(UpdateProcThreadAttribute(list,0,(IntPtr)0x20000,parentValue,(IntPtr)IntPtr.Size,IntPtr.Zero,IntPtr.Zero));
94
+ handleList=Marshal.AllocHGlobal(3*IntPtr.Size);
95
+ for(int i=0;i<3;i++) Marshal.WriteIntPtr(handleList,i*IntPtr.Size,remote[i]);
96
+ Check(UpdateProcThreadAttribute(list,0,(IntPtr)0x20002,handleList,(IntPtr)(3*IntPtr.Size),IntPtr.Zero,IntPtr.Zero));
97
+ var startup=new StartupInfoEx(); startup.startup.cb=Marshal.SizeOf(startup); startup.attributes=list;
98
+ startup.startup.flags=0x100; startup.startup.input=remote[0]; startup.startup.output=remote[1]; startup.startup.error=remote[2];
99
+ var command=new StringBuilder(Quote(binary)); foreach(var arg in args) command.Append(' ').Append(Quote(arg));
100
+ Check(CreateProcessW(binary,command,IntPtr.Zero,IntPtr.Zero,true,0x80000|0x8000000|4,IntPtr.Zero,Environment.CurrentDirectory,ref startup,out info));
101
+ if(lifetime!=null) Check(AssignProcessToJobObject(lifetime.handle,info.process));
102
+ for(int i=0;i<3;i++) { IntPtr local; Check(DuplicateHandle(parent,remote[i],current,out local,0,false,3)); CloseHandle(local); remote[i]=IntPtr.Zero; }
103
+ Check(ResumeThread(info.thread)!=0xffffffff);
104
+ var child=new Child(info.process,info.pid); info.process=IntPtr.Zero; return child;
105
+ } catch {
106
+ if(info.process!=IntPtr.Zero) TerminateProcess(info.process,1);
107
+ throw;
108
+ } finally {
109
+ if(nul!=IntPtr.Zero && nul!=new IntPtr(-1)) CloseHandle(nul);
110
+ if(info.thread!=IntPtr.Zero) CloseHandle(info.thread); if(info.process!=IntPtr.Zero) CloseHandle(info.process);
111
+ foreach(var handle in remote) if(handle!=IntPtr.Zero) { IntPtr local; if(DuplicateHandle(parent,handle,GetCurrentProcess(),out local,0,false,3)) CloseHandle(local); }
112
+ if(initialized) DeleteProcThreadAttributeList(list);
113
+ if(list!=IntPtr.Zero) Marshal.FreeHGlobal(list); if(parentValue!=IntPtr.Zero) Marshal.FreeHGlobal(parentValue); if(handleList!=IntPtr.Zero) Marshal.FreeHGlobal(handleList);
114
+ if(parent!=IntPtr.Zero) CloseHandle(parent);
115
+ }
116
+ }
117
+ }
118
+ `;
@@ -1,154 +1,94 @@
1
- // gpt-connectorのWindows実装をMIT条件で移植。実行時の外部製品依存は持たない。
2
- // Windowsの起動processと認証付きloopback接続を所有する。JSON-RPC本文は変更しない。
3
- import { spawn } from "node:child_process";
1
+ // Windowsの認証付きloopback接続と接続情報だけを所有する。起動・stdio中継の契約は共通。
4
2
  import { createServer } from "node:net";
5
- import { createInterface } from "node:readline";
6
3
  import { once } from "node:events";
7
4
  import { randomBytes, randomUUID } from "node:crypto";
8
5
  import { join } from "node:path";
9
6
  import { fileURLToPath } from "node:url";
10
7
  import { writeFileSync, rmSync } from "node:fs";
11
- import { setTimeout as delay } from "node:timers/promises";
12
8
  import WebSocket from "ws";
13
9
  import { ensurePrivateDirectory, makeFilePrivate } from "./windows-codex-state.js";
14
10
  import { readWindowsProcesses } from "./windows-codex-connection.js";
15
- export function windowsServerArguments(args) {
16
- let server = false;
17
- const result = [];
18
- for (let i = 0; i < args.length; i++) {
19
- const arg = args[i];
20
- if (["-c", "--config", "--enable", "--disable"].includes(arg)) {
21
- result.push(arg, args[++i]);
22
- continue;
23
- }
24
- if (/^(?:--config=|--enable=|--disable=|-c.)/.test(arg)) {
25
- result.push(arg);
26
- continue;
27
- }
28
- if (!server) {
29
- if (arg !== "app-server")
30
- return null;
31
- server = true;
32
- result.push(arg);
33
- continue;
34
- }
35
- if (["proxy", "daemon", "start", "stop", "status", "generate-ts", "generate-json-schema", "--help", "-h", "--version", "-V"].includes(arg))
36
- return null;
37
- if (arg === "--stdio" || arg === "--listen=stdio://")
38
- continue;
39
- if (arg === "--listen") {
40
- if (args[++i] !== "stdio://")
41
- throw new Error("stdio以外の接続指定は変更できません");
42
- continue;
43
- }
44
- if (arg.startsWith("--listen=") || arg.startsWith("--ws-"))
45
- throw new Error("既存の接続・認証指定は変更できません");
46
- result.push(arg);
47
- }
48
- return server ? result : null;
49
- }
50
- export async function runWindowsCodexRelay(binary, root, args) {
51
- const serverArgs = windowsServerArguments(args);
52
- if (!serverArgs) {
53
- const child = spawn(binary, args, { stdio: "inherit", windowsHide: true });
54
- return (await once(child, "close"))[0] ?? 1;
55
- }
11
+ import { codexServerArguments } from "./codex-relay-arguments.js";
12
+ import { relayCodexStdio } from "./codex-relay-stdio.js";
13
+ export { codexServerArguments as windowsServerArguments } from "./codex-relay-arguments.js";
14
+ export async function prepareWindowsRelay(root, args) {
15
+ const serverArgs = codexServerArguments(args);
16
+ if (!serverArgs)
17
+ return { directory: null, endpoint: null, arguments: args };
56
18
  const directory = join(root, randomUUID());
57
19
  ensurePrivateDirectory(directory);
58
- const file = join(directory, "connection.json");
59
- const tokenFile = join(directory, "token");
60
- const token = randomBytes(32).toString("hex");
61
- writeFileSync(tokenFile, token, { flag: "wx" });
62
- makeFilePrivate(tokenFile);
63
- // OSが割り当てた空きportを使い、取得競合は公式serverの起動失敗として明示する。
64
- const reservation = createServer();
65
- reservation.listen(0, "127.0.0.1");
66
- await once(reservation, "listening");
67
- const port = reservation.address().port;
68
- await new Promise((resolve, reject) => reservation.close(error => error ? reject(error) : resolve()));
69
- const endpoint = `ws://127.0.0.1:${port}`;
70
- const child = spawn(binary, [...serverArgs, "--listen", endpoint, "--ws-auth", "capability-token", "--ws-token-file", tokenFile], {
71
- stdio: ["ignore", "ignore", "pipe"], windowsHide: true,
72
- });
73
- const exited = new Promise(resolve => child.once("close", () => resolve()));
74
- let failed;
75
- child.on("error", error => { failed = error; });
76
- // WebSocketの定型起動案内だけを除き、公式CLIの診断はDesktopへそのまま返す。
77
- const diagnostics = createInterface({ input: child.stderr });
78
- diagnostics.on("line", line => {
79
- if (!/^(?:\s*|codex app-server \(WebSockets\)| {2}(?:listening on:|readyz:|healthz:|note:).*)$/.test(line))
80
- process.stderr.write(line + "\n");
81
- });
82
- let socket;
83
- const input = createInterface({ input: process.stdin, crlfDelay: Infinity });
84
- const lines = input[Symbol.asyncIterator]();
85
- let ending = false;
86
- const stop = () => { ending = true; child.kill(); socket?.terminate(); input.close(); process.stdin.destroy(); };
87
- input.once("close", stop);
88
- process.stdout.once("error", stop);
89
20
  try {
90
- const deadline = Date.now() + 15_000;
91
- while (!socket) {
92
- if (failed || child.exitCode !== null || child.signalCode !== null)
93
- throw new Error("公式Codexの起動に失敗しました", { cause: failed });
94
- const candidate = new WebSocket(endpoint, { headers: { Authorization: `Bearer ${token}` }, handshakeTimeout: 1_000 });
95
- try {
96
- await once(candidate, "open");
97
- socket = candidate;
21
+ const tokenFile = join(directory, "token");
22
+ writeFileSync(tokenFile, randomBytes(32).toString("hex"), { flag: "wx" });
23
+ makeFilePrivate(tokenFile);
24
+ // port取得後の競合は公式serverの起動失敗として明示する。
25
+ const reservation = createServer();
26
+ reservation.listen(0, "127.0.0.1");
27
+ await once(reservation, "listening");
28
+ const port = reservation.address().port;
29
+ await new Promise((resolve, reject) => reservation.close(error => error ? reject(error) : resolve()));
30
+ const endpoint = "ws://127.0.0.1:" + port;
31
+ return { directory, endpoint, arguments: [...serverArgs, "--listen", endpoint, "--ws-auth", "capability-token", "--ws-token-file", tokenFile] };
32
+ }
33
+ catch (error) {
34
+ rmSync(directory, { recursive: true, force: true });
35
+ throw error;
36
+ }
37
+ }
38
+ export async function serveWindowsRelay(binary, directory, endpoint, serverPid) {
39
+ if (process.ppid !== serverPid)
40
+ throw new Error("公式Codexの子として中継を起動してください");
41
+ const { readFileSync } = await import("node:fs");
42
+ const token = readFileSync(join(directory, "token"), "utf8");
43
+ try {
44
+ await relayCodexStdio({
45
+ alive: () => { try {
46
+ process.kill(serverPid, 0);
47
+ return true;
98
48
  }
99
49
  catch (error) {
100
- candidate.on("error", () => { });
101
- candidate.terminate();
102
- if (error.code !== "ECONNREFUSED" || Date.now() >= deadline)
103
- throw error;
104
- await delay(50);
105
- }
106
- }
107
- const server = readWindowsProcesses().find(row => row.pid === child.pid);
108
- if (!server)
109
- throw new Error("公式Codexのprocess情報がありません");
110
- const record = { schema: "aiterm.windows-relay.v1", serverPid: server.pid, serverStarted: server.started, binary, endpoint };
111
- writeFileSync(file, JSON.stringify(record), { flag: "wx" });
112
- makeFilePrivate(file);
113
- const connected = socket;
114
- connected.on("message", (data, binary) => {
115
- if (binary) {
116
- failed = new Error("公式Codexから予期しないバイナリ応答を受信しました");
117
- stop();
118
- return;
119
- }
120
- if (!process.stdout.write(data.toString() + "\n"))
121
- connected.pause();
50
+ if (error.code === "ESRCH")
51
+ return false;
52
+ throw error;
53
+ } },
54
+ socket: () => new WebSocket(endpoint, { headers: { Authorization: "Bearer " + token }, handshakeTimeout: 1_000, perMessageDeflate: false }),
55
+ stop: async () => {
56
+ // launcherが保持するprocess handleにより、このPIDは終了確認まで再利用されない。
57
+ try {
58
+ process.kill(serverPid);
59
+ }
60
+ catch (error) {
61
+ if (error.code !== "ESRCH")
62
+ throw error;
63
+ }
64
+ },
65
+ connected: () => {
66
+ const server = readWindowsProcesses().find(row => row.pid === serverPid);
67
+ if (!server || server.executable.toLowerCase() !== binary.toLowerCase())
68
+ throw new Error("公式Codexの起動を確認できません");
69
+ const record = { schema: "aiterm.windows-relay.v1", serverPid, serverStarted: server.started, binary, endpoint };
70
+ const file = join(directory, "connection.json");
71
+ writeFileSync(file, JSON.stringify(record), { flag: "wx" });
72
+ makeFilePrivate(file);
73
+ },
122
74
  });
123
- process.stdout.on("drain", () => connected.resume());
124
- connected.on("error", () => { failed = new Error("公式Codexとの接続に失敗しました"); stop(); });
125
- connected.on("close", () => { if (!ending) {
126
- failed = new Error("公式Codexとの接続が終了しました");
127
- stop();
128
- } });
129
- // initializeが接続準備前に届いても失わないよう、iteratorを接続前から保持する。
130
- for await (const line of lines)
131
- await new Promise((resolve, reject) => connected.send(line, error => error ? reject(error) : resolve()));
132
- if (failed)
133
- throw failed;
134
- return 0;
135
75
  }
136
76
  finally {
137
- stop();
138
- await exited;
139
- diagnostics.close();
140
77
  rmSync(directory, { recursive: true, force: true });
141
78
  }
142
79
  }
143
80
  if (process.argv[1] === fileURLToPath(import.meta.url)) {
144
- const [binary, root, ...args] = process.argv.slice(2);
81
+ const [mode, ...args] = process.argv.slice(2);
145
82
  try {
146
- if (!binary || !root)
147
- throw new Error("Windows中継の起動指定がありません");
148
- process.exitCode = await runWindowsCodexRelay(binary, root, args);
83
+ if (mode === "--prepare" && args[0])
84
+ process.stdout.write(JSON.stringify(await prepareWindowsRelay(args[0], args.slice(1))));
85
+ else if (mode === "--serve" && args.length === 4 && /^[1-9][0-9]*$/.test(args[3]))
86
+ await serveWindowsRelay(args[0], args[1], args[2], Number(args[3]));
87
+ else
88
+ throw new Error("Windows中継の起動指定が不正です");
149
89
  }
150
90
  catch (error) {
151
- process.stderr.write(`aiterm-mcp: ${error instanceof Error ? error.message : "Windows中継が失敗しました"}\n`);
91
+ process.stderr.write("aiterm-mcp: " + (error instanceof Error ? error.message : "Windows中継が失敗しました") + "\n");
152
92
  process.exitCode = 1;
153
93
  }
154
94
  }
@@ -1,18 +1,20 @@
1
1
  // gpt-connectorのWindows実装をMIT条件で移植。実行時の外部製品依存は持たない。
2
2
  // Windows Desktop用の実行file、ユーザー環境変数、親processの接続確認を所有する。
3
3
  import { existsSync, readFileSync, writeFileSync, renameSync, readdirSync, rmSync } from "node:fs";
4
- import { join, dirname } from "node:path";
4
+ import { join } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { spawnSync } from "node:child_process";
7
7
  import { randomUUID, createHash } from "node:crypto";
8
8
  import { homedir } from "node:os";
9
- import { relayConfigDirectory, readRelayConfig } from "./codex-relay-config.js";
9
+ import { relayConfigDirectory } from "./codex-relay-config.js";
10
10
  import { SetupError } from "./setup-platform.js";
11
11
  import { verifyRelayLauncher } from "./setup-codex-relay.js";
12
12
  import { withCodexRelay } from "./codex-relay-client.js";
13
13
  import { windowsPowerShellSync, quotePowerShell } from "./windows-codex-state.js";
14
14
  import { ensurePrivateDirectory, makeFilePrivate } from "./windows-codex-state.js";
15
- import { readWindowsRelay, windowsDesktopRelay } from "./windows-codex-connection.js";
15
+ import { windowsDesktopRelay } from "./windows-codex-connection.js";
16
+ import { configureRelay } from "./codex-relay-setup.js";
17
+ import { windowsLauncherSource } from "./windows-codex-launcher.js";
16
18
  export function findWindowsCodexCache(resources, cache) {
17
19
  // Desktopが展開した4実行fileを配布元と照合する。展開・更新はDesktop自身が所有する。
18
20
  const names = ["codex.exe", "codex-code-mode-host.exe", "codex-windows-sandbox-setup.exe", "codex-command-runner.exe"];
@@ -60,48 +62,7 @@ $result = [UIntPtr]::Zero
60
62
  [void][AitermEnvironment]::SendMessageTimeout([IntPtr]0xffff, 0x1a, [UIntPtr]::Zero, 'Environment', 2, 5000, [ref]$result)
61
63
  `, 10_000);
62
64
  }
63
- export function windowsLauncherSource(node, relay, binary, root) {
64
- return `using System;
65
- using System.Diagnostics;
66
- using System.Text;
67
- public static class AitermLauncher {
68
- static async System.Threading.Tasks.Task Input(Process child) {
69
- var input = Console.OpenStandardInput(); var output = child.StandardInput.BaseStream;
70
- var buffer = new byte[8192]; int count;
71
- while ((count = await input.ReadAsync(buffer, 0, buffer.Length)) > 0) {
72
- await output.WriteAsync(buffer, 0, count); await output.FlushAsync();
73
- }
74
- child.StandardInput.Close();
75
- }
76
- static string Quote(string text) {
77
- var result = new StringBuilder().Append('"'); int slashes = 0;
78
- foreach (char c in text) {
79
- if (c == '\\\\') { slashes++; continue; }
80
- if (c == '"') { result.Append('\\\\', slashes * 2 + 1); result.Append(c); }
81
- else { result.Append('\\\\', slashes); result.Append(c); }
82
- slashes = 0;
83
- }
84
- result.Append('\\\\', slashes * 2); return result.Append('"').ToString();
85
- }
86
- public static int Main(string[] args) {
87
- try {
88
- var arguments = new StringBuilder();
89
- foreach (var value in new string[] { ${[relay, binary, root].map(value => JSON.stringify(value)).join(", ")} }) arguments.Append(Quote(value)).Append(' ');
90
- foreach (var value in args) arguments.Append(Quote(value)).Append(' ');
91
- var start = new ProcessStartInfo(${JSON.stringify(node)}, arguments.ToString());
92
- start.UseShellExecute = false; start.CreateNoWindow = true;
93
- start.RedirectStandardInput = true; start.RedirectStandardOutput = true; start.RedirectStandardError = true;
94
- using (var child = Process.Start(start)) {
95
- Input(child).ContinueWith(task => { if (task.IsFaulted && !child.HasExited) child.Kill(); });
96
- var output = child.StandardOutput.BaseStream.CopyToAsync(Console.OpenStandardOutput());
97
- var error = child.StandardError.BaseStream.CopyToAsync(Console.OpenStandardError());
98
- child.WaitForExit(); System.Threading.Tasks.Task.WaitAll(output, error); return child.ExitCode;
99
- }
100
- } catch { Console.Error.WriteLine("aiterm-mcp: Windows中継を起動できません"); return 1; }
101
- }
102
- }
103
- `;
104
- }
65
+ export { windowsLauncherSource } from "./windows-codex-launcher.js";
105
66
  export function buildWindowsLauncher(directory, source) {
106
67
  const digest = createHash("sha256").update(source).digest("hex").slice(0, 16);
107
68
  const launcher = join(directory, `codex-${digest}.exe`);
@@ -113,80 +74,38 @@ export function buildWindowsLauncher(directory, source) {
113
74
  throw new SetupError("codex_launcher_compiler_unavailable", "Windows標準.NET Frameworkのコンパイラが見つかりません。");
114
75
  writeFileSync(sourceFile, source);
115
76
  makeFilePrivate(sourceFile);
116
- const result = spawnSync(compiler, ["/nologo", "/target:exe", `/out:${launcher}`, sourceFile], { encoding: "utf8", windowsHide: true, timeout: 20_000 });
77
+ const result = spawnSync(compiler, ["/nologo", "/target:exe", "/reference:System.Runtime.Serialization.dll", `/out:${launcher}`, sourceFile], { encoding: "utf8", windowsHide: true, timeout: 20_000 });
117
78
  if (result.status !== 0)
118
79
  throw new SetupError("codex_launcher_build_failed", "Windows用のCodex起動fileを作成できません。");
119
80
  makeFilePrivate(launcher);
120
81
  return launcher;
121
82
  }
122
83
  async function live(config) {
123
- const file = windowsDesktopRelay(config.launcher);
84
+ const file = windowsDesktopRelay(config.launcher, undefined, config.socket_root);
124
85
  if (!file)
125
86
  return false;
126
87
  await withCodexRelay(file, request => request("thread/loaded/list", { limit: 1 }));
127
88
  return true;
128
89
  }
90
+ export function windowsCodexRuntime(overrides = {}) {
91
+ const directory = overrides.directory ?? relayConfigDirectory();
92
+ const runtime = {
93
+ directory, socket_root: join(directory, "sessions"), node: process.execPath,
94
+ relay: fileURLToPath(new URL("./windows-codex-relay.js", import.meta.url)),
95
+ findBinary: findWindowsCodexBinary, getGui, setGui, verify: verifyRelayLauncher, live,
96
+ // User環境変数はsetGuiの一操作で永続化される。
97
+ persist: () => { }, unpersist: () => { }, prepare: ensurePrivateDirectory, save,
98
+ build: async (binary) => {
99
+ ensurePrivateDirectory(runtime.socket_root);
100
+ const launcher = buildWindowsLauncher(runtime.directory, windowsLauncherSource(runtime.node, runtime.relay, binary, runtime.socket_root));
101
+ await runtime.verify(launcher);
102
+ return launcher;
103
+ }, ...overrides,
104
+ };
105
+ return runtime;
106
+ }
129
107
  export async function configureWindowsCodexSteer(action, overrides = {}) {
130
- const runtime = { directory: relayConfigDirectory(), node: process.execPath,
131
- relay: fileURLToPath(new URL("./windows-codex-relay.js", import.meta.url)), findBinary: findWindowsCodexBinary, getGui, setGui,
132
- verify: verifyRelayLauncher, live,
133
- shared: async (launcher) => {
134
- const file = windowsDesktopRelay(launcher);
135
- if (!file)
136
- return null;
137
- await withCodexRelay(file, request => request("thread/loaded/list", { limit: 1 }));
138
- return { binary: readWindowsRelay(file).binary, root: dirname(dirname(file)) };
139
- }, ...overrides };
140
- const previous = readRelayConfig(runtime.directory);
141
- const current = runtime.getGui("CODEX_CLI_PATH");
142
- if (action === "status") {
143
- if (!previous?.enabled)
144
- return { status: "disabled" };
145
- if (current !== previous.launcher)
146
- return { status: "failed", reason_code: "codex_steer_configuration_changed" };
147
- return await runtime.live(previous) ? { status: "ready" } : { status: "restart_required", reason_code: "codex_restart_required" };
148
- }
149
- if (action === "disable") {
150
- if (!previous?.enabled)
151
- return { status: "disabled" };
152
- if (current !== previous.launcher)
153
- throw new SetupError("codex_steer_configuration_changed", "変更されたCodex起動設定は上書きしません。");
154
- if (previous.launcher !== previous.previous_cli_path)
155
- runtime.setGui("CODEX_CLI_PATH", previous.previous_cli_path);
156
- if (runtime.getGui("CODEX_CLI_PATH") !== previous.previous_cli_path)
157
- throw new SetupError("codex_steer_readback_failed", "元の起動設定を確認できません。");
158
- save(runtime.directory, { ...previous, enabled: false });
159
- return { status: "restart_required", reason_code: "codex_restart_required" };
160
- }
161
- if (previous?.enabled && previous.launcher !== previous.previous_cli_path && ![previous.launcher, previous.previous_cli_path].includes(current)) {
162
- throw new SetupError("codex_steer_configuration_changed", "変更されたCodex起動設定は上書きしません。");
163
- }
164
- if (current && (current !== previous?.launcher || previous.launcher === previous.previous_cli_path)) {
165
- const shared = await runtime.shared(current);
166
- if (!shared)
167
- throw new SetupError("codex_steer_configuration_conflict", "別のCodex起動設定との互換接続を確認できないため変更していません。");
168
- ensurePrivateDirectory(runtime.directory);
169
- save(runtime.directory, { schema: "aiterm.codex-relay.v1", enabled: true, binary: shared.binary,
170
- node: runtime.node, launcher: current, socket_root: shared.root, previous_cli_path: current });
171
- return { status: "ready" };
172
- }
173
- for (const key of ["CODEX_APP_SERVER_WS_URL", "CODEX_APP_SERVER_USE_LOCAL_DAEMON", "CODEX_APP_SERVER_FORCE_CLI"]) {
174
- if (runtime.getGui(key))
175
- throw new SetupError("codex_steer_configuration_conflict", `${key}が設定されているため変更していません。`);
176
- }
177
- ensurePrivateDirectory(runtime.directory);
178
- const binary = runtime.findBinary();
179
- const root = join(runtime.directory, "sessions");
180
- ensurePrivateDirectory(root);
181
- const launcher = buildWindowsLauncher(runtime.directory, windowsLauncherSource(runtime.node, runtime.relay, binary, root));
182
- await runtime.verify(launcher);
183
- const config = { schema: "aiterm.codex-relay.v1", enabled: true, binary, node: runtime.node, launcher,
184
- socket_root: root, previous_cli_path: previous?.enabled && previous.launcher !== previous.previous_cli_path ? previous.previous_cli_path : current };
185
- save(runtime.directory, config);
186
- runtime.setGui("CODEX_CLI_PATH", launcher);
187
- if (runtime.getGui("CODEX_CLI_PATH") !== launcher)
188
- throw new SetupError("codex_steer_readback_failed", "WindowsのCodex起動設定を確認できません。");
189
- return await runtime.live(config) ? { status: "ready" } : { status: "restart_required", reason_code: "codex_restart_required" };
108
+ return configureRelay(action, windowsCodexRuntime(overrides));
190
109
  }
191
110
  function save(directory, config) {
192
111
  const file = join(directory, `${randomUUID()}.tmp`);
package/docs/DESIGN.md CHANGED
@@ -68,7 +68,7 @@ Cursorのsubmitはadapterがextended keyboard protocolのEnterへ変換し、呼
68
68
  宛先はCodexのMCP handshakeと各要求の`_meta.threadId`から取得する。modelが指定したIDや環境変数で代用しない。
69
69
  単品導入の`src/codex-parent-receiver.ts`は同じ`CODEX_HOME`の公式app-serverへstdioで接続し、
70
70
  `thread/read`と`thread/queue/list`で宛先を確認してから`thread/queue/add`へ本文をJSONで渡す。
71
- Steerを選択したmacOS Desktopでは、MCP processの祖先にある公式App Serverの本人所有socketへ接続する。
71
+ Steerを選択したmacOS・Windows Desktopでは、MCP processの祖先にある公式App Serverの本人専用接続へ接続する。
72
72
  `thread/loaded/list`と`thread/read`で同じ親を確認し、公式`turn/start`へ一度だけ送る。
73
73
  公式の`start_or_steer_turn`が実行中なら同じturnへ追加入力し、終了後なら同じtaskの次turnを開始する。
74
74
  状態を読んでから送信方法を選ぶraceや再送は作らない。model・権限のoverrideも渡さない。
@@ -84,17 +84,20 @@ macOSのユーザーLaunchAgentはログイン時に同じ起動設定を適用
84
84
  保存した元の`CODEX_CLI_PATH`へ戻す。他から変更された設定は上書きしない。
85
85
  `status=ready`は公式binary・Desktopの直接子・同じsocketのRPC応答を確認した場合だけ返す。
86
86
  初回の設定保存後は`restart_required`であり、Desktopの完全終了・再起動を要する。
87
- WindowsはAitermのnative launcherからNode中継と公式Desktop CLIを起動し、通常の親子関係を保つ。
88
- Windows標準の.NET Frameworkでlauncherを作り、引数とstdioをそのまま中継する。
87
+ 引数の解釈、JSONL中継、設定の有効化・競合・復元は両OSで共通の処理を使う。
88
+ Windows標準の.NET Frameworkでlauncherを作り、`CreateProcessW`の親process属性と継承handle一覧で、
89
+ 公式CLIをDesktopの直接の子、中継Nodeを公式CLIの直接の子として起動する。
90
+ launcherは終了監視だけを担い、JSON-RPCを通さない。Windowsには同一PIDでのexecがないためPIDは変わるが、
91
+ 公式binary、起動元との直接の親子関係、通常の環境とstdioは維持する。
89
92
  公式Desktopが展開した4実行ファイルを配布元のSHA-256と照合し、AitermがCodexをコピー・再配布しない。
90
93
  公式CLIの認証付きloopback WebSocketを使い、本人専用ACLのtokenと接続記録をAitermのsessions下へ置く。
91
94
  Windowsのprocess情報はprocess-runtimeが所有し、接続はPID・開始時刻・実行ファイル・token引数で照合する。
92
95
  MCP processの祖先から親を選び、同じloaded taskへ送る。開始時刻は小数桁の表記差を正規化して比較する。
93
- stdio EOFで自分が起動した公式serverを停止し、接続記録を削除する。
94
- 既存の互換launcherは公式serverからlauncherとDesktopへの祖先関係、ACL、RPCが成立した場合だけ共有する。
95
- 対応する接続記録はaiterm.windows-relay.v1とgpt-connector.windows-relay.v1で、後者もAiterm内で解釈する。
96
- 共有時はlauncherとprevious_cli_pathを同値で記録し、enableとdisableで所有外の起動設定を変更しない。
97
- 接続共有は任意であり、他製品のコード・設定・コマンドはAiterm単独導入の前提にならない。
96
+ stdio EOFで自分が起動した公式serverを停止し、接続記録を削除する。native launcherはJob Objectで
97
+ 所有するprocessだけを終了し、公式serverの終了確認後に戻る。
98
+ 両OSとも有効化前に元の起動設定を保存し、Aitermのlauncherを設定する。解除は元の値へ戻す。
99
+ 旧Windows版で既存launcherを共有した設定は読取り可能だが、次のenableでAitermのlauncherへ移行する。
100
+ 起動・配送・設定・診断は他製品のコード、設定、コマンドへ依存しない。
98
101
  LinuxのSteer選択はunsupportedとする。通常の単品導入・queue配送は全対応OSで維持する。
99
102
 
100
103
  単品導入のqueue配送は親の実行中turnを中断せず、親がidleになった後に処理される。
package/docs/RELEASE.md CHANGED
@@ -41,7 +41,7 @@ Codex Steerを変更した場合は、公式バイナリを指定した`test/cod
41
41
  再起動後の`aiterm-setup --codex-steer status`が`ready`であることと、通常の親からの子の回答配送を確認する。
42
42
  接続があるだけで成功とせず、公式binaryとDesktopの親子関係まで確認する。
43
43
  Windowsは`AITERM_TEST_CODEX_BINARY`に公式Desktopの実行fileを指定し、
44
- `test/windows-codex.test.mjs`で単独launcher、ACL、親識別、既存接続共有、設定解除も確認する。
44
+ `test/windows-codex.test.mjs`で単独launcher、ACL、親識別、元の起動設定の保存と解除も確認する。
45
45
  同じ環境変数で`test/codex-relay-official.test.mjs`の実行中Steer・終了後再開・承認中継・EOFを確認できる。
46
46
  Linuxの未対応Steer選択が理由付きで停止することもCIで確認する。
47
47
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiterm-mcp",
3
- "version": "0.37.1",
3
+ "version": "0.37.2",
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": [