aiterm-mcp 0.27.1 → 0.27.6

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.
@@ -0,0 +1,178 @@
1
+ // エージェント CLI(claude / codex / grok)・Throughline・pane shell(Windows は Git Bash)の
2
+ // 実行ファイルをどう見つけ、どう起動するかの所有者(OS 分岐の所有者。tmux とは独立)。
3
+ import { spawnSync } from "node:child_process";
4
+ import * as fs from "node:fs";
5
+ import * as path from "node:path";
6
+ import * as os from "node:os";
7
+ import { AitermError } from "./errors.js";
8
+ import { isWin } from "./tmux-runtime.js";
9
+ export function isUsableExecutableFile(candidate) {
10
+ try {
11
+ if (!fs.statSync(candidate).isFile())
12
+ return false;
13
+ if (isWin)
14
+ return /\.(?:exe|cmd|bat|com)$/i.test(candidate);
15
+ fs.accessSync(candidate, fs.constants.X_OK);
16
+ return true;
17
+ }
18
+ catch {
19
+ return false;
20
+ }
21
+ }
22
+ export function isWindowsDrivePath(candidate) {
23
+ return /^[A-Za-z]:[\\/]/.test(candidate);
24
+ }
25
+ export function isWindowsNativeExecutable(candidate) {
26
+ return isWindowsDrivePath(candidate) && /\.(?:exe|com|cmd|bat)$/i.test(candidate);
27
+ }
28
+ // Windows の bin 受入: native 実行ファイル(.exe/.cmd/.bat)に加え、pane shell
29
+ // (Git Bash)が shebang で実行できる script も実在すれば受け入れる。旧 WSL 側
30
+ // バイナリ検査への黙ったフォールバックは廃止(別 HOME・別 auth の subagent を
31
+ // 作るため)。native 実行ファイルの強制が要る vendor(grok/composer の実効
32
+ // sandbox 等)は openAgent 側の専用ゲートが明示エラーで担う。
33
+ export function isUsableAgentExecutableFile(candidate) {
34
+ if (!isWin)
35
+ return isUsableExecutableFile(candidate);
36
+ if (isWindowsNativeExecutable(candidate))
37
+ return isUsableExecutableFile(candidate);
38
+ try {
39
+ return fs.statSync(candidate).isFile();
40
+ }
41
+ catch {
42
+ return false;
43
+ }
44
+ }
45
+ export function resolveWindowsCodexShim(kind, candidate) {
46
+ if (!isWin || kind !== "codex" || !/\.(?:cmd|bat)$/i.test(candidate))
47
+ return candidate;
48
+ const packageRoot = path.join(path.dirname(candidate), "node_modules", "@openai", "codex", "node_modules", "@openai");
49
+ try {
50
+ for (const platformPackage of fs.readdirSync(packageRoot).filter((name) => name.startsWith("codex-win32-"))) {
51
+ const vendorRoot = path.join(packageRoot, platformPackage, "vendor");
52
+ for (const target of fs.readdirSync(vendorRoot)) {
53
+ const executable = path.join(vendorRoot, target, "bin", "codex.exe");
54
+ if (isUsableExecutableFile(executable))
55
+ return executable;
56
+ }
57
+ }
58
+ }
59
+ catch {
60
+ /* 下の明示エラーへ */
61
+ }
62
+ throw new AitermError(`CODEX_BIN のnpm shimからWindows native codex.exeを解決できません: ${candidate}。` +
63
+ "@openai/codexを再インストールするか、CODEX_BINへcodex.exeを指定してください", 2);
64
+ }
65
+ // pane 内の POSIX shell(Windows は Git Bash 等)へ渡す bin パス。Git Bash は
66
+ // バックスラッシュをエスケープとして解釈しうるため、ドライブパスは forward slash 形へ。
67
+ export function agentBinForPaneShell(bin) {
68
+ return isWin && isWindowsDrivePath(bin) ? bin.replace(/\\/g, "/") : bin;
69
+ }
70
+ export function spawnAgentControlCommand(bin, args, _cwd, options) {
71
+ // 受入(isUsableAgentExecutableFile)が「使える」と判定した bin は、control command
72
+ //(`claude auth status --json`/`grok models`)でも同じく実行できなければならない。
73
+ if (isWin && !/\.(?:exe|com)$/i.test(bin)) {
74
+ if (/\.(?:cmd|bat)$/i.test(bin)) {
75
+ // Node は CVE-2024-27980 対処以降、.cmd/.bat の直接 spawn を EINVAL で拒否する。
76
+ // 受入が .cmd/.bat を許す以上、control 経路は shell 経由で実行する(args は固定語彙)。
77
+ return spawnSync(bin, args, { ...options, shell: true });
78
+ }
79
+ // 受入は pane shell(Git Bash)が shebang で実行できる script も許す。Windows の
80
+ // CreateProcess は shebang を解さないため、control command も同じ Git Bash で実行する。
81
+ // これを直接 spawn すると常に失敗し、「受入が通した bin で起動が必ず失敗する」矛盾になる。
82
+ return spawnSync(resolveWinPaneShell("bash"), [bin, ...args], options);
83
+ }
84
+ return spawnSync(bin, args, options);
85
+ }
86
+ export function resolveWinPaneShell(shell) {
87
+ if (!isWin || shell !== "bash")
88
+ return shell;
89
+ const fromEnv = process.env.AITERM_BASH;
90
+ if (fromEnv) {
91
+ if (isUsableExecutableFile(fromEnv))
92
+ return fromEnv;
93
+ throw new AitermError(`AITERM_BASH に指定された bash が存在しません: ${fromEnv}`, 2);
94
+ }
95
+ const candidates = [
96
+ path.join(process.env.ProgramFiles ?? "C:\\Program Files", "Git", "bin", "bash.exe"),
97
+ path.join(process.env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)", "Git", "bin", "bash.exe"),
98
+ ];
99
+ const gitPath = spawnSync("where.exe", ["git.exe"], { encoding: "utf8", timeout: 5000 })
100
+ .stdout?.split(/\r?\n/)
101
+ .find(Boolean);
102
+ if (gitPath)
103
+ candidates.push(path.join(path.dirname(path.dirname(gitPath)), "bin", "bash.exe"));
104
+ for (const cand of candidates) {
105
+ if (isUsableExecutableFile(cand))
106
+ return cand;
107
+ }
108
+ throw new AitermError("Git Bash が見つかりません。Windows ネイティブの pane shell には Git for Windows の bash.exe が必要です" +
109
+ "(System32 の bash.exe は WSL launcher のため使いません)。Git for Windows を導入するか、" +
110
+ "AITERM_BASH に bash.exe のパスを指定してください。", 2);
111
+ }
112
+ export function resolveAgentBin(kind) {
113
+ const home = process.env.HOME ?? os.homedir();
114
+ const [envVar, rel, name] = kind === "claude"
115
+ ? ["CLAUDE_BIN", [".local", "bin", "claude"], "claude"]
116
+ : kind === "codex"
117
+ ? ["CODEX_BIN", [".local", "bin", "codex"], "codex"]
118
+ : ["GROK_BIN", [".grok", "bin", isWin ? "grok.exe" : "grok"], "grok"];
119
+ const fromEnv = process.env[envVar];
120
+ if (fromEnv) {
121
+ // 明示指定 env は実在を検証する。存在しないパスを黙って返すと、session を作って
122
+ // `'/typo' ...` を送信し bash が command not found を出すだけで openAgent は「起動した」と
123
+ // 偽成功を返す(既定パス/PATH 経路は検証するのに env だけ無検証だった非対称の解消・A3)。
124
+ if (isUsableAgentExecutableFile(fromEnv))
125
+ return resolveWindowsCodexShim(kind, fromEnv);
126
+ throw new AitermError(`${envVar} に指定された ${name} が存在しません: ${fromEnv}`, 2);
127
+ }
128
+ const cand = path.join(home, ...rel);
129
+ if (isUsableAgentExecutableFile(cand))
130
+ return resolveWindowsCodexShim(kind, cand);
131
+ const w = spawnSync(isWin ? "where" : "which", [name], {
132
+ encoding: "utf8",
133
+ timeout: 5000,
134
+ });
135
+ if (w.status === 0 && (w.stdout ?? "").trim()) {
136
+ const found = w.stdout.trim().split(/\r?\n/).filter(Boolean);
137
+ const ordered = isWin
138
+ ? [...found.filter((p) => /\.(?:exe|com|cmd|bat)$/i.test(p)), ...found.filter((p) => !/\.(?:exe|com|cmd|bat)$/i.test(p))]
139
+ : found;
140
+ for (const resolved of ordered) {
141
+ if (isUsableAgentExecutableFile(resolved))
142
+ return resolveWindowsCodexShim(kind, resolved);
143
+ }
144
+ }
145
+ return null;
146
+ }
147
+ export function resolveThroughlineBin() {
148
+ const fromEnv = process.env.THROUGHLINE_BIN;
149
+ if (fromEnv)
150
+ return isUsableExecutableFile(fromEnv) ? fromEnv : null;
151
+ const resolved = spawnSync(isWin ? "where" : "which", ["throughline"], {
152
+ encoding: "utf8",
153
+ }).stdout?.split(/\r?\n/).find(Boolean) ?? null;
154
+ return resolved && isUsableExecutableFile(resolved) ? resolved : null;
155
+ }
156
+ export function runThroughlineHandoffContext(bin, sessionId) {
157
+ if (isWin && /\.(?:cmd|bat)$/i.test(bin)) {
158
+ const ps1 = path.join(path.dirname(bin), `${path.basename(bin, path.extname(bin))}.ps1`);
159
+ if (fs.existsSync(ps1)) {
160
+ return spawnSync("powershell.exe", [
161
+ "-NoLogo",
162
+ "-NoProfile",
163
+ "-NonInteractive",
164
+ "-ExecutionPolicy",
165
+ "Bypass",
166
+ "-File",
167
+ ps1,
168
+ "handoff-context",
169
+ "--session",
170
+ sessionId,
171
+ "--json",
172
+ ], { encoding: "utf8" });
173
+ }
174
+ }
175
+ return spawnSync(bin, ["handoff-context", "--session", sessionId, "--json"], {
176
+ encoding: "utf8",
177
+ });
178
+ }
@@ -2,12 +2,12 @@
2
2
  // aiterm-wait — agent turn 完了eventの純リーダー観測CLI。
3
3
  // 完了/timeout/close を1行のJSON receiptで返してexitする。lock・PTY・dispatch状態には一切触れない。
4
4
  // 親AIホストのバックグラウンドタスクとして起動し、exitを「観測終了の通知」として使う。
5
- // exit≠完了: exit code は outcome を映す(0=done / 5=running=未完了 / 3=timeout=未完了 / 4=closed / 1=エラー)。
5
+ // exit≠完了: exit code は outcome を映す(0=done / 5=running=未完了 / 3=timeout=未完了 / 4=closed / 6=rate_limited=vendor利用上限 / 1=エラー)。
6
6
  // --timeout 0 は待たずに一度だけ観測する照会で、未完了は running(timeout と混同させない)。
7
7
  // receipt の outcome が正で、done 以外は未完了。timeout の既定は core の DEFAULT_AGENT_DONE_TIMEOUT(600秒)。
8
8
  import { fileURLToPath } from "node:url";
9
9
  import * as fs from "node:fs";
10
- import { AitermError, observeAgentDone } from "./core.js";
10
+ import { AitermError, detectAgentRateLimit, observeAgentDone } from "./core.js";
11
11
  const SESSION_RE = /^[A-Za-z0-9_-]{1,64}$/;
12
12
  const OPERATION_RE = /^sha256:[0-9a-f]{64}$/;
13
13
  const USAGE = "usage: aiterm-wait --session <name> [--cursor <event_cursor>] [--operation sha256:<64hex>] [--timeout <sec>]" +
@@ -74,14 +74,39 @@ const OUTCOME_EXIT_CODES = {
74
74
  running: 5,
75
75
  timeout: 3,
76
76
  closed: 4,
77
+ rate_limited: 6,
77
78
  };
78
79
  export async function main(argv) {
79
80
  const cmd = parseArgs(argv);
80
- const result = await observeAgentDone(cmd.session, {
81
- operation_id: cmd.operationId,
82
- timeout: cmd.timeout,
83
- cursor: cmd.cursor,
84
- });
81
+ let result;
82
+ try {
83
+ result = await observeAgentDone(cmd.session, {
84
+ operation_id: cmd.operationId,
85
+ timeout: cmd.timeout,
86
+ cursor: cmd.cursor,
87
+ });
88
+ }
89
+ catch (e) {
90
+ // Grok は利用上限に達すると auth 正本を自壊させることがあり(2026-08-22 実測)、
91
+ // metadata 検証が「未ログイン」という嘘の原因で落ちる。pane log に上限バナーが
92
+ // あるなら、誤診でなく rate_limited として typed に回答する。
93
+ if (e instanceof AitermError && /Grok 認証正本/.test(e.message)) {
94
+ const limited = detectAgentRateLimit("grok", cmd.session);
95
+ if (limited) {
96
+ emit({
97
+ ok: false,
98
+ code: "AGENT_RATE_LIMITED",
99
+ vendor: "grok",
100
+ rate_limit: limited,
101
+ message: "Grok が利用上限に達している。別のベンダーのモデルに切り替えてください。" +
102
+ "(認証正本の不在は上限時に CLI が auth を自壊させた痕跡で、未ログインの誤診に注意)",
103
+ });
104
+ process.exitCode = 6;
105
+ return;
106
+ }
107
+ }
108
+ throw e;
109
+ }
85
110
  emit(result);
86
111
  process.exitCode = OUTCOME_EXIT_CODES[result.outcome];
87
112
  }