aiterm-mcp 0.8.0 → 0.9.0
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/README.ja.md +13 -12
- package/README.md +13 -12
- package/dist/codex-stop-hook.js +124 -0
- package/dist/core.js +927 -4
- package/dist/grok-stop-hook.js +120 -0
- package/dist/index.js +28 -3
- package/package.json +1 -1
package/dist/core.js
CHANGED
|
@@ -12,6 +12,7 @@ import * as fs from "node:fs";
|
|
|
12
12
|
import * as path from "node:path";
|
|
13
13
|
import * as os from "node:os";
|
|
14
14
|
import { createHash, randomBytes } from "node:crypto";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
15
16
|
import * as rtk from "./rtk.js";
|
|
16
17
|
// Windows ネイティブには tmux が無い。その場合だけ全 tmux 呼び出しを WSL 経由へ橋渡しする
|
|
17
18
|
// (POSIX = Linux/WSL2/macOS は従来どおり tmux を直接叩く)。
|
|
@@ -39,6 +40,17 @@ const NON_POSIX_MARK_SHELLS = new Set(["fish", "csh", "tcsh"]);
|
|
|
39
40
|
// エコーは rc=%d(リテラル)。数字アンカーでエコーに免疫化し、部分一致による早期誤完了を防ぐ(B1)。
|
|
40
41
|
// send の printf 書式(`rc=%d`)と対で保守すること。
|
|
41
42
|
const MARK_DONE_RE = /<<<AITERM_DONE rc=[0-9]+>>>/;
|
|
43
|
+
const LAUNCH_ID_RE = /^[0-9a-f]{32}$/;
|
|
44
|
+
const AGENT_DONE_POLL_MS = 100;
|
|
45
|
+
const AGENT_DONE_SETTLE_MIN_MS = 250;
|
|
46
|
+
const AGENT_DONE_SCREEN_SETTLE_POLL_MS = 100;
|
|
47
|
+
const AGENT_DONE_SCREEN_SETTLE_MAX_POLLS = 5;
|
|
48
|
+
const AGENT_DONE_SCREEN_SETTLE_MIN_SAMPLES = 3;
|
|
49
|
+
const AGENT_SUBMIT_DELAY_MS = 250;
|
|
50
|
+
const AGENT_EVENT_MAX_BYTES = 1024 * 1024;
|
|
51
|
+
const AGENT_TUI_READY_TIMEOUT_MS = 30_000;
|
|
52
|
+
const AGENT_TUI_READY_POLL_MS = 500;
|
|
53
|
+
const AGENT_TUI_READY_LINES = 45;
|
|
42
54
|
// 出力削減(RTK の CAP 思想を移植)
|
|
43
55
|
const MAX_LINES_BEFORE_ELIDE = 60;
|
|
44
56
|
const HEAD_LINES = 30;
|
|
@@ -193,6 +205,141 @@ function assertSessionName(name) {
|
|
|
193
205
|
if (!/^[A-Za-z0-9_-]{1,64}$/.test(name))
|
|
194
206
|
throw new AitermError(`session 名は英数字と _ - のみ・64文字以内にしてください: ${JSON.stringify(name)}`, 2);
|
|
195
207
|
}
|
|
208
|
+
function currentUid() {
|
|
209
|
+
if (typeof process.getuid !== "function") {
|
|
210
|
+
throw new AitermError("agent_done は POSIX/macOS/Linux のみ対応です(native Windows は未対応)", 2);
|
|
211
|
+
}
|
|
212
|
+
return process.getuid();
|
|
213
|
+
}
|
|
214
|
+
function runtimeStateBase() {
|
|
215
|
+
const xdg = process.env.XDG_RUNTIME_DIR;
|
|
216
|
+
if (xdg) {
|
|
217
|
+
try {
|
|
218
|
+
if (fs.statSync(xdg).isDirectory())
|
|
219
|
+
return xdg;
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
/* XDG_RUNTIME_DIR が壊れている CI/非 login 環境では os.tmpdir() に戻す */
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return os.tmpdir();
|
|
226
|
+
}
|
|
227
|
+
function stateRoot() {
|
|
228
|
+
const uid = currentUid();
|
|
229
|
+
const base = runtimeStateBase();
|
|
230
|
+
return path.join(base, `aiterm-mcp-${uid}`);
|
|
231
|
+
}
|
|
232
|
+
function ensureSecureStateRoot() {
|
|
233
|
+
const root = stateRoot();
|
|
234
|
+
fs.mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
235
|
+
const st = fs.lstatSync(root);
|
|
236
|
+
if (!st.isDirectory() || st.isSymbolicLink()) {
|
|
237
|
+
throw new AitermError(`agent state root が安全な directory ではありません: ${root}`, 2);
|
|
238
|
+
}
|
|
239
|
+
if (st.uid !== currentUid()) {
|
|
240
|
+
throw new AitermError(`agent state root の owner が現在ユーザーではありません: ${root}`, 2);
|
|
241
|
+
}
|
|
242
|
+
if ((st.mode & 0o077) !== 0) {
|
|
243
|
+
fs.chmodSync(root, 0o700);
|
|
244
|
+
}
|
|
245
|
+
const agents = path.join(root, "agents");
|
|
246
|
+
fs.mkdirSync(agents, { recursive: true, mode: 0o700 });
|
|
247
|
+
const ast = fs.lstatSync(agents);
|
|
248
|
+
if (!ast.isDirectory() || ast.isSymbolicLink() || ast.uid !== currentUid()) {
|
|
249
|
+
throw new AitermError(`agent state dir が安全な directory ではありません: ${agents}`, 2);
|
|
250
|
+
}
|
|
251
|
+
if ((ast.mode & 0o077) !== 0)
|
|
252
|
+
fs.chmodSync(agents, 0o700);
|
|
253
|
+
return root;
|
|
254
|
+
}
|
|
255
|
+
function agentsDir() {
|
|
256
|
+
return path.join(ensureSecureStateRoot(), "agents");
|
|
257
|
+
}
|
|
258
|
+
function agentEventPath(name, launchId) {
|
|
259
|
+
assertSessionName(name);
|
|
260
|
+
if (!LAUNCH_ID_RE.test(launchId))
|
|
261
|
+
throw new AitermError(`launch_id が不正です: ${launchId}`, 2);
|
|
262
|
+
return path.join(agentsDir(), `${name}.${launchId}.events.jsonl`);
|
|
263
|
+
}
|
|
264
|
+
function agentMetadataPath(name, launchId) {
|
|
265
|
+
assertSessionName(name);
|
|
266
|
+
if (!LAUNCH_ID_RE.test(launchId))
|
|
267
|
+
throw new AitermError(`launch_id が不正です: ${launchId}`, 2);
|
|
268
|
+
return path.join(agentsDir(), `${name}.${launchId}.agent.json`);
|
|
269
|
+
}
|
|
270
|
+
function agentWaitLockPath(name, launchId) {
|
|
271
|
+
assertSessionName(name);
|
|
272
|
+
if (!LAUNCH_ID_RE.test(launchId))
|
|
273
|
+
throw new AitermError(`launch_id が不正です: ${launchId}`, 2);
|
|
274
|
+
return path.join(agentsDir(), `${name}.${launchId}.wait.lock`);
|
|
275
|
+
}
|
|
276
|
+
function agentManagedCodexHomePath(name, launchId) {
|
|
277
|
+
assertSessionName(name);
|
|
278
|
+
if (!LAUNCH_ID_RE.test(launchId))
|
|
279
|
+
throw new AitermError(`launch_id が不正です: ${launchId}`, 2);
|
|
280
|
+
return path.join(agentsDir(), `${name}.${launchId}.codex-home`);
|
|
281
|
+
}
|
|
282
|
+
function agentManagedGrokHomePath(name, launchId) {
|
|
283
|
+
assertSessionName(name);
|
|
284
|
+
if (!LAUNCH_ID_RE.test(launchId))
|
|
285
|
+
throw new AitermError(`launch_id が不正です: ${launchId}`, 2);
|
|
286
|
+
return path.join(agentsDir(), `${name}.${launchId}.grok-home`);
|
|
287
|
+
}
|
|
288
|
+
function agentManagedGrokUserHomePath(name, launchId) {
|
|
289
|
+
assertSessionName(name);
|
|
290
|
+
if (!LAUNCH_ID_RE.test(launchId))
|
|
291
|
+
throw new AitermError(`launch_id が不正です: ${launchId}`, 2);
|
|
292
|
+
return path.join(agentsDir(), `${name}.${launchId}.home`);
|
|
293
|
+
}
|
|
294
|
+
function existingAgentsDir() {
|
|
295
|
+
if (typeof process.getuid !== "function")
|
|
296
|
+
return null;
|
|
297
|
+
const root = path.join(runtimeStateBase(), `aiterm-mcp-${process.getuid()}`);
|
|
298
|
+
const dir = path.join(root, "agents");
|
|
299
|
+
try {
|
|
300
|
+
const rst = fs.lstatSync(root);
|
|
301
|
+
if (!rst.isDirectory() || rst.isSymbolicLink() || rst.uid !== process.getuid())
|
|
302
|
+
return null;
|
|
303
|
+
if ((rst.mode & 0o077) !== 0)
|
|
304
|
+
fs.chmodSync(root, 0o700);
|
|
305
|
+
const st = fs.lstatSync(dir);
|
|
306
|
+
if (!st.isDirectory() || st.isSymbolicLink() || st.uid !== process.getuid())
|
|
307
|
+
return null;
|
|
308
|
+
if ((st.mode & 0o077) !== 0)
|
|
309
|
+
fs.chmodSync(dir, 0o700);
|
|
310
|
+
return dir;
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function cleanupAgentState(name) {
|
|
317
|
+
assertSessionName(name);
|
|
318
|
+
const dir = existingAgentsDir();
|
|
319
|
+
if (!dir)
|
|
320
|
+
return;
|
|
321
|
+
const prefix = `${name}.`;
|
|
322
|
+
try {
|
|
323
|
+
for (const f of fs.readdirSync(dir)) {
|
|
324
|
+
if (!f.startsWith(prefix))
|
|
325
|
+
continue;
|
|
326
|
+
const p = path.join(dir, f);
|
|
327
|
+
try {
|
|
328
|
+
if (f.endsWith(".agent.json") || f.endsWith(".events.jsonl") || f.endsWith(".wait.lock"))
|
|
329
|
+
fs.unlinkSync(p);
|
|
330
|
+
else if (f.endsWith(".codex-home") || f.endsWith(".grok-home") || f.endsWith(".home")) {
|
|
331
|
+
fs.rmSync(p, { recursive: true, force: true });
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
/* stale agent state cleanup is best-effort */
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
/* stale agent state cleanup is best-effort */
|
|
341
|
+
}
|
|
342
|
+
}
|
|
196
343
|
function logpath(name) {
|
|
197
344
|
return path.join(SOCKDIR, name + ".log");
|
|
198
345
|
}
|
|
@@ -538,6 +685,7 @@ export function openSession(name, shell = "bash") {
|
|
|
538
685
|
/* noop */
|
|
539
686
|
}
|
|
540
687
|
}
|
|
688
|
+
cleanupAgentState(nm);
|
|
541
689
|
// pipe-pane の引数は tmux 内部の /bin/sh -c で再解釈される(argv ではない)。パスは単一引用符で包み、
|
|
542
690
|
// パス自身の ' は '\'' イディオムでエスケープする(名前は検証済みだが、Windows ユーザー名 O'Brien 等が
|
|
543
691
|
// 一時パスに ' を持ち込み redirect を壊すのを防ぐ。空白対策も兼ねる)。Windows は WSL から見える /mnt/c 形へ。
|
|
@@ -764,6 +912,9 @@ export function listSessions() {
|
|
|
764
912
|
}
|
|
765
913
|
export function closeSession(name) {
|
|
766
914
|
assertSessionName(name);
|
|
915
|
+
if (agentWaitLocks.has(name)) {
|
|
916
|
+
throw new AitermError(`agent session '${name}' は agent_done 待機中のため close できません`, 2);
|
|
917
|
+
}
|
|
767
918
|
tmux("kill-session", "-t", name);
|
|
768
919
|
for (const p of [logpath(name), offsetpath(name), lastcmdpath(name), markpath(name)]) {
|
|
769
920
|
try {
|
|
@@ -773,9 +924,13 @@ export function closeSession(name) {
|
|
|
773
924
|
/* noop */
|
|
774
925
|
}
|
|
775
926
|
}
|
|
927
|
+
cleanupAgentState(name);
|
|
776
928
|
return `closed ${name}`;
|
|
777
929
|
}
|
|
778
930
|
export function killAll() {
|
|
931
|
+
if (agentWaitLocks.size > 0) {
|
|
932
|
+
throw new AitermError(`agent_done 待機中の session があるため killAll できません: ${Array.from(agentWaitLocks).join(",")}`, 2);
|
|
933
|
+
}
|
|
779
934
|
tmux("kill-server");
|
|
780
935
|
// B9: SOCKDIR 内の .log/.offset/.lastcmd/.mark 残骸も掃除する(残すと B5 の stale-log 復活の温床)。
|
|
781
936
|
try {
|
|
@@ -793,8 +948,731 @@ export function killAll() {
|
|
|
793
948
|
catch {
|
|
794
949
|
/* SOCKDIR 不在等は無視 */
|
|
795
950
|
}
|
|
951
|
+
const adir = existingAgentsDir();
|
|
952
|
+
if (adir) {
|
|
953
|
+
try {
|
|
954
|
+
for (const f of fs.readdirSync(adir)) {
|
|
955
|
+
if (f.endsWith(".agent.json") ||
|
|
956
|
+
f.endsWith(".events.jsonl") ||
|
|
957
|
+
f.endsWith(".wait.lock") ||
|
|
958
|
+
f.endsWith(".codex-home") ||
|
|
959
|
+
f.endsWith(".grok-home") ||
|
|
960
|
+
f.endsWith(".home")) {
|
|
961
|
+
try {
|
|
962
|
+
fs.rmSync(path.join(adir, f), { recursive: true, force: true });
|
|
963
|
+
}
|
|
964
|
+
catch {
|
|
965
|
+
/* noop */
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
catch {
|
|
971
|
+
/* agent state dir 不在等は無視 */
|
|
972
|
+
}
|
|
973
|
+
}
|
|
796
974
|
return "killed all sessions on this socket";
|
|
797
975
|
}
|
|
976
|
+
const DEFAULT_AGENT_DONE_TIMEOUT = 600;
|
|
977
|
+
const agentWaitLocks = new Set();
|
|
978
|
+
function codexHookScriptPath() {
|
|
979
|
+
return path.join(path.dirname(fileURLToPath(import.meta.url)), "codex-stop-hook.js");
|
|
980
|
+
}
|
|
981
|
+
function grokHookScriptPath() {
|
|
982
|
+
return path.join(path.dirname(fileURLToPath(import.meta.url)), "grok-stop-hook.js");
|
|
983
|
+
}
|
|
984
|
+
function safeStatSize(p) {
|
|
985
|
+
try {
|
|
986
|
+
return fs.statSync(p).size;
|
|
987
|
+
}
|
|
988
|
+
catch {
|
|
989
|
+
return 0;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
function readFileRange(p, from, to) {
|
|
993
|
+
const len = Math.max(0, to - from);
|
|
994
|
+
if (len === 0)
|
|
995
|
+
return Buffer.alloc(0);
|
|
996
|
+
let fd;
|
|
997
|
+
try {
|
|
998
|
+
fd = fs.openSync(p, "r");
|
|
999
|
+
const buf = Buffer.alloc(len);
|
|
1000
|
+
const n = fs.readSync(fd, buf, 0, len, from);
|
|
1001
|
+
return n === len ? buf : buf.subarray(0, Math.max(0, n));
|
|
1002
|
+
}
|
|
1003
|
+
catch {
|
|
1004
|
+
return Buffer.alloc(0);
|
|
1005
|
+
}
|
|
1006
|
+
finally {
|
|
1007
|
+
if (fd !== undefined) {
|
|
1008
|
+
try {
|
|
1009
|
+
fs.closeSync(fd);
|
|
1010
|
+
}
|
|
1011
|
+
catch {
|
|
1012
|
+
/* noop */
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
function writeJson0600(p, v) {
|
|
1018
|
+
fs.writeFileSync(p, JSON.stringify(v, null, 2) + "\n", { mode: 0o600 });
|
|
1019
|
+
try {
|
|
1020
|
+
fs.chmodSync(p, 0o600);
|
|
1021
|
+
}
|
|
1022
|
+
catch {
|
|
1023
|
+
/* noop */
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
function writeText0600(p, text) {
|
|
1027
|
+
fs.writeFileSync(p, text, { mode: 0o600 });
|
|
1028
|
+
try {
|
|
1029
|
+
fs.chmodSync(p, 0o600);
|
|
1030
|
+
}
|
|
1031
|
+
catch {
|
|
1032
|
+
/* noop */
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
function createEmpty0600NoFollow(p) {
|
|
1036
|
+
const nofollow = fs.constants.O_NOFOLLOW ?? 0;
|
|
1037
|
+
const fd = fs.openSync(p, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY | nofollow, 0o600);
|
|
1038
|
+
fs.closeSync(fd);
|
|
1039
|
+
}
|
|
1040
|
+
function realCodexHome() {
|
|
1041
|
+
return process.env.CODEX_HOME || path.join(process.env.HOME ?? os.homedir(), ".codex");
|
|
1042
|
+
}
|
|
1043
|
+
function createManagedCodexHome(name, launchId) {
|
|
1044
|
+
const srcHome = realCodexHome();
|
|
1045
|
+
let srcSt;
|
|
1046
|
+
try {
|
|
1047
|
+
srcSt = fs.statSync(srcHome);
|
|
1048
|
+
}
|
|
1049
|
+
catch {
|
|
1050
|
+
throw new AitermError(`Codex home が見つかりません: ${srcHome}`, 2);
|
|
1051
|
+
}
|
|
1052
|
+
if (!srcSt.isDirectory())
|
|
1053
|
+
throw new AitermError(`Codex home が directory ではありません: ${srcHome}`, 2);
|
|
1054
|
+
const managedHome = agentManagedCodexHomePath(name, launchId);
|
|
1055
|
+
fs.mkdirSync(managedHome, { recursive: false, mode: 0o700 });
|
|
1056
|
+
fs.chmodSync(managedHome, 0o700);
|
|
1057
|
+
let authLinked = false;
|
|
1058
|
+
for (const entry of fs.readdirSync(srcHome)) {
|
|
1059
|
+
if (entry === "hooks.json" || entry.startsWith("hooks.json."))
|
|
1060
|
+
continue;
|
|
1061
|
+
const src = path.join(srcHome, entry);
|
|
1062
|
+
const dst = path.join(managedHome, entry);
|
|
1063
|
+
if (entry === "auth.json") {
|
|
1064
|
+
const st = fs.statSync(src);
|
|
1065
|
+
if (!st.isFile())
|
|
1066
|
+
throw new AitermError(`Codex auth.json が通常ファイルではありません: ${src}`, 2);
|
|
1067
|
+
fs.symlinkSync(src, dst);
|
|
1068
|
+
authLinked = true;
|
|
1069
|
+
}
|
|
1070
|
+
else if (entry === "config.toml") {
|
|
1071
|
+
const st = fs.statSync(src);
|
|
1072
|
+
if (st.isFile()) {
|
|
1073
|
+
fs.copyFileSync(src, dst);
|
|
1074
|
+
fs.chmodSync(dst, 0o600);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
else {
|
|
1078
|
+
fs.symlinkSync(src, dst);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
if (!authLinked)
|
|
1082
|
+
throw new AitermError(`Codex auth.json が見つかりません。先に codex login が必要です: ${srcHome}`, 2);
|
|
1083
|
+
const hookScript = codexHookScriptPath();
|
|
1084
|
+
if (!fs.existsSync(hookScript)) {
|
|
1085
|
+
throw new AitermError(`Codex Stop hook wrapper が見つかりません。npm run build を実行してください: ${hookScript}`, 2);
|
|
1086
|
+
}
|
|
1087
|
+
writeJson0600(path.join(managedHome, "hooks.json"), {
|
|
1088
|
+
hooks: {
|
|
1089
|
+
Stop: [
|
|
1090
|
+
{
|
|
1091
|
+
hooks: [
|
|
1092
|
+
{
|
|
1093
|
+
type: "command",
|
|
1094
|
+
command: `${shq(process.execPath)} ${shq(hookScript)}`,
|
|
1095
|
+
timeoutSec: 10,
|
|
1096
|
+
async: false,
|
|
1097
|
+
statusMessage: null,
|
|
1098
|
+
},
|
|
1099
|
+
],
|
|
1100
|
+
},
|
|
1101
|
+
],
|
|
1102
|
+
},
|
|
1103
|
+
});
|
|
1104
|
+
return managedHome;
|
|
1105
|
+
}
|
|
1106
|
+
function realGrokHome() {
|
|
1107
|
+
return process.env.GROK_HOME || path.join(process.env.HOME ?? os.homedir(), ".grok");
|
|
1108
|
+
}
|
|
1109
|
+
function validateGrokAuthLock(lockPath) {
|
|
1110
|
+
let st;
|
|
1111
|
+
try {
|
|
1112
|
+
st = fs.lstatSync(lockPath);
|
|
1113
|
+
}
|
|
1114
|
+
catch (e) {
|
|
1115
|
+
throw e;
|
|
1116
|
+
}
|
|
1117
|
+
if (st.isSymbolicLink())
|
|
1118
|
+
throw new AitermError(`Grok auth lock が symlink です: ${lockPath}`, 2);
|
|
1119
|
+
if (!st.isFile())
|
|
1120
|
+
throw new AitermError(`Grok auth lock が通常ファイルではありません: ${lockPath}`, 2);
|
|
1121
|
+
if (st.nlink !== 1)
|
|
1122
|
+
throw new AitermError(`Grok auth lock が hard link です: ${lockPath}`, 2);
|
|
1123
|
+
try {
|
|
1124
|
+
fs.chmodSync(lockPath, 0o600);
|
|
1125
|
+
}
|
|
1126
|
+
catch {
|
|
1127
|
+
/* lock file permission tightening is best-effort */
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
function ensureGrokAuthLock(srcHome) {
|
|
1131
|
+
const lockPath = path.join(srcHome, "auth.json.lock");
|
|
1132
|
+
try {
|
|
1133
|
+
validateGrokAuthLock(lockPath);
|
|
1134
|
+
return lockPath;
|
|
1135
|
+
}
|
|
1136
|
+
catch (e) {
|
|
1137
|
+
if (e.code !== "ENOENT")
|
|
1138
|
+
throw e;
|
|
1139
|
+
}
|
|
1140
|
+
try {
|
|
1141
|
+
createEmpty0600NoFollow(lockPath);
|
|
1142
|
+
}
|
|
1143
|
+
catch (e) {
|
|
1144
|
+
if (e.code !== "EEXIST")
|
|
1145
|
+
throw e;
|
|
1146
|
+
}
|
|
1147
|
+
validateGrokAuthLock(lockPath);
|
|
1148
|
+
return lockPath;
|
|
1149
|
+
}
|
|
1150
|
+
function linkGrokOauthFiles(srcHome, grokHome) {
|
|
1151
|
+
const srcAuth = path.join(srcHome, "auth.json");
|
|
1152
|
+
let authExists = false;
|
|
1153
|
+
try {
|
|
1154
|
+
const authSt = fs.statSync(srcAuth);
|
|
1155
|
+
if (!authSt.isFile())
|
|
1156
|
+
throw new AitermError(`Grok auth.json が通常ファイルではありません: ${srcAuth}`, 2);
|
|
1157
|
+
authExists = true;
|
|
1158
|
+
}
|
|
1159
|
+
catch (e) {
|
|
1160
|
+
if (e.code !== "ENOENT")
|
|
1161
|
+
throw e;
|
|
1162
|
+
}
|
|
1163
|
+
if (!authExists) {
|
|
1164
|
+
if (process.env.XAI_API_KEY)
|
|
1165
|
+
return;
|
|
1166
|
+
throw new AitermError(`Grok auth.json が見つかりません。先に grok login が必要です: ${srcHome}`, 2);
|
|
1167
|
+
}
|
|
1168
|
+
const srcLock = ensureGrokAuthLock(srcHome);
|
|
1169
|
+
fs.symlinkSync(srcAuth, path.join(grokHome, "auth.json"));
|
|
1170
|
+
fs.symlinkSync(srcLock, path.join(grokHome, "auth.json.lock"));
|
|
1171
|
+
}
|
|
1172
|
+
function writeManagedGrokConfig(grokHome) {
|
|
1173
|
+
fs.mkdirSync(path.join(grokHome, "hooks"), { recursive: false, mode: 0o700 });
|
|
1174
|
+
writeText0600(path.join(grokHome, "config.toml"), [
|
|
1175
|
+
"[cli]",
|
|
1176
|
+
"auto_update = false",
|
|
1177
|
+
"",
|
|
1178
|
+
"[features]",
|
|
1179
|
+
"remote_fetch = false",
|
|
1180
|
+
"managed_config = false",
|
|
1181
|
+
"",
|
|
1182
|
+
"[compat.claude]",
|
|
1183
|
+
"skills = false",
|
|
1184
|
+
"rules = false",
|
|
1185
|
+
"agents = false",
|
|
1186
|
+
"mcps = false",
|
|
1187
|
+
"hooks = false",
|
|
1188
|
+
"",
|
|
1189
|
+
"[compat.cursor]",
|
|
1190
|
+
"skills = false",
|
|
1191
|
+
"rules = false",
|
|
1192
|
+
"agents = false",
|
|
1193
|
+
"mcps = false",
|
|
1194
|
+
"hooks = false",
|
|
1195
|
+
"",
|
|
1196
|
+
].join("\n"));
|
|
1197
|
+
}
|
|
1198
|
+
function createManagedGrokHome(name, launchId) {
|
|
1199
|
+
const srcHome = realGrokHome();
|
|
1200
|
+
let srcSt;
|
|
1201
|
+
try {
|
|
1202
|
+
srcSt = fs.statSync(srcHome);
|
|
1203
|
+
}
|
|
1204
|
+
catch {
|
|
1205
|
+
throw new AitermError(`Grok home が見つかりません: ${srcHome}`, 2);
|
|
1206
|
+
}
|
|
1207
|
+
if (!srcSt.isDirectory())
|
|
1208
|
+
throw new AitermError(`Grok home が directory ではありません: ${srcHome}`, 2);
|
|
1209
|
+
const grokHome = agentManagedGrokHomePath(name, launchId);
|
|
1210
|
+
const fakeHome = agentManagedGrokUserHomePath(name, launchId);
|
|
1211
|
+
fs.mkdirSync(grokHome, { recursive: false, mode: 0o700 });
|
|
1212
|
+
fs.mkdirSync(fakeHome, { recursive: false, mode: 0o700 });
|
|
1213
|
+
fs.chmodSync(grokHome, 0o700);
|
|
1214
|
+
fs.chmodSync(fakeHome, 0o700);
|
|
1215
|
+
fs.symlinkSync(grokHome, path.join(fakeHome, ".grok"));
|
|
1216
|
+
// OAuth refresh token rotation は auth.json だけでなく vendor lock と同じ実体を共有させる。
|
|
1217
|
+
// per-launch GROK_HOME 隔離は維持し、通常 Grok home の hook/config/session は読ませない。
|
|
1218
|
+
linkGrokOauthFiles(srcHome, grokHome);
|
|
1219
|
+
const hookScript = grokHookScriptPath();
|
|
1220
|
+
if (!fs.existsSync(hookScript)) {
|
|
1221
|
+
throw new AitermError(`Grok Stop hook wrapper が見つかりません。npm run build を実行してください: ${hookScript}`, 2);
|
|
1222
|
+
}
|
|
1223
|
+
writeManagedGrokConfig(grokHome);
|
|
1224
|
+
writeJson0600(path.join(grokHome, "hooks", "aiterm-stop.json"), {
|
|
1225
|
+
hooks: {
|
|
1226
|
+
Stop: [
|
|
1227
|
+
{
|
|
1228
|
+
hooks: [
|
|
1229
|
+
{
|
|
1230
|
+
type: "command",
|
|
1231
|
+
command: `${shq(process.execPath)} ${shq(hookScript)}`,
|
|
1232
|
+
timeout: 10,
|
|
1233
|
+
},
|
|
1234
|
+
],
|
|
1235
|
+
},
|
|
1236
|
+
],
|
|
1237
|
+
},
|
|
1238
|
+
});
|
|
1239
|
+
// Grok 0.2.87 は compat false でも ~/.claude/plugins の hook file を拾う。HOME を一時化して
|
|
1240
|
+
// plugin/hook source を完全に 0 にする。実 HOME は必要なら hook/agent 側で参照できるよう env で渡す。
|
|
1241
|
+
return { grokHome, home: fakeHome };
|
|
1242
|
+
}
|
|
1243
|
+
function writeAgentMetadata(meta) {
|
|
1244
|
+
writeJson0600(agentMetadataPath(meta.aiterm_session, meta.launch_id), meta);
|
|
1245
|
+
}
|
|
1246
|
+
function acquireAgentWaitFileLock(meta) {
|
|
1247
|
+
const p = agentWaitLockPath(meta.aiterm_session, meta.launch_id);
|
|
1248
|
+
const nofollow = fs.constants.O_NOFOLLOW ?? 0;
|
|
1249
|
+
let fd;
|
|
1250
|
+
try {
|
|
1251
|
+
fd = fs.openSync(p, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY | nofollow, 0o600);
|
|
1252
|
+
}
|
|
1253
|
+
catch (e) {
|
|
1254
|
+
if (e.code === "EEXIST") {
|
|
1255
|
+
throw new AitermError(`agent session '${meta.aiterm_session}' は別プロセスの agent_done 待機中です`, 2);
|
|
1256
|
+
}
|
|
1257
|
+
throw e;
|
|
1258
|
+
}
|
|
1259
|
+
try {
|
|
1260
|
+
fs.writeSync(fd, JSON.stringify({ pid: process.pid, at: new Date().toISOString() }) + "\n", undefined, "utf8");
|
|
1261
|
+
}
|
|
1262
|
+
finally {
|
|
1263
|
+
fs.closeSync(fd);
|
|
1264
|
+
}
|
|
1265
|
+
try {
|
|
1266
|
+
fs.chmodSync(p, 0o600);
|
|
1267
|
+
}
|
|
1268
|
+
catch {
|
|
1269
|
+
/* noop */
|
|
1270
|
+
}
|
|
1271
|
+
return () => {
|
|
1272
|
+
try {
|
|
1273
|
+
const st = fs.lstatSync(p);
|
|
1274
|
+
if (st.isFile() && !st.isSymbolicLink() && st.uid === currentUid())
|
|
1275
|
+
fs.unlinkSync(p);
|
|
1276
|
+
}
|
|
1277
|
+
catch {
|
|
1278
|
+
/* noop */
|
|
1279
|
+
}
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
function createCodexAgentMetadata(name, cwd, initialPrompt) {
|
|
1283
|
+
const launchId = randomBytes(16).toString("hex");
|
|
1284
|
+
const eventFile = agentEventPath(name, launchId);
|
|
1285
|
+
createEmpty0600NoFollow(eventFile);
|
|
1286
|
+
const codexHome = createManagedCodexHome(name, launchId);
|
|
1287
|
+
const meta = {
|
|
1288
|
+
kind: "codex",
|
|
1289
|
+
aiterm_session: name,
|
|
1290
|
+
launch_id: launchId,
|
|
1291
|
+
event_file: eventFile,
|
|
1292
|
+
created_at: new Date().toISOString(),
|
|
1293
|
+
cwd,
|
|
1294
|
+
vendor_session_id: null,
|
|
1295
|
+
initial_prompt: initialPrompt,
|
|
1296
|
+
hook_route: "managed_codex_home",
|
|
1297
|
+
node_platform: process.platform,
|
|
1298
|
+
codex_home: codexHome,
|
|
1299
|
+
};
|
|
1300
|
+
writeAgentMetadata(meta);
|
|
1301
|
+
return meta;
|
|
1302
|
+
}
|
|
1303
|
+
function createGrokAgentMetadata(kind, name, cwd, initialPrompt) {
|
|
1304
|
+
const launchId = randomBytes(16).toString("hex");
|
|
1305
|
+
const eventFile = agentEventPath(name, launchId);
|
|
1306
|
+
createEmpty0600NoFollow(eventFile);
|
|
1307
|
+
const managed = createManagedGrokHome(name, launchId);
|
|
1308
|
+
const meta = {
|
|
1309
|
+
kind,
|
|
1310
|
+
aiterm_session: name,
|
|
1311
|
+
launch_id: launchId,
|
|
1312
|
+
event_file: eventFile,
|
|
1313
|
+
created_at: new Date().toISOString(),
|
|
1314
|
+
cwd,
|
|
1315
|
+
vendor_session_id: null,
|
|
1316
|
+
initial_prompt: initialPrompt,
|
|
1317
|
+
hook_route: "managed_grok_home",
|
|
1318
|
+
node_platform: process.platform,
|
|
1319
|
+
grok_home: managed.grokHome,
|
|
1320
|
+
home: managed.home,
|
|
1321
|
+
};
|
|
1322
|
+
writeAgentMetadata(meta);
|
|
1323
|
+
return meta;
|
|
1324
|
+
}
|
|
1325
|
+
function loadAgentMetadata(name) {
|
|
1326
|
+
assertSessionName(name);
|
|
1327
|
+
const dir = agentsDir();
|
|
1328
|
+
const files = fs
|
|
1329
|
+
.readdirSync(dir)
|
|
1330
|
+
.filter((f) => f.startsWith(`${name}.`) && f.endsWith(".agent.json"));
|
|
1331
|
+
if (files.length === 0) {
|
|
1332
|
+
throw new AitermError(`session '${name}' は agent_done 管理セッションではありません。codex_agent(agent_done:true) で起動してください。`, 2);
|
|
1333
|
+
}
|
|
1334
|
+
if (files.length !== 1) {
|
|
1335
|
+
throw new AitermError(`session '${name}' の agent metadata が複数あります。閉じて起動し直してください。`, 2);
|
|
1336
|
+
}
|
|
1337
|
+
let raw;
|
|
1338
|
+
try {
|
|
1339
|
+
raw = JSON.parse(fs.readFileSync(path.join(dir, files[0]), "utf8"));
|
|
1340
|
+
}
|
|
1341
|
+
catch (e) {
|
|
1342
|
+
throw new AitermError(`agent metadata を読めません: ${e.message}`, 2);
|
|
1343
|
+
}
|
|
1344
|
+
const m = raw;
|
|
1345
|
+
if ((m.kind !== "codex" && m.kind !== "grok" && m.kind !== "composer") ||
|
|
1346
|
+
m.aiterm_session !== name ||
|
|
1347
|
+
typeof m.launch_id !== "string" ||
|
|
1348
|
+
!LAUNCH_ID_RE.test(m.launch_id)) {
|
|
1349
|
+
throw new AitermError(`agent metadata が不正です: ${files[0]}`, 2);
|
|
1350
|
+
}
|
|
1351
|
+
const expectedEvent = agentEventPath(name, m.launch_id);
|
|
1352
|
+
if (m.event_file !== expectedEvent) {
|
|
1353
|
+
throw new AitermError("agent metadata の path が現在の secure state root と一致しません", 2);
|
|
1354
|
+
}
|
|
1355
|
+
if (m.kind === "codex") {
|
|
1356
|
+
const expectedHome = agentManagedCodexHomePath(name, m.launch_id);
|
|
1357
|
+
if (m.hook_route !== "managed_codex_home" || m.codex_home !== expectedHome) {
|
|
1358
|
+
throw new AitermError("agent metadata の path が現在の secure state root と一致しません", 2);
|
|
1359
|
+
}
|
|
1360
|
+
return {
|
|
1361
|
+
kind: "codex",
|
|
1362
|
+
aiterm_session: name,
|
|
1363
|
+
launch_id: m.launch_id,
|
|
1364
|
+
event_file: expectedEvent,
|
|
1365
|
+
created_at: typeof m.created_at === "string" ? m.created_at : "",
|
|
1366
|
+
cwd: typeof m.cwd === "string" ? m.cwd : null,
|
|
1367
|
+
vendor_session_id: typeof m.vendor_session_id === "string" ? m.vendor_session_id : null,
|
|
1368
|
+
initial_prompt: m.initial_prompt === true,
|
|
1369
|
+
hook_route: "managed_codex_home",
|
|
1370
|
+
node_platform: process.platform,
|
|
1371
|
+
codex_home: expectedHome,
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
const expectedGrokHome = agentManagedGrokHomePath(name, m.launch_id);
|
|
1375
|
+
const expectedHome = agentManagedGrokUserHomePath(name, m.launch_id);
|
|
1376
|
+
if (m.hook_route !== "managed_grok_home" || m.grok_home !== expectedGrokHome || m.home !== expectedHome) {
|
|
1377
|
+
throw new AitermError("agent metadata の path が現在の secure state root と一致しません", 2);
|
|
1378
|
+
}
|
|
1379
|
+
return {
|
|
1380
|
+
kind: m.kind,
|
|
1381
|
+
aiterm_session: name,
|
|
1382
|
+
launch_id: m.launch_id,
|
|
1383
|
+
event_file: expectedEvent,
|
|
1384
|
+
created_at: typeof m.created_at === "string" ? m.created_at : "",
|
|
1385
|
+
cwd: typeof m.cwd === "string" ? m.cwd : null,
|
|
1386
|
+
vendor_session_id: typeof m.vendor_session_id === "string" ? m.vendor_session_id : null,
|
|
1387
|
+
initial_prompt: m.initial_prompt === true,
|
|
1388
|
+
hook_route: "managed_grok_home",
|
|
1389
|
+
node_platform: process.platform,
|
|
1390
|
+
grok_home: expectedGrokHome,
|
|
1391
|
+
home: expectedHome,
|
|
1392
|
+
};
|
|
1393
|
+
}
|
|
1394
|
+
function parseAgentDoneEvent(line, meta) {
|
|
1395
|
+
let obj;
|
|
1396
|
+
try {
|
|
1397
|
+
obj = JSON.parse(line);
|
|
1398
|
+
}
|
|
1399
|
+
catch {
|
|
1400
|
+
return { event: null, malformed: true };
|
|
1401
|
+
}
|
|
1402
|
+
const ev = obj;
|
|
1403
|
+
if (ev.type !== "agent_done" ||
|
|
1404
|
+
ev.vendor !== meta.kind ||
|
|
1405
|
+
ev.aiterm_session !== meta.aiterm_session ||
|
|
1406
|
+
ev.launch_id !== meta.launch_id ||
|
|
1407
|
+
ev.done_status !== "turn_done") {
|
|
1408
|
+
return { event: null, malformed: false };
|
|
1409
|
+
}
|
|
1410
|
+
if (meta.vendor_session_id && ev.vendor_session_id !== meta.vendor_session_id) {
|
|
1411
|
+
return { event: null, malformed: false };
|
|
1412
|
+
}
|
|
1413
|
+
return {
|
|
1414
|
+
event: {
|
|
1415
|
+
type: "agent_done",
|
|
1416
|
+
vendor: meta.kind,
|
|
1417
|
+
aiterm_session: meta.aiterm_session,
|
|
1418
|
+
launch_id: meta.launch_id,
|
|
1419
|
+
vendor_session_id: typeof ev.vendor_session_id === "string" ? ev.vendor_session_id : null,
|
|
1420
|
+
turn_id: typeof ev.turn_id === "string" ? ev.turn_id : null,
|
|
1421
|
+
reason: typeof ev.reason === "string" ? ev.reason : "Stop",
|
|
1422
|
+
done_status: "turn_done",
|
|
1423
|
+
stop_hook_active: !!ev.stop_hook_active,
|
|
1424
|
+
at: typeof ev.at === "string" ? ev.at : new Date().toISOString(),
|
|
1425
|
+
},
|
|
1426
|
+
malformed: false,
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
function scanAgentDoneLines(lines, meta) {
|
|
1430
|
+
let malformedEvents = 0;
|
|
1431
|
+
let candidate = null;
|
|
1432
|
+
for (const line of lines) {
|
|
1433
|
+
if (!line.trim())
|
|
1434
|
+
continue;
|
|
1435
|
+
if (Buffer.byteLength(line, "utf8") > 64 * 1024) {
|
|
1436
|
+
malformedEvents++;
|
|
1437
|
+
continue;
|
|
1438
|
+
}
|
|
1439
|
+
const parsed = parseAgentDoneEvent(line, meta);
|
|
1440
|
+
if (parsed.malformed) {
|
|
1441
|
+
malformedEvents++;
|
|
1442
|
+
continue;
|
|
1443
|
+
}
|
|
1444
|
+
const ev = parsed.event;
|
|
1445
|
+
if (!ev)
|
|
1446
|
+
continue;
|
|
1447
|
+
if (meta.vendor_session_id)
|
|
1448
|
+
return { event: ev, malformedEvents, ambiguousVendorSession: false };
|
|
1449
|
+
if (candidate?.vendor_session_id &&
|
|
1450
|
+
ev.vendor_session_id &&
|
|
1451
|
+
candidate.vendor_session_id !== ev.vendor_session_id) {
|
|
1452
|
+
return { event: null, malformedEvents, ambiguousVendorSession: true };
|
|
1453
|
+
}
|
|
1454
|
+
if (!candidate)
|
|
1455
|
+
candidate = ev;
|
|
1456
|
+
}
|
|
1457
|
+
return { event: candidate, malformedEvents, ambiguousVendorSession: false };
|
|
1458
|
+
}
|
|
1459
|
+
function bindAgentVendorSession(meta, ev) {
|
|
1460
|
+
if (!meta.vendor_session_id && ev.vendor_session_id) {
|
|
1461
|
+
meta.vendor_session_id = ev.vendor_session_id;
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
function bindCompletedInitialPrompt(meta) {
|
|
1465
|
+
if (!meta.initial_prompt || meta.vendor_session_id)
|
|
1466
|
+
return;
|
|
1467
|
+
const size = safeStatSize(meta.event_file);
|
|
1468
|
+
if (size === 0) {
|
|
1469
|
+
throw new AitermError(`agent session '${meta.aiterm_session}' は起動時 prompt の完了待ちです。初回応答完了後に再度 pty_send(wait:"agent_done") してください。`, 2);
|
|
1470
|
+
}
|
|
1471
|
+
const text = readFileRange(meta.event_file, 0, size).toString("utf8");
|
|
1472
|
+
const lines = text.split("\n");
|
|
1473
|
+
const tail = lines.pop() ?? "";
|
|
1474
|
+
const scanned = scanAgentDoneLines(lines, meta);
|
|
1475
|
+
if (scanned.ambiguousVendorSession) {
|
|
1476
|
+
throw new AitermError("agent event file に複数の vendor_session_id が混在しています。該当セッションを閉じて起動し直してください。", 2);
|
|
1477
|
+
}
|
|
1478
|
+
if (!scanned.event) {
|
|
1479
|
+
const malformed = scanned.malformedEvents ? ` malformed_events=${scanned.malformedEvents}` : "";
|
|
1480
|
+
const partial = tail.trim() ? " partial_event=true" : "";
|
|
1481
|
+
throw new AitermError(`agent session '${meta.aiterm_session}' は起動時 prompt の完了 event をまだ確認できません。初回応答完了後に再度 pty_send(wait:"agent_done") してください。${malformed}${partial}`, 2);
|
|
1482
|
+
}
|
|
1483
|
+
bindAgentVendorSession(meta, scanned.event);
|
|
1484
|
+
meta.initial_prompt = false;
|
|
1485
|
+
writeAgentMetadata(meta);
|
|
1486
|
+
}
|
|
1487
|
+
async function waitAgentDoneEvent(meta, startOffset, timeout) {
|
|
1488
|
+
const deadline = performance.now() + timeout * 1000;
|
|
1489
|
+
let cursor = startOffset;
|
|
1490
|
+
let carry = "";
|
|
1491
|
+
let malformedEvents = 0;
|
|
1492
|
+
for (;;) {
|
|
1493
|
+
const size = safeStatSize(meta.event_file);
|
|
1494
|
+
if (size < cursor) {
|
|
1495
|
+
cursor = 0;
|
|
1496
|
+
carry = "";
|
|
1497
|
+
}
|
|
1498
|
+
if (size > cursor) {
|
|
1499
|
+
if (size - cursor > AGENT_EVENT_MAX_BYTES) {
|
|
1500
|
+
throw new AitermError("agent event file の増分が大きすぎます。該当セッションを閉じて起動し直してください。", 2);
|
|
1501
|
+
}
|
|
1502
|
+
carry += readFileRange(meta.event_file, cursor, size).toString("utf8");
|
|
1503
|
+
cursor = size;
|
|
1504
|
+
const parts = carry.split("\n");
|
|
1505
|
+
carry = parts.pop() ?? "";
|
|
1506
|
+
const scanned = scanAgentDoneLines(parts, meta);
|
|
1507
|
+
malformedEvents += scanned.malformedEvents;
|
|
1508
|
+
if (scanned.ambiguousVendorSession) {
|
|
1509
|
+
throw new AitermError("agent event file に複数の vendor_session_id が混在しています。該当セッションを閉じて起動し直してください。", 2);
|
|
1510
|
+
}
|
|
1511
|
+
if (scanned.event) {
|
|
1512
|
+
bindAgentVendorSession(meta, scanned.event);
|
|
1513
|
+
if (meta.vendor_session_id)
|
|
1514
|
+
writeAgentMetadata(meta);
|
|
1515
|
+
return { event: scanned.event, malformedEvents };
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
if (performance.now() >= deadline)
|
|
1519
|
+
return { event: null, malformedEvents };
|
|
1520
|
+
await sleep(AGENT_DONE_POLL_MS);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
function agentDoneSuffix(wait, vendor) {
|
|
1524
|
+
const ev = wait.event;
|
|
1525
|
+
if (!ev) {
|
|
1526
|
+
const malformed = wait.malformedEvents ? ` malformed_events=${wait.malformedEvents}` : "";
|
|
1527
|
+
return ` [is_complete=False via agent_timeout vendor=${vendor}${malformed}]`;
|
|
1528
|
+
}
|
|
1529
|
+
const bits = [
|
|
1530
|
+
"is_complete=True",
|
|
1531
|
+
"via agent_done",
|
|
1532
|
+
`vendor=${ev.vendor}`,
|
|
1533
|
+
ev.turn_id ? `turn_id=${ev.turn_id}` : null,
|
|
1534
|
+
ev.vendor_session_id ? `vendor_session_id=${ev.vendor_session_id}` : null,
|
|
1535
|
+
`done_status=${ev.done_status}`,
|
|
1536
|
+
].filter(Boolean);
|
|
1537
|
+
return ` [${bits.join(" ")}]`;
|
|
1538
|
+
}
|
|
1539
|
+
function isAgentTuiReady(kind, screen) {
|
|
1540
|
+
if (kind === "codex") {
|
|
1541
|
+
return screen.includes("OpenAI Codex") && /(^|\n)\s*[›>]/.test(screen);
|
|
1542
|
+
}
|
|
1543
|
+
return screen.includes("Grok Build") && /(^|\n|\s)❯/.test(screen);
|
|
1544
|
+
}
|
|
1545
|
+
async function waitAgentTuiReadyImpl(kind, sample, sleepFn, opts = {}) {
|
|
1546
|
+
const timeoutMs = opts.timeoutMs ?? AGENT_TUI_READY_TIMEOUT_MS;
|
|
1547
|
+
const pollMs = opts.pollMs ?? AGENT_TUI_READY_POLL_MS;
|
|
1548
|
+
const deadline = performance.now() + timeoutMs;
|
|
1549
|
+
let samples = 0;
|
|
1550
|
+
let lastScreen = "";
|
|
1551
|
+
for (;;) {
|
|
1552
|
+
lastScreen = sample();
|
|
1553
|
+
samples++;
|
|
1554
|
+
if (isAgentTuiReady(kind, lastScreen))
|
|
1555
|
+
return { ready: true, samples, lastScreen };
|
|
1556
|
+
if (performance.now() >= deadline)
|
|
1557
|
+
return { ready: false, samples, lastScreen };
|
|
1558
|
+
await sleepFn(pollMs);
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
async function waitAgentTuiReady(name, meta, timeoutMs = AGENT_TUI_READY_TIMEOUT_MS) {
|
|
1562
|
+
return waitAgentTuiReadyImpl(meta.kind, () => captureScreen(name, AGENT_TUI_READY_LINES), sleep, { timeoutMs });
|
|
1563
|
+
}
|
|
1564
|
+
async function settleAgentDoneScreenImpl(sample, sleepFn, opts = {}) {
|
|
1565
|
+
const minDelayMs = opts.minDelayMs ?? AGENT_DONE_SETTLE_MIN_MS;
|
|
1566
|
+
const pollMs = opts.pollMs ?? AGENT_DONE_SCREEN_SETTLE_POLL_MS;
|
|
1567
|
+
const maxPolls = opts.maxPolls ?? AGENT_DONE_SCREEN_SETTLE_MAX_POLLS;
|
|
1568
|
+
const minSamples = opts.minSamples ?? AGENT_DONE_SCREEN_SETTLE_MIN_SAMPLES;
|
|
1569
|
+
if (minDelayMs > 0)
|
|
1570
|
+
await sleepFn(minDelayMs);
|
|
1571
|
+
let prev = sample();
|
|
1572
|
+
let samples = 1;
|
|
1573
|
+
let stableStreak = 1;
|
|
1574
|
+
for (let i = 0; i < maxPolls; i++) {
|
|
1575
|
+
if (pollMs > 0)
|
|
1576
|
+
await sleepFn(pollMs);
|
|
1577
|
+
const current = sample();
|
|
1578
|
+
samples++;
|
|
1579
|
+
if (current.screen === prev.screen && current.logSize === prev.logSize) {
|
|
1580
|
+
stableStreak++;
|
|
1581
|
+
if (samples >= minSamples && stableStreak >= 2)
|
|
1582
|
+
return { unstable: false, samples };
|
|
1583
|
+
}
|
|
1584
|
+
else {
|
|
1585
|
+
stableStreak = 1;
|
|
1586
|
+
}
|
|
1587
|
+
prev = current;
|
|
1588
|
+
}
|
|
1589
|
+
return { unstable: true, samples };
|
|
1590
|
+
}
|
|
1591
|
+
async function settleAgentDoneScreen(name, lines) {
|
|
1592
|
+
return settleAgentDoneScreenImpl(() => ({
|
|
1593
|
+
screen: captureScreen(name, lines),
|
|
1594
|
+
logSize: safeStatSize(logpath(name)),
|
|
1595
|
+
}), sleep);
|
|
1596
|
+
}
|
|
1597
|
+
export async function __testSettleAgentDoneScreen(samples, opts = {}) {
|
|
1598
|
+
if (samples.length === 0)
|
|
1599
|
+
throw new AitermError("screen settle test samples が空です", 2);
|
|
1600
|
+
let i = 0;
|
|
1601
|
+
const sleeps = [];
|
|
1602
|
+
const result = await settleAgentDoneScreenImpl(() => samples[Math.min(i++, samples.length - 1)], async (ms) => {
|
|
1603
|
+
sleeps.push(ms);
|
|
1604
|
+
}, opts);
|
|
1605
|
+
return { ...result, sleeps };
|
|
1606
|
+
}
|
|
1607
|
+
export function __testIsAgentTuiReady(kind, screen) {
|
|
1608
|
+
return isAgentTuiReady(kind, screen);
|
|
1609
|
+
}
|
|
1610
|
+
export async function __testWaitAgentTuiReady(kind, samples, opts = {}) {
|
|
1611
|
+
if (samples.length === 0)
|
|
1612
|
+
throw new AitermError("agent ready test samples が空です", 2);
|
|
1613
|
+
let i = 0;
|
|
1614
|
+
const sleeps = [];
|
|
1615
|
+
const result = await waitAgentTuiReadyImpl(kind, () => samples[Math.min(i++, samples.length - 1)], async (ms) => {
|
|
1616
|
+
sleeps.push(ms);
|
|
1617
|
+
}, opts);
|
|
1618
|
+
return { ...result, sleeps };
|
|
1619
|
+
}
|
|
1620
|
+
export async function sendAndWaitAgentDone(name, text, o = {}) {
|
|
1621
|
+
assertSessionName(name);
|
|
1622
|
+
if (o.enter === false)
|
|
1623
|
+
throw new AitermError('wait:"agent_done" は enter:false と併用できません', 2);
|
|
1624
|
+
if (o.mark)
|
|
1625
|
+
throw new AitermError('wait:"agent_done" と mark:true は併用できません', 2);
|
|
1626
|
+
if (o.rtk)
|
|
1627
|
+
throw new AitermError('wait:"agent_done" と rtk:true は併用できません', 2);
|
|
1628
|
+
const meta = loadAgentMetadata(name);
|
|
1629
|
+
if (agentWaitLocks.has(name))
|
|
1630
|
+
throw new AitermError(`agent session '${name}' は別の agent_done 待機中です`, 2);
|
|
1631
|
+
const releaseFileLock = acquireAgentWaitFileLock(meta);
|
|
1632
|
+
const timeout = o.timeout ?? DEFAULT_AGENT_DONE_TIMEOUT;
|
|
1633
|
+
const screen = o.screen ?? true;
|
|
1634
|
+
agentWaitLocks.add(name);
|
|
1635
|
+
try {
|
|
1636
|
+
bindCompletedInitialPrompt(meta);
|
|
1637
|
+
if (!meta.vendor_session_id) {
|
|
1638
|
+
const ready = await waitAgentTuiReady(name, meta, o.ready_timeout ?? AGENT_TUI_READY_TIMEOUT_MS);
|
|
1639
|
+
if (!ready.ready) {
|
|
1640
|
+
throw new AitermError(`agent session '${name}' の ${agentLabel(meta.kind)} TUI が入力受付状態になりません。文字列は送信していません。` +
|
|
1641
|
+
"少し後で pty_read(screen:true) を確認し、TUI が起動済みなら再度 pty_send(wait:\"agent_done\") してください。", 2);
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
const startOffset = safeStatSize(meta.event_file);
|
|
1645
|
+
send(name, text, {
|
|
1646
|
+
enter: false,
|
|
1647
|
+
force: o.force,
|
|
1648
|
+
raw: o.raw,
|
|
1649
|
+
mark: false,
|
|
1650
|
+
rtk: false,
|
|
1651
|
+
});
|
|
1652
|
+
// Codex TUI は literal text 投入直後の Enter を取り落とすことがある。agent 経路だけ submit を分離する。
|
|
1653
|
+
await sleep(AGENT_SUBMIT_DELAY_MS);
|
|
1654
|
+
sendKey(name, "Enter");
|
|
1655
|
+
const wait = await waitAgentDoneEvent(meta, startOffset, timeout);
|
|
1656
|
+
const settled = wait.event
|
|
1657
|
+
? await settleAgentDoneScreen(name, o.lines ?? 0)
|
|
1658
|
+
: { unstable: false, samples: 0 };
|
|
1659
|
+
const out = await readOutput(name, {
|
|
1660
|
+
screen,
|
|
1661
|
+
lines: o.lines ?? null,
|
|
1662
|
+
timeout: 0,
|
|
1663
|
+
});
|
|
1664
|
+
writeOffset(name, safeStatSize(logpath(name)));
|
|
1665
|
+
return out + agentDoneSuffix(wait, meta.kind) + (settled.unstable ? " [agent_done_but_screen_unstable]" : "");
|
|
1666
|
+
}
|
|
1667
|
+
finally {
|
|
1668
|
+
agentWaitLocks.delete(name);
|
|
1669
|
+
releaseFileLock();
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
// ── 対話型エージェント起動(Codex / Grok Build(Grok) / Grok Build(Composer))──────
|
|
1673
|
+
// aiterm の永続端末に、指定モデルの対話エージェント TUI を起動する。以後は pty_read で画面を
|
|
1674
|
+
// 読み、pty_send で操作する=aiterm の対話パラダイムそのもの。モデルはツールごとに固定し、
|
|
1675
|
+
// reasoning effort は引数で渡す。CLI 未導入環境は明示エラー(動くフリをしない)。
|
|
798
1676
|
function resolveAgentBin(kind) {
|
|
799
1677
|
const home = process.env.HOME ?? os.homedir();
|
|
800
1678
|
const [envVar, rel, name] = kind === "codex"
|
|
@@ -824,23 +1702,60 @@ function resolveAgentBin(kind) {
|
|
|
824
1702
|
function shq(s) {
|
|
825
1703
|
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
826
1704
|
}
|
|
827
|
-
function buildAgentCmd(kind, bin, effort, prompt) {
|
|
1705
|
+
function buildAgentCmd(kind, bin, effort, prompt, meta = null) {
|
|
828
1706
|
const parts = [shq(bin)];
|
|
829
1707
|
if (kind === "codex") {
|
|
1708
|
+
if (meta?.kind === "codex")
|
|
1709
|
+
parts.push("--dangerously-bypass-hook-trust");
|
|
830
1710
|
// codex は config override で reasoning effort(例: low/medium/high)を渡す。
|
|
831
1711
|
if (effort)
|
|
832
1712
|
parts.push("-c", `model_reasoning_effort=${shq(effort)}`);
|
|
833
1713
|
}
|
|
834
1714
|
else {
|
|
835
1715
|
// grok / composer は同じ grok CLI をモデル違いで起動。effort は low/medium/high/xhigh/max。
|
|
1716
|
+
parts.push("--no-auto-update");
|
|
1717
|
+
if (meta?.kind === "grok" || meta?.kind === "composer")
|
|
1718
|
+
parts.push("--no-alt-screen");
|
|
836
1719
|
parts.push("--model", kind === "composer" ? "grok-composer-2.5-fast" : "grok-build");
|
|
837
1720
|
if (effort)
|
|
838
1721
|
parts.push("--effort", shq(effort));
|
|
1722
|
+
if ((meta?.kind === "grok" || meta?.kind === "composer") && prompt)
|
|
1723
|
+
parts.push("--verbatim");
|
|
839
1724
|
}
|
|
840
1725
|
if (prompt)
|
|
841
1726
|
parts.push(shq(prompt)); // 初手プロンプト(任意)
|
|
842
1727
|
return parts.join(" ");
|
|
843
1728
|
}
|
|
1729
|
+
function agentEnvPrefix(meta, sid) {
|
|
1730
|
+
if (!meta)
|
|
1731
|
+
return "";
|
|
1732
|
+
const common = [
|
|
1733
|
+
`AITERM_AGENT_KIND=${shq(meta.kind)}`,
|
|
1734
|
+
`AITERM_SESSION_ID=${shq(sid)}`,
|
|
1735
|
+
`AITERM_AGENT_SESSION_ID=${shq(sid)}`,
|
|
1736
|
+
`AITERM_AGENT_LAUNCH_ID=${shq(meta.launch_id)}`,
|
|
1737
|
+
];
|
|
1738
|
+
if (meta.kind === "codex") {
|
|
1739
|
+
return [`CODEX_HOME=${shq(meta.codex_home ?? "")}`, ...common].join(" ") + " ";
|
|
1740
|
+
}
|
|
1741
|
+
return [
|
|
1742
|
+
`HOME=${shq(meta.home ?? "")}`,
|
|
1743
|
+
`GROK_HOME=${shq(meta.grok_home ?? "")}`,
|
|
1744
|
+
"GROK_DISABLE_AUTOUPDATER=1",
|
|
1745
|
+
"GROK_CLAUDE_HOOKS_ENABLED=false",
|
|
1746
|
+
"GROK_CURSOR_HOOKS_ENABLED=false",
|
|
1747
|
+
"GROK_CLAUDE_SKILLS_ENABLED=false",
|
|
1748
|
+
"GROK_CURSOR_SKILLS_ENABLED=false",
|
|
1749
|
+
"GROK_CLAUDE_RULES_ENABLED=false",
|
|
1750
|
+
"GROK_CURSOR_RULES_ENABLED=false",
|
|
1751
|
+
"GROK_CLAUDE_AGENTS_ENABLED=false",
|
|
1752
|
+
"GROK_CURSOR_AGENTS_ENABLED=false",
|
|
1753
|
+
"GROK_CLAUDE_MCPS_ENABLED=false",
|
|
1754
|
+
"GROK_CURSOR_MCPS_ENABLED=false",
|
|
1755
|
+
`AITERM_REAL_HOME=${shq(process.env.HOME ?? os.homedir())}`,
|
|
1756
|
+
...common,
|
|
1757
|
+
].join(" ") + " ";
|
|
1758
|
+
}
|
|
844
1759
|
function agentLabel(kind) {
|
|
845
1760
|
return kind === "composer"
|
|
846
1761
|
? "Grok Build(Composer)"
|
|
@@ -858,6 +1773,7 @@ export function openAgent(kind, opts = {}) {
|
|
|
858
1773
|
if (effort && kind !== "codex" && !GROK_EFFORTS.has(effort)) {
|
|
859
1774
|
throw new AitermError(`reasoning_effort '${effort}' は不正です(${label} は low/medium/high/xhigh/max)`, 2);
|
|
860
1775
|
}
|
|
1776
|
+
const agentDone = !!opts.agent_done;
|
|
861
1777
|
const bin = resolveAgentBin(kind);
|
|
862
1778
|
if (!bin) {
|
|
863
1779
|
const where = kind === "codex" ? "~/.local/bin/codex" : "~/.grok/bin/grok";
|
|
@@ -893,9 +1809,15 @@ export function openAgent(kind, opts = {}) {
|
|
|
893
1809
|
const binForCmd = isWin ? toWslPath(bin) : bin;
|
|
894
1810
|
const cwdForCmd = cwd && isWin ? toWslPath(cwd) : cwd;
|
|
895
1811
|
const [sid, hint] = openSession(opts.session_name ?? null, "bash");
|
|
896
|
-
const cmd = buildAgentCmd(kind, binForCmd, effort, opts.prompt ?? null);
|
|
897
|
-
const full = cwdForCmd ? `cd ${shq(cwdForCmd)} && ${cmd}` : cmd;
|
|
898
1812
|
try {
|
|
1813
|
+
const meta = agentDone
|
|
1814
|
+
? kind === "codex"
|
|
1815
|
+
? createCodexAgentMetadata(sid, cwd, !!opts.prompt)
|
|
1816
|
+
: createGrokAgentMetadata(kind, sid, cwd, !!opts.prompt)
|
|
1817
|
+
: null;
|
|
1818
|
+
const cmd = buildAgentCmd(kind, binForCmd, effort, opts.prompt ?? null, meta);
|
|
1819
|
+
const envPrefix = agentEnvPrefix(meta, sid);
|
|
1820
|
+
const full = cwdForCmd ? `cd ${shq(cwdForCmd)} && ${envPrefix}${cmd}` : `${envPrefix}${cmd}`;
|
|
899
1821
|
// force:true で送る。起動骨格は `bin '...'` の固定形で、prompt/cwd/effort は shq でクオート済みの
|
|
900
1822
|
// 引数=シェルは決して破壊コマンドとして実行しない。破壊ゲート(生シェルコマンド想定)を prompt に
|
|
901
1823
|
// 掛けるのは純誤検知で、`codex 'rm -rf / を説明して'` 等の正当な起動を塞いでしまう(A4)。
|
|
@@ -913,7 +1835,8 @@ export function openAgent(kind, opts = {}) {
|
|
|
913
1835
|
}
|
|
914
1836
|
return [
|
|
915
1837
|
sid,
|
|
916
|
-
`${label} を session ${sid}
|
|
1838
|
+
`${label} を session ${sid} で起動した。${agentDone ? "agent_done 待機が有効。" : ""}` +
|
|
1839
|
+
`${agentDone && kind !== "codex" ? " hook 汚染防止のため Grok 実行中は一時 HOME を使う。" : ""}\n${hint}\n` +
|
|
917
1840
|
`TUI の描画には数秒かかる。少し置いてから pty_read(${sid}, screen:true) で画面を読み、` +
|
|
918
1841
|
`pty_send(${sid}, "...") で入力・pty_key(${sid}, "Enter"/"Up"/"C-c" 等) で操作する(対話)。` +
|
|
919
1842
|
`起動直後に増分 pty_read すると空/半描画になり得るので screen:true を使う。`,
|