aiterm-mcp 0.36.0 → 0.37.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -1
- package/LICENSE +1 -0
- package/README.ja.md +12 -7
- package/README.md +12 -7
- package/dist/codex-relay-client.js +10 -3
- package/dist/codex-relay-config.js +5 -1
- package/dist/core.js +15 -2
- package/dist/harnesses/codex.js +2 -2
- package/dist/process-runtime.js +2 -1
- package/dist/setup-cli.js +2 -2
- package/dist/setup-codex-relay.js +4 -1
- package/dist/windows-codex-connection.js +111 -0
- package/dist/windows-codex-relay.js +154 -0
- package/dist/windows-codex-setup.js +201 -0
- package/dist/windows-codex-state.js +44 -0
- package/docs/DESIGN.md +14 -1
- package/docs/RELEASE.md +7 -3
- 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.1] - 2026-09-13
|
|
11
|
+
|
|
12
|
+
### 修正
|
|
13
|
+
|
|
14
|
+
- WindowsのCodexでhook確認画面の見出しが画面上部へ出た場合も認識し、`trust_project: true`による起動準備を完了する。
|
|
15
|
+
- npmのshell shimとnative Codexの間にNode processがある場合、同じ起動を複数候補と誤判定して停止する問題を修正した。
|
|
16
|
+
|
|
17
|
+
## [0.37.0] - 2026-09-13
|
|
18
|
+
|
|
19
|
+
### 追加
|
|
20
|
+
|
|
21
|
+
- WindowsのCodex Desktop親へのSteerと終了後再開に対応。公式CLI、認証付きloopback接続、native launcher、設定・解除をAitermだけで提供する。
|
|
22
|
+
- 稼働中の互換launcherは親process・ACL・公式RPCを確認した時だけ共有し、既存の起動設定を保持する。
|
|
23
|
+
|
|
10
24
|
## [0.36.0] - 2026-09-13
|
|
11
25
|
|
|
12
26
|
### 追加
|
|
@@ -1657,7 +1671,9 @@ prototype (preserved under `prototype/python/` as the porting source and referen
|
|
|
1657
1671
|
`ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
|
|
1658
1672
|
provenance.
|
|
1659
1673
|
|
|
1660
|
-
[Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.
|
|
1674
|
+
[Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.37.1...HEAD
|
|
1675
|
+
[0.37.1]: https://github.com/kitepon/aiterm-mcp/compare/v0.37.0...v0.37.1
|
|
1676
|
+
[0.37.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.36.0...v0.37.0
|
|
1661
1677
|
[0.36.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.35.1...v0.36.0
|
|
1662
1678
|
[0.35.1]: https://github.com/kitepon/aiterm-mcp/compare/v0.35.0...v0.35.1
|
|
1663
1679
|
[0.35.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.34.0...v0.35.0
|
package/LICENSE
CHANGED
package/README.ja.md
CHANGED
|
@@ -47,7 +47,7 @@ Ubuntu/Debianはsudoとaptでtmuxを準備する。必要な公式package mana
|
|
|
47
47
|
AI別の`integrations`と選択機能の`codex_steer`を持つ。失敗時は`reason_code`を付け、終了コードはreadyなら0、再起動待ちは3、それ以外は2となる。
|
|
48
48
|
|
|
49
49
|
|
|
50
|
-
### Codex DesktopへSteerを有効にする(macOS)
|
|
50
|
+
### Codex DesktopへSteerを有効にする(macOS・Windows)
|
|
51
51
|
|
|
52
52
|
対話実行の`aiterm-setup`で「Aiterm単品」と「Steer付き」を選べます。無人導入では明示します。
|
|
53
53
|
|
|
@@ -57,14 +57,18 @@ aiterm-setup --json --codex-steer enable
|
|
|
57
57
|
|
|
58
58
|
公式Codex Desktopに同梱されたApp Serverを使い、実行中の親には同じターンへ回答を送り、
|
|
59
59
|
終了後に届いた回答でも同じタスクを自動再開します。App Serverの改造・再ビルド・別配布は不要です。
|
|
60
|
-
|
|
60
|
+
新しいlauncherを設定した場合は`restart_required`(終了コード3)を返します。Codexを完全終了して再起動し、
|
|
61
61
|
`aiterm-setup --codex-steer status`で`ready`を確認してください。接続できない時にキューへ自動退避しません。
|
|
62
62
|
|
|
63
|
-
選択と復元情報は`~/.config/aiterm-mcp/codex-relay
|
|
64
|
-
|
|
63
|
+
選択と復元情報は`~/.config/aiterm-mcp/codex-relay/`へ保存します。macOSはユーザーLaunchAgent、
|
|
64
|
+
Windowsはユーザー環境変数`CODEX_CLI_PATH`で起動設定を維持します。更新時の`aiterm-setup --json`は選択を維持して中継を更新します。
|
|
65
65
|
解除・旧版への巻き戻し前は`aiterm-setup --codex-steer disable`を実行してCodexを再起動してください。
|
|
66
|
-
SteerはmacOS
|
|
67
|
-
Windows
|
|
66
|
+
SteerはmacOS・Windowsの公式Codex Desktopと同梱CLI 0.154以上に対応します。Steer有効時の宛先は中継で起動したDesktopに限ります。
|
|
67
|
+
Windowsでは公式Desktopが展開した実行ファイルを照合し、認証付きのローカル接続へ中継します。
|
|
68
|
+
Aitermのpackageがlauncher・中継・設定・親への配送を含み、gpt-connectorやdotagentsの導入は不要です。
|
|
69
|
+
既存の互換launcherがある場合は、稼働中のDesktop接続を確認した時だけ共有し、その起動設定を変更しません。
|
|
70
|
+
互換性を確認できない設定は理由付きで停止します。WindowsのDesktop更新後はsetupを再実行してください。
|
|
71
|
+
LinuxのSteer付き導入は理由付き`unsupported`を返します。Aiterm単品は従来どおり利用できます。
|
|
68
72
|
|
|
69
73
|
cloneもビルドも不要。どのクライアントでも公開パッケージを次のコマンドで起動する:
|
|
70
74
|
|
|
@@ -191,7 +195,7 @@ runtime-error store は canonical dotagents config の `collection.enabled: true
|
|
|
191
195
|
場合だけ収集し、既定OFF、network送信は行いません。tag起点CIのnpm provenance(OIDC Trusted
|
|
192
196
|
Publishing)で公開し、GitHub Release が Official MCP Registry を再登録します。
|
|
193
197
|
|
|
194
|
-
**状態:** 開発継続中 · 現行公開版 **v0.
|
|
198
|
+
**状態:** 開発継続中 · 現行公開版 **v0.37.1** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
|
|
195
199
|
|
|
196
200
|
### 更新と巻き戻し
|
|
197
201
|
|
|
@@ -471,6 +475,7 @@ aiterm は同じ核心の洞察——端末を出会いの場にする——を
|
|
|
471
475
|
|
|
472
476
|
`agent_launch({ harness, cwd, trust_project: true })`はpromptなしでも既知のworkspace・project hooks・MCP初期同意を
|
|
473
477
|
進め、入力受付とharness生存を確認して`startup.status="ready"`を返す。指定なしのpromptなし起動は`not_checked`。
|
|
478
|
+
WindowsのCodexもhook確認を認識し、npm shim経由の起動を一つのharnessとして識別する。
|
|
474
479
|
初手の`initial_prompt.status`は`not_requested`/`not_sent`/`submitted_unconfirmed`/`started`を区別する。
|
|
475
480
|
未送信・未確認の失敗もsession付きstructuredContentを保持する。未確認のpromptを再送せず、返ったcursorで観測する。
|
|
476
481
|
|
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ Ubuntu/Debianはsudoとaptでtmuxを準備する。必要な公式package mana
|
|
|
47
47
|
AI別の`integrations`と選択機能の`codex_steer`を持つ。失敗時は`reason_code`を付け、終了コードはreadyなら0、再起動待ちは3、それ以外は2となる。
|
|
48
48
|
|
|
49
49
|
|
|
50
|
-
### Codex DesktopへSteerを有効にする(macOS)
|
|
50
|
+
### Codex DesktopへSteerを有効にする(macOS・Windows)
|
|
51
51
|
|
|
52
52
|
対話実行の`aiterm-setup`で「Aiterm単品」と「Steer付き」を選べます。無人導入では明示します。
|
|
53
53
|
|
|
@@ -57,14 +57,18 @@ aiterm-setup --json --codex-steer enable
|
|
|
57
57
|
|
|
58
58
|
公式Codex Desktopに同梱されたApp Serverを使い、実行中の親には同じターンへ回答を送り、
|
|
59
59
|
終了後に届いた回答でも同じタスクを自動再開します。App Serverの改造・再ビルド・別配布は不要です。
|
|
60
|
-
|
|
60
|
+
新しいlauncherを設定した場合は`restart_required`(終了コード3)を返します。Codexを完全終了して再起動し、
|
|
61
61
|
`aiterm-setup --codex-steer status`で`ready`を確認してください。接続できない時にキューへ自動退避しません。
|
|
62
62
|
|
|
63
|
-
選択と復元情報は`~/.config/aiterm-mcp/codex-relay
|
|
64
|
-
|
|
63
|
+
選択と復元情報は`~/.config/aiterm-mcp/codex-relay/`へ保存します。macOSはユーザーLaunchAgent、
|
|
64
|
+
Windowsはユーザー環境変数`CODEX_CLI_PATH`で起動設定を維持します。更新時の`aiterm-setup --json`は選択を維持して中継を更新します。
|
|
65
65
|
解除・旧版への巻き戻し前は`aiterm-setup --codex-steer disable`を実行してCodexを再起動してください。
|
|
66
|
-
SteerはmacOS
|
|
67
|
-
Windows
|
|
66
|
+
SteerはmacOS・Windowsの公式Codex Desktopと同梱CLI 0.154以上に対応します。Steer有効時の宛先は中継で起動したDesktopに限ります。
|
|
67
|
+
Windowsでは公式Desktopが展開した実行ファイルを照合し、認証付きのローカル接続へ中継します。
|
|
68
|
+
Aitermのpackageがlauncher・中継・設定・親への配送を含み、gpt-connectorやdotagentsの導入は不要です。
|
|
69
|
+
既存の互換launcherがある場合は、稼働中のDesktop接続を確認した時だけ共有し、その起動設定を変更しません。
|
|
70
|
+
互換性を確認できない設定は理由付きで停止します。WindowsのDesktop更新後はsetupを再実行してください。
|
|
71
|
+
LinuxのSteer付き導入は理由付き`unsupported`を返します。Aiterm単品は従来どおり利用できます。
|
|
68
72
|
|
|
69
73
|
No clone or build is required. Each client launches the published package with:
|
|
70
74
|
|
|
@@ -205,7 +209,7 @@ collection is off by default and performs no network I/O. It ships via
|
|
|
205
209
|
tag-triggered CI with npm provenance (OIDC Trusted Publishing); the GitHub
|
|
206
210
|
Release re-registers the Official MCP Registry entry.
|
|
207
211
|
|
|
208
|
-
**Status:** actively maintained · current public release **v0.
|
|
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).
|
|
209
213
|
|
|
210
214
|
### Update and rollback
|
|
211
215
|
|
|
@@ -504,6 +508,7 @@ displayed token count or null. Callers do not need raw argv or pane-text parsing
|
|
|
504
508
|
consent even without a prompt, then verifies input readiness and harness liveness before returning `startup.status="ready"`.
|
|
505
509
|
A prompt-free launch without this option retains `startup.status="not_checked"`. `initial_prompt.status` distinguishes
|
|
506
510
|
`not_requested`, `not_sent`, `submitted_unconfirmed`, and `started`. Failure responses retain structured session information.
|
|
511
|
+
WindowsのCodexもhook確認を認識し、npm shim経由の起動を一つのharnessとして識別する。
|
|
507
512
|
Do not resend an unconfirmed prompt; observe or wait using its returned cursor.
|
|
508
513
|
|
|
509
514
|
For a live Codex approval, inspect with `agent_approval({ action: "inspect", session_id })`, review `prompt` and `choices`,
|
|
@@ -2,18 +2,25 @@ import { createConnection } from "node:net";
|
|
|
2
2
|
import WebSocket from "ws";
|
|
3
3
|
import { CodexDeliveryError } from "./codex-delivery-error.js";
|
|
4
4
|
import { verifyRelaySocket } from "./codex-relay-config.js";
|
|
5
|
+
import { windowsSocketConnection } from "./windows-codex-connection.js";
|
|
5
6
|
/** 公式受付へ追加接続する。Desktopへの承認要求や通知には応答しない。 */
|
|
6
7
|
export async function withCodexRelay(socketPath, action, timeout = 15_000) {
|
|
8
|
+
let connection;
|
|
7
9
|
try {
|
|
8
|
-
|
|
10
|
+
if (process.platform === "win32")
|
|
11
|
+
connection = windowsSocketConnection(socketPath);
|
|
12
|
+
else {
|
|
13
|
+
verifyRelaySocket(socketPath);
|
|
14
|
+
connection = { url: "ws://localhost/rpc", options: { createConnection: () => createConnection(socketPath) } };
|
|
15
|
+
}
|
|
9
16
|
}
|
|
10
17
|
catch (error) {
|
|
11
18
|
if (error instanceof CodexDeliveryError)
|
|
12
19
|
throw error;
|
|
13
20
|
throw new CodexDeliveryError("CODEX_RELAY_UNAVAILABLE", "公式App Serverのsocketがありません。Codexを再起動してください");
|
|
14
21
|
}
|
|
15
|
-
const socket = new WebSocket(
|
|
16
|
-
|
|
22
|
+
const socket = new WebSocket(connection.url, {
|
|
23
|
+
...connection.options, handshakeTimeout: timeout, perMessageDeflate: false,
|
|
17
24
|
});
|
|
18
25
|
let sequence = 0;
|
|
19
26
|
let failure = null;
|
|
@@ -5,6 +5,7 @@ import { homedir } from "node:os";
|
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { readRuntimeProcesses } from "./process-runtime.js";
|
|
7
7
|
import { CodexDeliveryError } from "./codex-delivery-error.js";
|
|
8
|
+
import { findWindowsParentSocket } from "./windows-codex-connection.js";
|
|
8
9
|
export const relayConfigSchema = z.object({
|
|
9
10
|
schema: z.literal("aiterm.codex-relay.v1"),
|
|
10
11
|
enabled: z.boolean(),
|
|
@@ -49,7 +50,10 @@ export function verifyRelaySocket(socket) {
|
|
|
49
50
|
throw new CodexDeliveryError("CODEX_RELAY_PATH_INVALID", "本人所有の公式socketを確認できません");
|
|
50
51
|
}
|
|
51
52
|
}
|
|
52
|
-
export function parentRelaySocket(config, processes
|
|
53
|
+
export function parentRelaySocket(config, processes, pid = process.pid) {
|
|
54
|
+
if (process.platform === "win32")
|
|
55
|
+
return findWindowsParentSocket(undefined, pid, config.launcher === config.previous_cli_path ? undefined : config.socket_root);
|
|
56
|
+
processes ??= readRuntimeProcesses();
|
|
53
57
|
const rows = new Map(processes.map(row => [row.pid, row]));
|
|
54
58
|
const seen = new Set();
|
|
55
59
|
let current = rows.get(pid)?.parent_pid;
|
package/dist/core.js
CHANGED
|
@@ -1051,7 +1051,7 @@ function decodeActivityCursor(cursor, name) {
|
|
|
1051
1051
|
throw new AitermError("活動観測cursorが対象sessionの形式と一致しません", 2);
|
|
1052
1052
|
return value;
|
|
1053
1053
|
}
|
|
1054
|
-
function selectHarnessProcesses(meta, rows, subtree) {
|
|
1054
|
+
export function selectHarnessProcesses(meta, rows, subtree) {
|
|
1055
1055
|
const matches = (row) => {
|
|
1056
1056
|
const first = /^(?:"([^"]+)"|(\S+))/.exec(row.command);
|
|
1057
1057
|
const executable = path.posix.basename((first?.[1] ?? first?.[2] ?? "").replace(/\\/g, "/"))
|
|
@@ -1065,7 +1065,20 @@ function selectHarnessProcesses(meta, rows, subtree) {
|
|
|
1065
1065
|
// WindowsのMSYS execでnative親子関係が切れる場合も、launch固有引数で相関する。
|
|
1066
1066
|
const correlated = rows.filter(row => matches(row) && row.command.includes(meta.launch_id));
|
|
1067
1067
|
const candidates = correlated.length ? correlated : subtree.filter(matches);
|
|
1068
|
-
const
|
|
1068
|
+
const byPid = new Map(rows.map(row => [row.pid, row]));
|
|
1069
|
+
const candidatePids = new Set(candidates.map(row => row.pid));
|
|
1070
|
+
// npm shimとnative本体の間にNodeなどの非候補processがいても、同じ起動の祖先を辿る。
|
|
1071
|
+
const roots = candidates.filter(row => {
|
|
1072
|
+
const seen = new Set();
|
|
1073
|
+
let parent = byPid.get(row.parent_pid);
|
|
1074
|
+
while (parent && !seen.has(parent.pid)) {
|
|
1075
|
+
if (candidatePids.has(parent.pid))
|
|
1076
|
+
return false;
|
|
1077
|
+
seen.add(parent.pid);
|
|
1078
|
+
parent = byPid.get(parent.parent_pid);
|
|
1079
|
+
}
|
|
1080
|
+
return true;
|
|
1081
|
+
});
|
|
1069
1082
|
return roots;
|
|
1070
1083
|
}
|
|
1071
1084
|
export function observeSession(name, cursor) {
|
package/dist/harnesses/codex.js
CHANGED
|
@@ -386,7 +386,7 @@ export function codexTuiReady(screen) {
|
|
|
386
386
|
return codexFrontend && /(^|\n)[ \t]*[›>](?![ \t]*\d+\.)/.test(screen);
|
|
387
387
|
}
|
|
388
388
|
function currentCodexDialog(screen) {
|
|
389
|
-
const footers = [...screen.matchAll(/Press enter to confirm or esc to cancel|enter to submit\s*\|\s*esc to cancel/gi)];
|
|
389
|
+
const footers = [...screen.matchAll(/Press enter to confirm or esc to (?:cancel|go back)|enter to submit\s*\|\s*esc to cancel/gi)];
|
|
390
390
|
// 過去の完了footerより前の質問・選択番号を、現在のdialogへ持ち込まない。
|
|
391
391
|
const previousFooter = footers.at(-2);
|
|
392
392
|
const current = previousFooter ? screen.slice(previousFooter.index + previousFooter[0].length) : screen;
|
|
@@ -400,7 +400,7 @@ export function codexPaneObservation(screen) {
|
|
|
400
400
|
const tail = screen.split("\n").slice(-24).join("\n");
|
|
401
401
|
// 現在のmodal footerがある時だけ、折返しで上へ出た質問を画面全体から探す。
|
|
402
402
|
const lastComposer = [...tail.matchAll(/(?:^|\n)[ \t]*[›>](?![ \t]*\d+\.)/g)].at(-1)?.index ?? -1;
|
|
403
|
-
const lastDialog = [...tail.matchAll(/Press enter to confirm or esc to cancel|enter to submit\s*\|\s*esc to cancel|Would you like to run the following command\?|Allow the [^\n]+ MCP server to run tool|Hooks need review|Do you trust the contents of this directory|Update available!/gi)].at(-1)?.index ?? -1;
|
|
403
|
+
const lastDialog = [...tail.matchAll(/Press enter to confirm or esc to (?:cancel|go back)|enter to submit\s*\|\s*esc to cancel|Would you like to run the following command\?|Allow the [^\n]+ MCP server to run tool|Hooks need review|Do you trust the contents of this directory|Update available!/gi)].at(-1)?.index ?? -1;
|
|
404
404
|
const modal = lastDialog > lastComposer;
|
|
405
405
|
if (!modal) {
|
|
406
406
|
if (/esc to interrupt/i.test(tail))
|
package/dist/process-runtime.js
CHANGED
|
@@ -44,7 +44,7 @@ export function readRuntimeProcesses() {
|
|
|
44
44
|
"$ErrorActionPreference='Stop'",
|
|
45
45
|
"[Console]::OutputEncoding=[Text.UTF8Encoding]::new($false)",
|
|
46
46
|
"@(Get-CimInstance Win32_Process | Where-Object { $null -ne $_.CreationDate -and $null -ne $_.CommandLine } | ForEach-Object {",
|
|
47
|
-
"[ordered]@{ pid=[int]$_.ProcessId; parent_pid=[int]$_.ParentProcessId; started_identity=$_.CreationDate.ToUniversalTime().ToString('o'); command=$_.CommandLine; cpu_seconds=([double]$_.KernelModeTime+[double]$_.UserModeTime)/10000000 }",
|
|
47
|
+
"[ordered]@{ pid=[int]$_.ProcessId; parent_pid=[int]$_.ParentProcessId; executable=[string]$_.ExecutablePath; started_identity=$_.CreationDate.ToUniversalTime().ToString('o'); command=$_.CommandLine; cpu_seconds=([double]$_.KernelModeTime+[double]$_.UserModeTime)/10000000 }",
|
|
48
48
|
"}) | ConvertTo-Json -Compress",
|
|
49
49
|
].join("\n");
|
|
50
50
|
const result = spawnSync(resolveWindowsPowerShell7(), ["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(script, "utf16le").toString("base64")], {
|
|
@@ -69,6 +69,7 @@ export function readRuntimeProcesses() {
|
|
|
69
69
|
const command = row.command.trim();
|
|
70
70
|
return {
|
|
71
71
|
pid: row.pid, parent_pid: row.parent_pid, process_group_id: null, stopped: null,
|
|
72
|
+
executable: row.executable,
|
|
72
73
|
started_identity: new Date(row.started_identity).toISOString(),
|
|
73
74
|
command, cpu_seconds: row.cpu_seconds, argv_digest: createHash("sha256").update(command).digest("hex"),
|
|
74
75
|
};
|
package/dist/setup-cli.js
CHANGED
|
@@ -18,7 +18,7 @@ if (args.length === 1 && args[0] === "--remove-claude-parent-hooks") {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
else if (args.length === 1 && ["--help", "-h"].includes(args[0])) {
|
|
21
|
-
process.stdout.write("使い方: aiterm-setup [--json] [--codex-steer enable|disable|status]\n依存準備、AIへの登録、MCPと端末の実動作確認を行います。対話実行ではAiterm単品かCodex Desktop Steer付きかを選べます。SteerはmacOS対応で、初回はCodexの再起動が必要です。disableは元の起動設定へ戻し、statusは実際の接続を確認します。--jsonは対話せず、Steerの選択を維持します。\n旧版へ戻す前のClaude専用hook解除: --remove-claude-parent-hooks\n");
|
|
21
|
+
process.stdout.write("使い方: aiterm-setup [--json] [--codex-steer enable|disable|status]\n依存準備、AIへの登録、MCPと端末の実動作確認を行います。対話実行ではAiterm単品かCodex Desktop Steer付きかを選べます。SteerはmacOS・Windows対応で、初回はCodexの再起動が必要です。disableは元の起動設定へ戻し、statusは実際の接続を確認します。--jsonは対話せず、Steerの選択を維持します。\n旧版へ戻す前のClaude専用hook解除: --remove-claude-parent-hooks\n");
|
|
22
22
|
}
|
|
23
23
|
else {
|
|
24
24
|
try {
|
|
@@ -33,7 +33,7 @@ else {
|
|
|
33
33
|
throw new Error("使い方: aiterm-setup [--json] [--codex-steer enable|disable|status]");
|
|
34
34
|
}
|
|
35
35
|
const onlySteer = action === "disable" || action === "status";
|
|
36
|
-
if (!action && !json && process.stdin.isTTY && process.stderr.isTTY && process.platform
|
|
36
|
+
if (!action && !json && process.stdin.isTTY && process.stderr.isTTY && ["darwin", "win32"].includes(process.platform)) {
|
|
37
37
|
const enabled = readRelayConfig()?.enabled ?? false;
|
|
38
38
|
const prompt = createInterface({ input: process.stdin, output: process.stderr });
|
|
39
39
|
try {
|
|
@@ -12,6 +12,7 @@ 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
16
|
function command(executable, args) {
|
|
16
17
|
const result = spawnSync(executable, args, { encoding: "utf8", timeout: 15_000 });
|
|
17
18
|
if (result.error || result.status !== 0)
|
|
@@ -51,7 +52,7 @@ export async function verifyRelayLauncher(launcher) {
|
|
|
51
52
|
const temporary = fs.mkdtempSync(path.join(tmpdir(), "aiterm-relay-setup-"));
|
|
52
53
|
fs.mkdirSync(path.join(temporary, ".codex"));
|
|
53
54
|
const child = spawn(launcher, ["-c", 'cli_auth_credentials_store="file"', "-c", 'mcp_oauth_credentials_store="file"', "app-server"], {
|
|
54
|
-
env: { ...process.env, HOME: temporary, CODEX_HOME: path.join(temporary, ".codex") }, stdio: ["pipe", "pipe", "ignore"],
|
|
55
|
+
env: { ...process.env, HOME: temporary, CODEX_HOME: path.join(temporary, ".codex") }, stdio: ["pipe", "pipe", "ignore"], windowsHide: true,
|
|
55
56
|
});
|
|
56
57
|
const reader = createInterface({ input: child.stdout });
|
|
57
58
|
const exited = new Promise(resolve => child.once("close", resolve));
|
|
@@ -121,6 +122,8 @@ function writeConfig(directory, config) {
|
|
|
121
122
|
fs.renameSync(temporary, path.join(directory, "config.json"));
|
|
122
123
|
}
|
|
123
124
|
export async function configureCodexSteer(action = "status", overrides = {}) {
|
|
125
|
+
if ((overrides.platform ?? process.platform) === "win32")
|
|
126
|
+
return configureWindowsCodexSteer(action, overrides);
|
|
124
127
|
const runtime = {
|
|
125
128
|
platform: process.platform, directory: relayConfigDirectory(), socket_root: `/tmp/aiterm-codex-${process.getuid?.() ?? 0}`,
|
|
126
129
|
node: process.execPath, relay: fileURLToPath(new URL("./codex-stdio-relay.js", import.meta.url)),
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// gpt-connectorのWindows実装をMIT条件で移植。実行時の外部製品依存は持たない。
|
|
2
|
+
import { lstatSync, readFileSync } from "node:fs";
|
|
3
|
+
import { basename, dirname, isAbsolute, join, relative } from "node:path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { windowsPowerShellSync, quotePowerShell } from "./windows-codex-state.js";
|
|
6
|
+
import { CodexDeliveryError } from "./codex-delivery-error.js";
|
|
7
|
+
import { readRuntimeProcesses } from "./process-runtime.js";
|
|
8
|
+
export const windowsRelaySchema = z.object({
|
|
9
|
+
schema: z.enum(["aiterm.windows-relay.v1", "gpt-connector.windows-relay.v1"]),
|
|
10
|
+
serverPid: z.number().int().positive(), serverStarted: z.string(), binary: z.string(),
|
|
11
|
+
endpoint: z.string().regex(/^ws:\/\/127\.0\.0\.1:[1-9][0-9]{0,4}$/),
|
|
12
|
+
}).strict();
|
|
13
|
+
const processSchema = z.object({ pid: z.number(), parent_pid: z.number(), command: z.string(), executable: z.string(), started: z.string() });
|
|
14
|
+
export function readWindowsProcesses() {
|
|
15
|
+
return readRuntimeProcesses().map(row => ({ pid: row.pid, parent_pid: row.parent_pid, command: row.command, executable: row.executable ?? "", started: row.started_identity }));
|
|
16
|
+
}
|
|
17
|
+
function invalid(message) { throw new CodexDeliveryError("CODEX_RELAY_CONNECTION_INVALID", message); }
|
|
18
|
+
export function readWindowsRelay(file, processes = readWindowsProcesses()) {
|
|
19
|
+
if (!isAbsolute(file) || lstatSync(file).isSymbolicLink() || !lstatSync(file).isFile())
|
|
20
|
+
invalid("WindowsのSteer接続記録が不正です。");
|
|
21
|
+
const root = dirname(file);
|
|
22
|
+
if (lstatSync(root).isSymbolicLink() || !lstatSync(root).isDirectory())
|
|
23
|
+
invalid("WindowsのSteer保存先が不正です。");
|
|
24
|
+
// 接続情報は外部入力。読む前に所有者とACLを検証し、権限を修復して成功扱いしない。
|
|
25
|
+
windowsPowerShellSync(`
|
|
26
|
+
$sid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value
|
|
27
|
+
foreach ($entry in @(${quotePowerShell(root)}, ${quotePowerShell(file)}, ${quotePowerShell(join(root, "token"))})) {
|
|
28
|
+
$acl = Get-Acl -LiteralPath $entry
|
|
29
|
+
if ($acl.GetOwner([System.Security.Principal.SecurityIdentifier]).Value -notin @($sid, 'S-1-5-32-544')) { throw 'Steer保存先の所有者が一致しません' }
|
|
30
|
+
foreach ($rule in $acl.Access) {
|
|
31
|
+
$identity = $rule.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]).Value
|
|
32
|
+
if ($rule.AccessControlType -eq 'Allow' -and $identity -notin @($sid, 'S-1-5-18', 'S-1-5-32-544')) { throw 'Steer保存先が他userへ公開されています' }
|
|
33
|
+
}
|
|
34
|
+
}`);
|
|
35
|
+
const record = windowsRelaySchema.parse(JSON.parse(readFileSync(file, "utf8")));
|
|
36
|
+
const server = processes.find(row => row.pid === record.serverPid);
|
|
37
|
+
if (!server || Date.parse(server.started) !== Date.parse(record.serverStarted) || server.executable.toLowerCase() !== record.binary.toLowerCase()
|
|
38
|
+
|| !server.command.includes(record.endpoint) || !server.command.includes(join(root, "token")))
|
|
39
|
+
invalid("Windowsの親Codex processを照合できません。");
|
|
40
|
+
return record;
|
|
41
|
+
}
|
|
42
|
+
export function findWindowsParentSocket(rows = readWindowsProcesses(), callerPid = process.pid, root) {
|
|
43
|
+
const parents = new Map(rows.map(row => [row.pid, row]));
|
|
44
|
+
const seen = new Set();
|
|
45
|
+
let pid = parents.get(callerPid)?.parent_pid;
|
|
46
|
+
while (pid && !seen.has(pid)) {
|
|
47
|
+
const row = parents.get(pid);
|
|
48
|
+
// 自分が起動した公式CLIの引数から接続先を取得する。MCPへの環境変数継承を前提にしない。
|
|
49
|
+
const token = row && basename(row.executable).toLowerCase() === "codex.exe"
|
|
50
|
+
? /(?:^|\s)--ws-token-file\s+(?:"([^"]+)"|(\S+))/.exec(row.command) : null;
|
|
51
|
+
const tokenFile = token?.[1] ?? token?.[2];
|
|
52
|
+
if (tokenFile && isAbsolute(tokenFile) && basename(tokenFile) === "token") {
|
|
53
|
+
const file = join(dirname(tokenFile), "connection.json");
|
|
54
|
+
if (root) {
|
|
55
|
+
const location = relative(root, file);
|
|
56
|
+
if (isAbsolute(location) || location === ".." || location.startsWith("..\\"))
|
|
57
|
+
invalid("親CodexはAitermのSteer起動設定を使っていません。Codexを完全再起動してください。");
|
|
58
|
+
}
|
|
59
|
+
const record = readWindowsRelay(file, rows);
|
|
60
|
+
if (record.serverPid !== pid)
|
|
61
|
+
invalid("Steer接続記録と親Codexが一致しません。");
|
|
62
|
+
return file;
|
|
63
|
+
}
|
|
64
|
+
seen.add(pid);
|
|
65
|
+
pid = row?.parent_pid;
|
|
66
|
+
}
|
|
67
|
+
invalid("親CodexのSteer接続がありません。aiterm-setup --codex-steer enableを実行し、Codexを再起動してください。");
|
|
68
|
+
}
|
|
69
|
+
export function windowsSocketConnection(file) {
|
|
70
|
+
const record = readWindowsRelay(file);
|
|
71
|
+
const tokenFile = join(dirname(file), "token");
|
|
72
|
+
if (lstatSync(tokenFile).isSymbolicLink() || !lstatSync(tokenFile).isFile())
|
|
73
|
+
invalid("Steer認証fileが不正です。");
|
|
74
|
+
const token = readFileSync(tokenFile, "utf8");
|
|
75
|
+
if (!/^[a-f0-9]{64}$/.test(token))
|
|
76
|
+
invalid("Steer認証情報が不正です。");
|
|
77
|
+
return { url: record.endpoint, options: { headers: { Authorization: `Bearer ${token}` } } };
|
|
78
|
+
}
|
|
79
|
+
/** 登録済みlauncherとDesktopの子孫にある、対応する公式接続だけを共有できる。 */
|
|
80
|
+
export function windowsDesktopRelay(launcher, rows = readWindowsProcesses()) {
|
|
81
|
+
const parents = new Map(rows.map(row => [row.pid, row]));
|
|
82
|
+
for (const server of rows) {
|
|
83
|
+
if (basename(server.executable).toLowerCase() !== "codex.exe" || !server.command.includes("--ws-token-file"))
|
|
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)
|
|
100
|
+
continue;
|
|
101
|
+
const token = /(?:^|\s)--ws-token-file\s+(?:"([^"]+)"|(\S+))/.exec(server.command);
|
|
102
|
+
const tokenFile = token?.[1] ?? token?.[2];
|
|
103
|
+
if (!tokenFile || !isAbsolute(tokenFile) || basename(tokenFile) !== "token")
|
|
104
|
+
continue;
|
|
105
|
+
const file = join(dirname(tokenFile), "connection.json");
|
|
106
|
+
if (readWindowsRelay(file, rows).serverPid !== server.pid)
|
|
107
|
+
invalid("Steer接続記録と親Codexが一致しません。");
|
|
108
|
+
return file;
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// gpt-connectorのWindows実装をMIT条件で移植。実行時の外部製品依存は持たない。
|
|
2
|
+
// Windowsの起動processと認証付きloopback接続を所有する。JSON-RPC本文は変更しない。
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { createServer } from "node:net";
|
|
5
|
+
import { createInterface } from "node:readline";
|
|
6
|
+
import { once } from "node:events";
|
|
7
|
+
import { randomBytes, randomUUID } from "node:crypto";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { writeFileSync, rmSync } from "node:fs";
|
|
11
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
12
|
+
import WebSocket from "ws";
|
|
13
|
+
import { ensurePrivateDirectory, makeFilePrivate } from "./windows-codex-state.js";
|
|
14
|
+
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
|
+
}
|
|
56
|
+
const directory = join(root, randomUUID());
|
|
57
|
+
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
|
+
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;
|
|
98
|
+
}
|
|
99
|
+
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();
|
|
122
|
+
});
|
|
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
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
stop();
|
|
138
|
+
await exited;
|
|
139
|
+
diagnostics.close();
|
|
140
|
+
rmSync(directory, { recursive: true, force: true });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
144
|
+
const [binary, root, ...args] = process.argv.slice(2);
|
|
145
|
+
try {
|
|
146
|
+
if (!binary || !root)
|
|
147
|
+
throw new Error("Windows中継の起動指定がありません");
|
|
148
|
+
process.exitCode = await runWindowsCodexRelay(binary, root, args);
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
process.stderr.write(`aiterm-mcp: ${error instanceof Error ? error.message : "Windows中継が失敗しました"}\n`);
|
|
152
|
+
process.exitCode = 1;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// gpt-connectorのWindows実装をMIT条件で移植。実行時の外部製品依存は持たない。
|
|
2
|
+
// Windows Desktop用の実行file、ユーザー環境変数、親processの接続確認を所有する。
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync, renameSync, readdirSync, rmSync } from "node:fs";
|
|
4
|
+
import { join, dirname } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import { randomUUID, createHash } from "node:crypto";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { relayConfigDirectory, readRelayConfig } from "./codex-relay-config.js";
|
|
10
|
+
import { SetupError } from "./setup-platform.js";
|
|
11
|
+
import { verifyRelayLauncher } from "./setup-codex-relay.js";
|
|
12
|
+
import { withCodexRelay } from "./codex-relay-client.js";
|
|
13
|
+
import { windowsPowerShellSync, quotePowerShell } from "./windows-codex-state.js";
|
|
14
|
+
import { ensurePrivateDirectory, makeFilePrivate } from "./windows-codex-state.js";
|
|
15
|
+
import { readWindowsRelay, windowsDesktopRelay } from "./windows-codex-connection.js";
|
|
16
|
+
export function findWindowsCodexCache(resources, cache) {
|
|
17
|
+
// Desktopが展開した4実行fileを配布元と照合する。展開・更新はDesktop自身が所有する。
|
|
18
|
+
const names = ["codex.exe", "codex-code-mode-host.exe", "codex-windows-sandbox-setup.exe", "codex-command-runner.exe"];
|
|
19
|
+
const digest = (file) => createHash("sha256").update(readFileSync(file)).digest("hex");
|
|
20
|
+
const expected = names.map(name => digest(join(resources, name)));
|
|
21
|
+
const found = (existsSync(cache) ? readdirSync(cache, { withFileTypes: true }) : [])
|
|
22
|
+
.filter(entry => entry.isDirectory() && !entry.isSymbolicLink() && /^[0-9a-f]{16}$/.test(entry.name))
|
|
23
|
+
.filter(entry => names.every((name, index) => {
|
|
24
|
+
const file = join(cache, entry.name, name);
|
|
25
|
+
return existsSync(file) && digest(file) === expected[index];
|
|
26
|
+
}));
|
|
27
|
+
if (found.length !== 1)
|
|
28
|
+
throw new SetupError("codex_desktop_runtime_unavailable", "現在のCodex Desktopが展開した実行fileを特定できません。公式Desktopを起動してからsetupを再実行してください。");
|
|
29
|
+
return join(cache, found[0].name, "codex.exe");
|
|
30
|
+
}
|
|
31
|
+
export function findWindowsCodexBinary() {
|
|
32
|
+
const resources = windowsPowerShellSync(`
|
|
33
|
+
$packages = @(Get-AppxPackage -Name OpenAI.Codex)
|
|
34
|
+
if ($packages.Count -ne 1) { throw '公式Codex Desktopを一つに特定できません' }
|
|
35
|
+
Join-Path $packages[0].InstallLocation 'app/resources'`);
|
|
36
|
+
const value = findWindowsCodexCache(resources, join(process.env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local"), "OpenAI", "Codex", "bin"));
|
|
37
|
+
const result = spawnSync(value, ["--version"], { encoding: "utf8", windowsHide: true, timeout: 10_000 });
|
|
38
|
+
if (result.error || result.status !== 0)
|
|
39
|
+
throw new SetupError("codex_binary_unavailable", "公式Codex Desktopの実行fileを起動できません。");
|
|
40
|
+
const match = /codex-cli (\d+)\.(\d+)\.(\d+)/.exec(result.stdout ?? "");
|
|
41
|
+
if (result.status !== 0 || !match || (Number(match[1]) === 0 && Number(match[2]) < 154))
|
|
42
|
+
throw new SetupError("codex_version_unsupported", "公式Codex Desktopの対応CLIを確認できません。");
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
function getGui(key) {
|
|
46
|
+
return windowsPowerShellSync(`[Environment]::GetEnvironmentVariable(${quotePowerShell(key)}, 'User')`) || null;
|
|
47
|
+
}
|
|
48
|
+
function setGui(key, value) {
|
|
49
|
+
windowsPowerShellSync(`
|
|
50
|
+
[Environment]::SetEnvironmentVariable(${quotePowerShell(key)}, ${value === null ? "$null" : quotePowerShell(value)}, 'User')
|
|
51
|
+
Add-Type -TypeDefinition @'
|
|
52
|
+
using System;
|
|
53
|
+
using System.Runtime.InteropServices;
|
|
54
|
+
public static class AitermEnvironment {
|
|
55
|
+
[DllImport("user32.dll", CharSet=CharSet.Unicode)]
|
|
56
|
+
public static extern IntPtr SendMessageTimeout(IntPtr window, uint message, UIntPtr wparam, string value, uint flags, uint timeout, out UIntPtr result);
|
|
57
|
+
}
|
|
58
|
+
'@
|
|
59
|
+
$result = [UIntPtr]::Zero
|
|
60
|
+
[void][AitermEnvironment]::SendMessageTimeout([IntPtr]0xffff, 0x1a, [UIntPtr]::Zero, 'Environment', 2, 5000, [ref]$result)
|
|
61
|
+
`, 10_000);
|
|
62
|
+
}
|
|
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
|
+
}
|
|
105
|
+
export function buildWindowsLauncher(directory, source) {
|
|
106
|
+
const digest = createHash("sha256").update(source).digest("hex").slice(0, 16);
|
|
107
|
+
const launcher = join(directory, `codex-${digest}.exe`);
|
|
108
|
+
const sourceFile = join(directory, `codex-${digest}.cs`);
|
|
109
|
+
if (existsSync(launcher) && existsSync(sourceFile) && readFileSync(sourceFile, "utf8") === source)
|
|
110
|
+
return launcher;
|
|
111
|
+
const compiler = join(process.env.SystemRoot ?? "C:\\Windows", "Microsoft.NET", "Framework64", "v4.0.30319", "csc.exe");
|
|
112
|
+
if (!existsSync(compiler))
|
|
113
|
+
throw new SetupError("codex_launcher_compiler_unavailable", "Windows標準.NET Frameworkのコンパイラが見つかりません。");
|
|
114
|
+
writeFileSync(sourceFile, source);
|
|
115
|
+
makeFilePrivate(sourceFile);
|
|
116
|
+
const result = spawnSync(compiler, ["/nologo", "/target:exe", `/out:${launcher}`, sourceFile], { encoding: "utf8", windowsHide: true, timeout: 20_000 });
|
|
117
|
+
if (result.status !== 0)
|
|
118
|
+
throw new SetupError("codex_launcher_build_failed", "Windows用のCodex起動fileを作成できません。");
|
|
119
|
+
makeFilePrivate(launcher);
|
|
120
|
+
return launcher;
|
|
121
|
+
}
|
|
122
|
+
async function live(config) {
|
|
123
|
+
const file = windowsDesktopRelay(config.launcher);
|
|
124
|
+
if (!file)
|
|
125
|
+
return false;
|
|
126
|
+
await withCodexRelay(file, request => request("thread/loaded/list", { limit: 1 }));
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
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" };
|
|
190
|
+
}
|
|
191
|
+
function save(directory, config) {
|
|
192
|
+
const file = join(directory, `${randomUUID()}.tmp`);
|
|
193
|
+
try {
|
|
194
|
+
writeFileSync(file, JSON.stringify(config, null, 2) + "\n", { flag: "wx" });
|
|
195
|
+
makeFilePrivate(file);
|
|
196
|
+
renameSync(file, join(directory, "config.json"));
|
|
197
|
+
}
|
|
198
|
+
finally {
|
|
199
|
+
rmSync(file, { force: true });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// WindowsのSteer保存先とPowerShell境界。接続記録の読取ではACLを変更しない。
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { mkdirSync, lstatSync } from "node:fs";
|
|
4
|
+
import { isAbsolute } from "node:path";
|
|
5
|
+
import { resolveWindowsPowerShell7 } from "./windows-powershell.js";
|
|
6
|
+
import { CodexDeliveryError } from "./codex-delivery-error.js";
|
|
7
|
+
export const quotePowerShell = (value) => `'${value.replace(/'/g, "''")}'`;
|
|
8
|
+
export function windowsPowerShellSync(script, timeout = 15_000) {
|
|
9
|
+
const source = "$ErrorActionPreference = 'Stop'\n[Console]::OutputEncoding = [Text.UTF8Encoding]::new($false)\n" + script;
|
|
10
|
+
try {
|
|
11
|
+
return execFileSync(resolveWindowsPowerShell7(), ["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand",
|
|
12
|
+
Buffer.from(source, "utf16le").toString("base64")], {
|
|
13
|
+
encoding: "utf8", windowsHide: true, timeout, maxBuffer: 16 * 1024 * 1024, stdio: ["ignore", "pipe", "pipe"],
|
|
14
|
+
}).trim();
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
throw new CodexDeliveryError("CODEX_WINDOWS_OPERATION_FAILED", "WindowsのSteer設定・接続情報を確認できません");
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function makePrivate(target, directory) {
|
|
21
|
+
const stat = lstatSync(target);
|
|
22
|
+
if (!isAbsolute(target) || stat.isSymbolicLink() || (directory ? !stat.isDirectory() : !stat.isFile())) {
|
|
23
|
+
throw new CodexDeliveryError("CODEX_RELAY_PATH_INVALID", "Steer保存先が不正です");
|
|
24
|
+
}
|
|
25
|
+
windowsPowerShellSync(`
|
|
26
|
+
$target = ${quotePowerShell(target)}
|
|
27
|
+
$sid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
|
|
28
|
+
$previous = Get-Acl -LiteralPath $target
|
|
29
|
+
if ($previous.GetOwner([System.Security.Principal.SecurityIdentifier]).Value -notin @($sid.Value, 'S-1-5-32-544')) { throw '所有者が一致しません' }
|
|
30
|
+
$acl = [System.Security.AccessControl.${directory ? "DirectorySecurity" : "FileSecurity"}]::new()
|
|
31
|
+
$acl.SetOwner($sid)
|
|
32
|
+
$acl.SetAccessRuleProtection($true, $false)
|
|
33
|
+
$rule = [System.Security.AccessControl.FileSystemAccessRule]::new($sid, 'FullControl', '${directory ? "ContainerInherit, ObjectInherit" : "None"}', 'None', 'Allow')
|
|
34
|
+
$acl.AddAccessRule($rule)
|
|
35
|
+
Set-Acl -LiteralPath $target -AclObject $acl
|
|
36
|
+
$actual = Get-Acl -LiteralPath $target
|
|
37
|
+
if ($actual.GetOwner([System.Security.Principal.SecurityIdentifier]).Value -ne $sid.Value -or !$actual.AreAccessRulesProtected) { throw 'ACLの適用を確認できません' }
|
|
38
|
+
`);
|
|
39
|
+
}
|
|
40
|
+
export function ensurePrivateDirectory(directory) {
|
|
41
|
+
mkdirSync(directory, { recursive: true });
|
|
42
|
+
makePrivate(directory, true);
|
|
43
|
+
}
|
|
44
|
+
export function makeFilePrivate(file) { makePrivate(file, false); }
|
package/docs/DESIGN.md
CHANGED
|
@@ -34,6 +34,8 @@ psmuxが出力した余分な環境値を返さない。通常PTYとagentへ`AIT
|
|
|
34
34
|
|
|
35
35
|
`pty_observe`は存在、pane/harnessの生存、画面状態と理由、native process identityを分ける。
|
|
36
36
|
PIDは開始識別子・argv digestと組にし、paneとharnessを同一視しない。特定できないidentityはnull。
|
|
37
|
+
同じlaunchに属するnpm shimとnative本体は、中間の非候補processも含めた祖先関係から一つの起動として扱う。
|
|
38
|
+
祖先を共有しない候補は別々に残し、複数候補を一つと推測しない。
|
|
37
39
|
POSIXの停止状態はOSのprocess表から取得し、SIGSTOP中は残画面より優先して`blocked/harness_stopped`を返す。
|
|
38
40
|
画面本文とargv本文は返さず、活動cursorには画面digestとprocess別CPUだけを持たせる。
|
|
39
41
|
初回とpane再作成後の差分はnull。区間中にprocessが消えた時は観測できたCPU増分だけを返し、
|
|
@@ -82,7 +84,18 @@ macOSのユーザーLaunchAgentはログイン時に同じ起動設定を適用
|
|
|
82
84
|
保存した元の`CODEX_CLI_PATH`へ戻す。他から変更された設定は上書きしない。
|
|
83
85
|
`status=ready`は公式binary・Desktopの直接子・同じsocketのRPC応答を確認した場合だけ返す。
|
|
84
86
|
初回の設定保存後は`restart_required`であり、Desktopの完全終了・再起動を要する。
|
|
85
|
-
|
|
87
|
+
WindowsはAitermのnative launcherからNode中継と公式Desktop CLIを起動し、通常の親子関係を保つ。
|
|
88
|
+
Windows標準の.NET Frameworkでlauncherを作り、引数とstdioをそのまま中継する。
|
|
89
|
+
公式Desktopが展開した4実行ファイルを配布元のSHA-256と照合し、AitermがCodexをコピー・再配布しない。
|
|
90
|
+
公式CLIの認証付きloopback WebSocketを使い、本人専用ACLのtokenと接続記録をAitermのsessions下へ置く。
|
|
91
|
+
Windowsのprocess情報はprocess-runtimeが所有し、接続はPID・開始時刻・実行ファイル・token引数で照合する。
|
|
92
|
+
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単独導入の前提にならない。
|
|
98
|
+
LinuxのSteer選択はunsupportedとする。通常の単品導入・queue配送は全対応OSで維持する。
|
|
86
99
|
|
|
87
100
|
単品導入のqueue配送は親の実行中turnを中断せず、親がidleになった後に処理される。
|
|
88
101
|
`parent_delivery`が配送IDと状態を返し、自動配送時の`wait_process`/`wait_command`はnullになる。
|
package/docs/RELEASE.md
CHANGED
|
@@ -39,8 +39,11 @@ Codex Steerを変更した場合は、公式バイナリを指定した`test/cod
|
|
|
39
39
|
実行中Steer・終了後再開・承認応答・stdio終了を先に確認する。公開packageの
|
|
40
40
|
`aiterm-setup --json --codex-steer enable`で選択導入し、`restart_required`なら人がDesktopを完全再起動する。
|
|
41
41
|
再起動後の`aiterm-setup --codex-steer status`が`ready`であることと、通常の親からの子の回答配送を確認する。
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
接続があるだけで成功とせず、公式binaryとDesktopの親子関係まで確認する。
|
|
43
|
+
Windowsは`AITERM_TEST_CODEX_BINARY`に公式Desktopの実行fileを指定し、
|
|
44
|
+
`test/windows-codex.test.mjs`で単独launcher、ACL、親識別、既存接続共有、設定解除も確認する。
|
|
45
|
+
同じ環境変数で`test/codex-relay-official.test.mjs`の実行中Steer・終了後再開・承認中継・EOFを確認できる。
|
|
46
|
+
Linuxの未対応Steer選択が理由付きで停止することもCIで確認する。
|
|
44
47
|
|
|
45
48
|
setupを変更した場合は、公開packageのglobal install後に`aiterm-setup --json`を実行し、
|
|
46
49
|
端末実行と検出した各AIの登録結果を確認する。初回と再実行は一時設定領域でも試験し、所有外の設定保持を確かめる。
|
|
@@ -70,7 +73,8 @@ aiterm-setup --json
|
|
|
70
73
|
巻き戻しは既知の正常版を指定する。
|
|
71
74
|
|
|
72
75
|
Steerを持たない旧版へ戻す時は、install前に`aiterm-setup --codex-steer disable`を実行する。
|
|
73
|
-
|
|
76
|
+
macOSは専用LaunchAgentの解除、Windowsは元のユーザー環境変数の復元を確認し、MCP clientを再起動する。
|
|
77
|
+
互換launcherを共有していた場合はAitermのSteer選択だけを解除し、所有外の起動設定を保持する。
|
|
74
78
|
回答record schemaは従来と共通であり、Steer送信済みrecordの`queued_submission_id`はnullとなる。
|
|
75
79
|
|
|
76
80
|
Claude親配送hookのない旧版へ戻す場合は、旧版のinstall前に`aiterm-setup --remove-claude-parent-hooks`を
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aiterm-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.37.1",
|
|
4
4
|
"mcpName": "io.github.kitepon/aiterm-mcp",
|
|
5
5
|
"description": "Persistent terminal MCP with one harness-based launcher for Claude Code, Codex CLI, Grok CLI, and Cursor Agent CLI, plus durable PTYs for SSH, containers, and REPLs.",
|
|
6
6
|
"keywords": [
|