aiterm-mcp 0.37.0 → 0.37.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
@@ -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増分だけを返し、
@@ -66,7 +68,7 @@ Cursorのsubmitはadapterがextended keyboard protocolのEnterへ変換し、呼
66
68
  宛先はCodexのMCP handshakeと各要求の`_meta.threadId`から取得する。modelが指定したIDや環境変数で代用しない。
67
69
  単品導入の`src/codex-parent-receiver.ts`は同じ`CODEX_HOME`の公式app-serverへstdioで接続し、
68
70
  `thread/read`と`thread/queue/list`で宛先を確認してから`thread/queue/add`へ本文をJSONで渡す。
69
- Steerを選択したmacOS Desktopでは、MCP processの祖先にある公式App Serverの本人所有socketへ接続する。
71
+ Steerを選択したmacOS・Windows Desktopでは、MCP processの祖先にある公式App Serverの本人専用接続へ接続する。
70
72
  `thread/loaded/list`と`thread/read`で同じ親を確認し、公式`turn/start`へ一度だけ送る。
71
73
  公式の`start_or_steer_turn`が実行中なら同じturnへ追加入力し、終了後なら同じtaskの次turnを開始する。
72
74
  状態を読んでから送信方法を選ぶraceや再送は作らない。model・権限のoverrideも渡さない。
@@ -82,17 +84,20 @@ macOSのユーザーLaunchAgentはログイン時に同じ起動設定を適用
82
84
  保存した元の`CODEX_CLI_PATH`へ戻す。他から変更された設定は上書きしない。
83
85
  `status=ready`は公式binary・Desktopの直接子・同じsocketのRPC応答を確認した場合だけ返す。
84
86
  初回の設定保存後は`restart_required`であり、Desktopの完全終了・再起動を要する。
85
- WindowsはAitermのnative launcherからNode中継と公式Desktop CLIを起動し、通常の親子関係を保つ。
86
- 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は維持する。
87
92
  公式Desktopが展開した4実行ファイルを配布元のSHA-256と照合し、AitermがCodexをコピー・再配布しない。
88
93
  公式CLIの認証付きloopback WebSocketを使い、本人専用ACLのtokenと接続記録をAitermのsessions下へ置く。
89
94
  Windowsのprocess情報はprocess-runtimeが所有し、接続はPID・開始時刻・実行ファイル・token引数で照合する。
90
95
  MCP processの祖先から親を選び、同じloaded taskへ送る。開始時刻は小数桁の表記差を正規化して比較する。
91
- stdio EOFで自分が起動した公式serverを停止し、接続記録を削除する。
92
- 既存の互換launcherは公式serverからlauncherとDesktopへの祖先関係、ACL、RPCが成立した場合だけ共有する。
93
- 対応する接続記録はaiterm.windows-relay.v1とgpt-connector.windows-relay.v1で、後者もAiterm内で解釈する。
94
- 共有時はlauncherとprevious_cli_pathを同値で記録し、enableとdisableで所有外の起動設定を変更しない。
95
- 接続共有は任意であり、他製品のコード・設定・コマンドはAiterm単独導入の前提にならない。
96
+ stdio EOFで自分が起動した公式serverを停止し、接続記録を削除する。native launcherはJob Objectで
97
+ 所有するprocessだけを終了し、公式serverの終了確認後に戻る。
98
+ 両OSとも有効化前に元の起動設定を保存し、Aitermのlauncherを設定する。解除は元の値へ戻す。
99
+ 旧Windows版で既存launcherを共有した設定は読取り可能だが、次のenableでAitermのlauncherへ移行する。
100
+ 起動・配送・設定・診断は他製品のコード、設定、コマンドへ依存しない。
96
101
  LinuxのSteer選択はunsupportedとする。通常の単品導入・queue配送は全対応OSで維持する。
97
102
 
98
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.0",
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": [