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 +17 -1
- package/README.ja.md +5 -3
- package/README.md +5 -3
- package/dist/codex-relay-arguments.js +36 -0
- package/dist/codex-relay-launcher.js +24 -61
- package/dist/codex-relay-setup.js +44 -0
- package/dist/codex-relay-stdio.js +81 -0
- package/dist/codex-stdio-relay.js +25 -92
- package/dist/setup-codex-relay.js +20 -53
- package/dist/windows-codex-connection.js +9 -17
- package/dist/windows-codex-launcher.js +62 -0
- package/dist/windows-codex-native.js +118 -0
- package/dist/windows-codex-relay.js +70 -128
- package/dist/windows-codex-setup.js +26 -107
- package/docs/DESIGN.md +14 -8
- package/docs/RELEASE.md +5 -1
- package/package.json +1 -1
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.
|
|
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
|
-
|
|
70
|
-
|
|
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.
|
|
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
|
-
|
|
70
|
-
|
|
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.
|
|
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
|
|
2
|
-
|
|
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
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
//
|
|
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 {
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
102
|
-
|
|
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("
|
|
105
|
-
if (!pid ||
|
|
106
|
-
throw new Error("
|
|
107
|
-
|
|
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
|
-
|
|
115
|
-
process.stderr.write("aiterm-relay:
|
|
116
|
-
process.exitCode =
|
|
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 {
|
|
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 {
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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;
|