@triflux/core 10.53.0 → 10.55.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/hooks/codex-session-hook.mjs +1 -17
- package/hub/cli-adapter-base.mjs +3 -424
- package/hub/index.mjs +0 -5
- package/hub/lib/hosts-compat.mjs +18 -64
- package/hub/lib/private-tmp.mjs +35 -0
- package/hub/lib/process-utils.mjs +19 -220
- package/hub/lib/prompt-tmp.mjs +6 -24
- package/hub/lib/spawn-trace.mjs +112 -5
- package/hub/lib/timeout-defaults.mjs +34 -6
- package/hub/lib/worker-lifecycle.mjs +3 -0
- package/hub/platform.mjs +3 -1
- package/hub/team/agent-map.json +29 -0
- package/hub/team/intervention.mjs +6 -4
- package/hub/team/nativeProxy.mjs +2 -48
- package/hub/team/pane.mjs +329 -0
- package/hub/team/psmux.mjs +1765 -0
- package/hub/team/retry-state-machine.mjs +1 -1
- package/hub/team/session.mjs +541 -0
- package/hud/constants.mjs +0 -3
- package/hud/providers/claude.mjs +35 -15
- package/hud/providers/codex.mjs +169 -42
- package/hud/utils.mjs +0 -23
- package/package.json +2 -2
- package/scripts/lib/backup-rotation.mjs +60 -0
- package/scripts/lib/codex-profile-config.mjs +20 -7
- package/scripts/lib/codex-recovery.sh +2 -3
- package/scripts/lib/install-blob-history.json +3196 -0
- package/scripts/lib/install-retire.mjs +203 -0
- package/scripts/lib/legacy-hook-cleanup.mjs +148 -13
- package/scripts/lib/legacy-mcp-cleanup.mjs +405 -104
- package/scripts/lib/machine-profile.mjs +2 -2
- package/scripts/lib/mcp-filter.mjs +1 -42
- package/scripts/lib/mcp-guard-engine.mjs +198 -19
- package/scripts/lib/mcp-manifest.mjs +0 -40
- package/scripts/lib/process-utils.mjs +19 -0
- package/scripts/lib/stable-node.mjs +35 -0
- package/scripts/lib/test-env.mjs +20 -0
- package/scripts/lib/tmr-install.mjs +183 -0
- package/hooks/agy-session-hook.mjs +0 -155
- package/hooks/claude-cwd-projection-refresh.mjs +0 -11
- package/hooks/hook-orchestrator.mjs +0 -11
- package/hooks/lib/resolve-root.mjs +0 -62
- package/hub/codex-adapter.mjs +0 -193
- package/hub/codex-preflight.mjs +0 -219
- package/hub/lib/cto-env.mjs +0 -70
- package/scripts/lib/remote-spawn-transfer.mjs +0 -226
- package/scripts/lib/session-presence.mjs +0 -151
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import {
|
|
4
|
+
chmodSync,
|
|
5
|
+
copyFileSync,
|
|
6
|
+
linkSync,
|
|
7
|
+
lstatSync,
|
|
8
|
+
mkdirSync,
|
|
9
|
+
mkdtempSync,
|
|
10
|
+
readdirSync,
|
|
11
|
+
rmSync,
|
|
12
|
+
symlinkSync,
|
|
13
|
+
writeFileSync,
|
|
14
|
+
} from "node:fs";
|
|
15
|
+
import { tmpdir } from "node:os";
|
|
16
|
+
import { delimiter, join } from "node:path";
|
|
17
|
+
|
|
18
|
+
// 버전과 체크섬은 이 상수 한 곳에서만 올린다. 받은 파일은 이 sha256 으로만 믿는다.
|
|
19
|
+
export const TMR_RELEASE = Object.freeze({
|
|
20
|
+
version: "0.1.0",
|
|
21
|
+
asset: "tmux-rooms-cli-0.1.0-macos-arm64.zip",
|
|
22
|
+
sha256: "342b34768a6f054d6bda6d3a63de37aeae08c5304d4531eecedf9631805a282f",
|
|
23
|
+
});
|
|
24
|
+
const releaseBase = ({ version }) =>
|
|
25
|
+
`https://github.com/tellang/tmux-rooms/releases/download/v${version}`;
|
|
26
|
+
|
|
27
|
+
export function tmrSupported(platform = process.platform, arch = process.arch) {
|
|
28
|
+
return platform === "darwin" && arch === "arm64";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function tmrBinDir(home) {
|
|
32
|
+
return join(home, ".local", "bin");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function findTmr({ home, env = process.env }) {
|
|
36
|
+
const dirs = [
|
|
37
|
+
tmrBinDir(home),
|
|
38
|
+
...String(env.PATH || "").split(delimiter),
|
|
39
|
+
].filter(Boolean);
|
|
40
|
+
// 깨진 링크도 사용자 것으로 보고 건드리지 않는다. 읽을 수 없는 PATH 항목은 건너뛴다.
|
|
41
|
+
for (const dir of dirs)
|
|
42
|
+
for (const name of ["tmuxrooms", "tmr"]) {
|
|
43
|
+
try {
|
|
44
|
+
if (lstatSync(join(dir, name), { throwIfNoEntry: false }))
|
|
45
|
+
return join(dir, name);
|
|
46
|
+
} catch {}
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function download(url, fetchFn) {
|
|
52
|
+
const response = await fetchFn(url, { signal: AbortSignal.timeout(60_000) });
|
|
53
|
+
if (!response.ok) throw new Error(`${url}: HTTP ${response.status}`);
|
|
54
|
+
return Buffer.from(await response.arrayBuffer());
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function sha256(buffer) {
|
|
58
|
+
return createHash("sha256").update(buffer).digest("hex");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function extractWithDitto(zipPath, directory) {
|
|
62
|
+
execFileSync("ditto", ["-x", "-k", zipPath, directory], {
|
|
63
|
+
stdio: "ignore",
|
|
64
|
+
timeout: 30_000,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 체크섬이 맞는 tmuxrooms 를 ~/.local/bin 에 넣고 상대 링크 tmr 을 만든다. */
|
|
69
|
+
export async function installTmr({
|
|
70
|
+
home,
|
|
71
|
+
release = TMR_RELEASE,
|
|
72
|
+
fetchFn = fetch,
|
|
73
|
+
extract = extractWithDitto,
|
|
74
|
+
run = execFileSync,
|
|
75
|
+
symlink = symlinkSync,
|
|
76
|
+
}) {
|
|
77
|
+
const base = releaseBase(release);
|
|
78
|
+
const zip = await download(`${base}/${release.asset}`, fetchFn);
|
|
79
|
+
if (sha256(zip) !== release.sha256)
|
|
80
|
+
throw new Error("zip 체크섬이 고정값과 다르다");
|
|
81
|
+
const sums = (await download(`${base}/SHA256SUMS`, fetchFn))
|
|
82
|
+
.toString("utf8")
|
|
83
|
+
.split(/\r?\n/)
|
|
84
|
+
.map((line) => line.trim().split(/\s+/));
|
|
85
|
+
const listed = sums.find(([, name]) => name === release.asset)?.[0];
|
|
86
|
+
if (listed !== release.sha256)
|
|
87
|
+
throw new Error("SHA256SUMS 의 값이 고정값과 다르다");
|
|
88
|
+
|
|
89
|
+
const work = mkdtempSync(join(tmpdir(), "tfx-tmr-"));
|
|
90
|
+
try {
|
|
91
|
+
const zipPath = join(work, release.asset);
|
|
92
|
+
const out = join(work, "out");
|
|
93
|
+
writeFileSync(zipPath, zip);
|
|
94
|
+
mkdirSync(out);
|
|
95
|
+
extract(zipPath, out);
|
|
96
|
+
const extracted = join(out, "tmuxrooms");
|
|
97
|
+
// zip 에는 실행 파일 하나만 있어야 한다. 다른 구성이면 받은 것을 쓰지 않는다.
|
|
98
|
+
const entries = readdirSync(out);
|
|
99
|
+
if (
|
|
100
|
+
entries.length !== 1 ||
|
|
101
|
+
entries[0] !== "tmuxrooms" ||
|
|
102
|
+
!lstatSync(extracted).isFile()
|
|
103
|
+
)
|
|
104
|
+
throw new Error("zip 구성이 예상과 다르다");
|
|
105
|
+
|
|
106
|
+
const binDir = tmrBinDir(home);
|
|
107
|
+
mkdirSync(binDir, { recursive: true, mode: 0o755 });
|
|
108
|
+
const target = join(binDir, "tmuxrooms");
|
|
109
|
+
// 임시 파일은 이번 시도만 쓰는 디렉터리에 둔다. 정리할 때 남의 파일을 지우지 않는다.
|
|
110
|
+
const staging = mkdtempSync(join(binDir, ".tfx-tmr-"));
|
|
111
|
+
try {
|
|
112
|
+
const temporary = join(staging, "tmuxrooms");
|
|
113
|
+
copyFileSync(extracted, temporary);
|
|
114
|
+
chmodSync(temporary, 0o755);
|
|
115
|
+
// 묻는 사이 다른 쪽이 tmuxrooms 를 만들었으면 덮지 않고 실패한다(EEXIST).
|
|
116
|
+
linkSync(temporary, target);
|
|
117
|
+
} finally {
|
|
118
|
+
rmSync(staging, { recursive: true, force: true });
|
|
119
|
+
}
|
|
120
|
+
const link = join(binDir, "tmr");
|
|
121
|
+
// 이미 있는 tmr 은 사용자 것일 수 있어 건드리지 않는다. 링크를 못 만들어도
|
|
122
|
+
// 체크섬이 맞는 tmuxrooms 는 완전한 설치본이라 지우지 않고 안내만 남긴다.
|
|
123
|
+
let linked = false;
|
|
124
|
+
try {
|
|
125
|
+
if (!lstatSync(link, { throwIfNoEntry: false })) {
|
|
126
|
+
symlink("tmuxrooms", link);
|
|
127
|
+
linked = true;
|
|
128
|
+
}
|
|
129
|
+
} catch {}
|
|
130
|
+
|
|
131
|
+
let runs = true;
|
|
132
|
+
try {
|
|
133
|
+
run(target, ["--help"], { stdio: "ignore", timeout: 10_000 });
|
|
134
|
+
} catch {
|
|
135
|
+
runs = false;
|
|
136
|
+
}
|
|
137
|
+
return { target, link, linked, runs };
|
|
138
|
+
} finally {
|
|
139
|
+
rmSync(work, { recursive: true, force: true });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** setup 동의 단계. macOS arm64 에서 tmr 이 없을 때만 묻고, 물을 수 없으면 안내만 남긴다. */
|
|
144
|
+
export async function offerTmrInstall({
|
|
145
|
+
interactive,
|
|
146
|
+
ask,
|
|
147
|
+
home,
|
|
148
|
+
env = process.env,
|
|
149
|
+
platform = process.platform,
|
|
150
|
+
arch = process.arch,
|
|
151
|
+
install = installTmr,
|
|
152
|
+
log = console.log,
|
|
153
|
+
warn = console.warn,
|
|
154
|
+
}) {
|
|
155
|
+
if (!tmrSupported(platform, arch)) return "not-needed";
|
|
156
|
+
if (findTmr({ home, env })) return "present";
|
|
157
|
+
if (!interactive) {
|
|
158
|
+
log("tmr 이 없다. tfx setup 에서 설치를 물을 수 있다.");
|
|
159
|
+
return "deferred";
|
|
160
|
+
}
|
|
161
|
+
if (!(await ask("tmr 을 설치할까요? (tmux 세션 목록 도구, ~/.local/bin)"))) {
|
|
162
|
+
log("tmr 설치를 건너뜀.");
|
|
163
|
+
return "declined";
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
const { target, link, linked, runs } = await install({ home });
|
|
167
|
+
log(`tmr ${TMR_RELEASE.version} 설치: ${target}`);
|
|
168
|
+
if (!linked && !lstatSync(link, { throwIfNoEntry: false }))
|
|
169
|
+
warn(`tmr 링크를 만들지 못했다. 직접 만든다: ln -s tmuxrooms "${link}"`);
|
|
170
|
+
if (!runs) warn(`설치한 ${target} 가 실행되지 않는다.`);
|
|
171
|
+
const binDir = tmrBinDir(home);
|
|
172
|
+
if (
|
|
173
|
+
!String(env.PATH || "")
|
|
174
|
+
.split(delimiter)
|
|
175
|
+
.includes(binDir)
|
|
176
|
+
)
|
|
177
|
+
log(`${binDir} 가 PATH 에 없다. 셸 설정에 추가한다.`);
|
|
178
|
+
return "installed";
|
|
179
|
+
} catch (error) {
|
|
180
|
+
warn(`tmr 설치 실패: ${error?.message || error}`);
|
|
181
|
+
return "failed";
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { argv, exit, stdin, stdout } from "node:process";
|
|
4
|
-
import { pathToFileURL } from "node:url";
|
|
5
|
-
import {
|
|
6
|
-
emitParticipantSessionStarted,
|
|
7
|
-
shouldSkipInteractiveRegistration,
|
|
8
|
-
} from "../scripts/lib/session-presence.mjs";
|
|
9
|
-
|
|
10
|
-
function parsePayload(stdinData) {
|
|
11
|
-
try {
|
|
12
|
-
const raw = typeof stdinData === "string" ? stdinData : "";
|
|
13
|
-
return raw.trim()
|
|
14
|
-
? { ok: true, payload: JSON.parse(raw) }
|
|
15
|
-
: { ok: false, payload: {} };
|
|
16
|
-
} catch {
|
|
17
|
-
return { ok: false, payload: {} };
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Convert an Antigravity hook payload into the Codex-shaped session payload
|
|
23
|
-
* consumed by the shared fast session registration helpers.
|
|
24
|
-
*
|
|
25
|
-
* Antigravity hook payloads use camelCase system metadata (`conversationId`,
|
|
26
|
-
* `workspacePaths`) rather than Codex's `session_id`/`cwd`. The
|
|
27
|
-
* `conversationId` is the stable per-conversation UUID; the first mounted
|
|
28
|
-
* workspace path is the effective cwd.
|
|
29
|
-
*
|
|
30
|
-
* @param {Record<string, unknown> | null | undefined} payload
|
|
31
|
-
* @returns {string} JSON string with `{ session_id, cwd, actor_cli }`.
|
|
32
|
-
*/
|
|
33
|
-
export function toSessionPayload(payload) {
|
|
34
|
-
const sessionId = String(payload?.conversationId || "").trim();
|
|
35
|
-
const workspacePaths = Array.isArray(payload?.workspacePaths)
|
|
36
|
-
? payload.workspacePaths
|
|
37
|
-
: [];
|
|
38
|
-
const cwd =
|
|
39
|
-
typeof workspacePaths[0] === "string" && workspacePaths[0]
|
|
40
|
-
? workspacePaths[0]
|
|
41
|
-
: process.cwd();
|
|
42
|
-
return JSON.stringify({ session_id: sessionId, cwd, actor_cli: "agy" });
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Decide whether a PreInvocation payload should register or heartbeat.
|
|
47
|
-
*
|
|
48
|
-
* agy has no distinct SessionStart/UserPromptSubmit events. PreInvocation fires
|
|
49
|
-
* before every model call, so per-conversation `invocationNum` gates register
|
|
50
|
-
* (first call) vs heartbeat (later calls). An explicit argv mode overrides this,
|
|
51
|
-
* mirroring the Codex hook.
|
|
52
|
-
*
|
|
53
|
-
* @param {string | null | undefined} argvMode
|
|
54
|
-
* @param {Record<string, unknown> | null | undefined} payload
|
|
55
|
-
* @returns {"register" | "heartbeat"}
|
|
56
|
-
*/
|
|
57
|
-
export function normalizeMode(argvMode, payload) {
|
|
58
|
-
const direct = String(argvMode || "")
|
|
59
|
-
.trim()
|
|
60
|
-
.toLowerCase();
|
|
61
|
-
if (direct === "register" || direct === "heartbeat") return direct;
|
|
62
|
-
|
|
63
|
-
const invocationNum = Number(payload?.invocationNum);
|
|
64
|
-
if (Number.isFinite(invocationNum)) {
|
|
65
|
-
return invocationNum <= 1 ? "register" : "heartbeat";
|
|
66
|
-
}
|
|
67
|
-
// 첫 호출 정보가 없으면 시작 기록을 남긴다.
|
|
68
|
-
return "register";
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function swallowStdoutWrite(_chunk, encodingOrCallback, callback) {
|
|
72
|
-
const done =
|
|
73
|
-
typeof encodingOrCallback === "function" ? encodingOrCallback : callback;
|
|
74
|
-
if (typeof done === "function") done();
|
|
75
|
-
return true;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async function runHookSideEffectsWithStdoutSuppressed(fn) {
|
|
79
|
-
const originalStdoutWrite = stdout.write;
|
|
80
|
-
const originalConsoleDebug = console.debug;
|
|
81
|
-
const originalConsoleInfo = console.info;
|
|
82
|
-
const originalConsoleLog = console.log;
|
|
83
|
-
|
|
84
|
-
stdout.write = swallowStdoutWrite;
|
|
85
|
-
console.debug = () => {};
|
|
86
|
-
console.info = () => {};
|
|
87
|
-
console.log = () => {};
|
|
88
|
-
try {
|
|
89
|
-
return await fn();
|
|
90
|
-
} finally {
|
|
91
|
-
stdout.write = originalStdoutWrite;
|
|
92
|
-
console.debug = originalConsoleDebug;
|
|
93
|
-
console.info = originalConsoleInfo;
|
|
94
|
-
console.log = originalConsoleLog;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export async function runAgySessionHook(stdinData, opts = {}) {
|
|
99
|
-
const output = "{}\n";
|
|
100
|
-
const parsed = parsePayload(stdinData);
|
|
101
|
-
if (!parsed.ok) {
|
|
102
|
-
if (opts.writeStdout !== false) stdout.write(output);
|
|
103
|
-
return output;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const sessionPayload = toSessionPayload(parsed.payload);
|
|
107
|
-
const sessionId = JSON.parse(sessionPayload).session_id;
|
|
108
|
-
// No conversation id means nothing to register; stay a silent no-op.
|
|
109
|
-
if (!sessionId) {
|
|
110
|
-
if (opts.writeStdout !== false) stdout.write(output);
|
|
111
|
-
return output;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const mode = normalizeMode(opts.argvMode ?? argv[2], parsed.payload);
|
|
115
|
-
const emitSessionStarted =
|
|
116
|
-
opts.emitSessionStarted || emitParticipantSessionStarted;
|
|
117
|
-
|
|
118
|
-
try {
|
|
119
|
-
await runHookSideEffectsWithStdoutSuppressed(async () => {
|
|
120
|
-
if (
|
|
121
|
-
mode === "register" &&
|
|
122
|
-
!shouldSkipInteractiveRegistration(JSON.parse(sessionPayload), opts)
|
|
123
|
-
) {
|
|
124
|
-
try {
|
|
125
|
-
await emitSessionStarted(sessionPayload);
|
|
126
|
-
} catch {}
|
|
127
|
-
}
|
|
128
|
-
});
|
|
129
|
-
} catch {
|
|
130
|
-
// 로컬 기록 실패가 세션을 막지 않게 한다.
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (opts.writeStdout !== false) {
|
|
134
|
-
stdout.write(output);
|
|
135
|
-
}
|
|
136
|
-
return output;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function readStdin() {
|
|
140
|
-
return new Promise((resolve) => {
|
|
141
|
-
let data = "";
|
|
142
|
-
stdin.setEncoding("utf8");
|
|
143
|
-
stdin.on("data", (chunk) => {
|
|
144
|
-
data += chunk;
|
|
145
|
-
});
|
|
146
|
-
stdin.on("end", () => resolve(data));
|
|
147
|
-
stdin.on("error", () => resolve(data));
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
if (argv[1] && import.meta.url === pathToFileURL(argv[1]).href) {
|
|
152
|
-
const stdinData = await readStdin();
|
|
153
|
-
await runAgySessionHook(stdinData);
|
|
154
|
-
exit(0);
|
|
155
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// ADR-0023 전환 stub. 다음 릴리스에서 지운다.
|
|
3
|
-
import { readFileSync } from "node:fs";
|
|
4
|
-
import { cleanupLegacyHooks } from "../scripts/lib/legacy-hook-cleanup.mjs";
|
|
5
|
-
|
|
6
|
-
try {
|
|
7
|
-
readFileSync(0);
|
|
8
|
-
cleanupLegacyHooks();
|
|
9
|
-
} catch {
|
|
10
|
-
// 오래된 Claude hook이 남아 있어도 세션 시작을 막지 않는다.
|
|
11
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// ADR-0023 전환 stub. 다음 릴리스에서 지운다.
|
|
3
|
-
import { readFileSync } from "node:fs";
|
|
4
|
-
import { cleanupLegacyHooks } from "../scripts/lib/legacy-hook-cleanup.mjs";
|
|
5
|
-
|
|
6
|
-
try {
|
|
7
|
-
readFileSync(0);
|
|
8
|
-
cleanupLegacyHooks();
|
|
9
|
-
} catch {
|
|
10
|
-
// 오래된 Claude hook이 남아 있어도 세션 시작을 막지 않는다.
|
|
11
|
-
}
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { dirname, join, resolve } from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
|
|
6
|
-
const BREADCRUMB_PATH = join(homedir(), ".claude", "scripts", ".tfx-pkg-root");
|
|
7
|
-
|
|
8
|
-
function normalizeCandidate(candidate) {
|
|
9
|
-
if (typeof candidate !== "string") return null;
|
|
10
|
-
const value = candidate.trim();
|
|
11
|
-
if (!value) return null;
|
|
12
|
-
return value;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function isValidPluginRoot(candidate) {
|
|
16
|
-
const root = normalizeCandidate(candidate);
|
|
17
|
-
if (!root) return false;
|
|
18
|
-
return existsSync(join(root, "hooks", "codex-session-hook.mjs"));
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function toPluginRootFromUrl(url) {
|
|
22
|
-
if (typeof url !== "string" || !url) return null;
|
|
23
|
-
try {
|
|
24
|
-
const sourceDir = dirname(fileURLToPath(url));
|
|
25
|
-
const hookScoped = sourceDir.match(/^(.*?)[\\/]hooks(?:[\\/].*)?$/);
|
|
26
|
-
if (hookScoped?.[1]) return hookScoped[1];
|
|
27
|
-
return resolve(sourceDir, "..");
|
|
28
|
-
} catch {
|
|
29
|
-
return null;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function readBreadcrumbRoot() {
|
|
34
|
-
if (!existsSync(BREADCRUMB_PATH)) return null;
|
|
35
|
-
try {
|
|
36
|
-
return normalizeCandidate(readFileSync(BREADCRUMB_PATH, "utf8"));
|
|
37
|
-
} catch {
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function resolvePluginRoot(callerUrl) {
|
|
43
|
-
const breadcrumbRoot = readBreadcrumbRoot();
|
|
44
|
-
if (isValidPluginRoot(breadcrumbRoot)) return breadcrumbRoot;
|
|
45
|
-
|
|
46
|
-
const pluginRoot = normalizeCandidate(process.env.PLUGIN_ROOT);
|
|
47
|
-
if (isValidPluginRoot(pluginRoot)) return pluginRoot;
|
|
48
|
-
|
|
49
|
-
const envRoot = normalizeCandidate(process.env.CLAUDE_PLUGIN_ROOT);
|
|
50
|
-
if (isValidPluginRoot(envRoot)) return envRoot;
|
|
51
|
-
|
|
52
|
-
const callerFallback = toPluginRootFromUrl(callerUrl);
|
|
53
|
-
if (isValidPluginRoot(callerFallback)) return callerFallback;
|
|
54
|
-
|
|
55
|
-
const moduleFallback = toPluginRootFromUrl(import.meta.url) || process.cwd();
|
|
56
|
-
process.stderr.write(
|
|
57
|
-
`[resolve-root] warning: failed to resolve plugin root from breadcrumb/env/caller; fallback=${moduleFallback}\n`,
|
|
58
|
-
);
|
|
59
|
-
return moduleFallback;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export const PLUGIN_ROOT = resolvePluginRoot(import.meta.url);
|
package/hub/codex-adapter.mjs
DELETED
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { codexProfileConfigOverrides } from "../scripts/lib/codex-profile-config.mjs";
|
|
5
|
-
import {
|
|
6
|
-
buildExecCommand,
|
|
7
|
-
executeWithAttempts,
|
|
8
|
-
normalizePathForShell,
|
|
9
|
-
runProcess,
|
|
10
|
-
shellQuote,
|
|
11
|
-
} from "./cli-adapter-base.mjs";
|
|
12
|
-
import { runPreflight } from "./codex-preflight.mjs";
|
|
13
|
-
import { isActivityLifecycleEnabled } from "./lib/worker-lifecycle.mjs";
|
|
14
|
-
|
|
15
|
-
// ── Codex-specific stall inference ──────────────────────────────
|
|
16
|
-
|
|
17
|
-
function inferStallMode(stdout, stderr) {
|
|
18
|
-
const text = `${stdout}\n${stderr}`.toLowerCase();
|
|
19
|
-
if (
|
|
20
|
-
/(rate.?limit|quota|throttl|too.many.requests|429|usage.limit)/u.test(text)
|
|
21
|
-
)
|
|
22
|
-
return "rate_limited";
|
|
23
|
-
if (/(approval|approve|permission|sandbox|bypass)/u.test(text))
|
|
24
|
-
return "approval_stall";
|
|
25
|
-
if (
|
|
26
|
-
/\bmcp\b|context7|playwright|tavily|exa|brave|sequential|server/u.test(text)
|
|
27
|
-
)
|
|
28
|
-
return "mcp_stall";
|
|
29
|
-
return "timeout";
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// ── Codex command building ──────────────────────────────────────
|
|
33
|
-
|
|
34
|
-
function commandWithOverrides(command, prompt, codexPath, overrides = []) {
|
|
35
|
-
const next = codexPath
|
|
36
|
-
? command.replace(/^codex\b/u, shellQuote(codexPath))
|
|
37
|
-
: command;
|
|
38
|
-
if (!overrides.length) return next;
|
|
39
|
-
const promptArg = JSON.stringify(prompt);
|
|
40
|
-
const flags = overrides
|
|
41
|
-
.flatMap((value) => ["-c", shellQuote(value)])
|
|
42
|
-
.join(" ");
|
|
43
|
-
return next.endsWith(promptArg)
|
|
44
|
-
? `${next.slice(0, -promptArg.length)}${flags} ${promptArg}`
|
|
45
|
-
: `${next} ${flags}`;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function buildOverrides(requested, excluded) {
|
|
49
|
-
return [
|
|
50
|
-
...new Set(
|
|
51
|
-
(requested || []).filter((name) => (excluded || []).includes(name)),
|
|
52
|
-
),
|
|
53
|
-
].map((name) => `mcp_servers.${name}.enabled=false`);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function buildAttempts(opts, preflight) {
|
|
57
|
-
const timeout = Number.isFinite(opts.timeout) ? opts.timeout : 300_000;
|
|
58
|
-
const requested = Array.isArray(opts.mcpServers) ? [...opts.mcpServers] : [];
|
|
59
|
-
const base = {
|
|
60
|
-
timeout,
|
|
61
|
-
profile: opts.profile,
|
|
62
|
-
requested,
|
|
63
|
-
excluded: [...(preflight.excludeMcpServers || [])],
|
|
64
|
-
forceBypass: preflight.needsBypass,
|
|
65
|
-
};
|
|
66
|
-
if (opts.retryOnFail === false) return [base];
|
|
67
|
-
const retryTimeout = isActivityLifecycleEnabled() ? timeout : timeout * 2;
|
|
68
|
-
return [
|
|
69
|
-
base,
|
|
70
|
-
{ ...base, timeout: retryTimeout, excluded: requested, forceBypass: true },
|
|
71
|
-
];
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// ── Launch script ───────────────────────────────────────────────
|
|
75
|
-
|
|
76
|
-
function createLaunchScriptText(opts) {
|
|
77
|
-
const parts = [
|
|
78
|
-
"codex",
|
|
79
|
-
"exec",
|
|
80
|
-
"--dangerously-bypass-approvals-and-sandbox",
|
|
81
|
-
"--skip-git-repo-check",
|
|
82
|
-
];
|
|
83
|
-
// Select the effort profile via `-c` config overrides instead of
|
|
84
|
-
// `--profile <name>` (see buildExecCommand): codex 0.134+ rejects `--profile
|
|
85
|
-
// X` whenever config.toml still contains an inline [profiles.X] table.
|
|
86
|
-
if (opts.profile) {
|
|
87
|
-
for (const override of codexProfileConfigOverrides(opts.profile)) {
|
|
88
|
-
parts.push("-c", shellQuote(override));
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
parts.push('$(cat "$PROMPT_FILE")');
|
|
92
|
-
return [
|
|
93
|
-
"#!/usr/bin/env bash",
|
|
94
|
-
"set -euo pipefail",
|
|
95
|
-
`cd ${shellQuote(normalizePathForShell(opts.workdir))}`,
|
|
96
|
-
`PROMPT_FILE=${shellQuote(normalizePathForShell(opts.promptFile))}`,
|
|
97
|
-
`TFX_CODEX_TIMEOUT_MS=${shellQuote(String(opts.timeout ?? ""))}`,
|
|
98
|
-
parts.join(" "),
|
|
99
|
-
"",
|
|
100
|
-
].join("\n");
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export function buildLaunchScript(opts = {}) {
|
|
104
|
-
const dir = join(tmpdir(), "triflux-codex-launch");
|
|
105
|
-
mkdirSync(dir, { recursive: true });
|
|
106
|
-
const path = join(dir, `${String(opts.id || "launch")}.sh`);
|
|
107
|
-
writeFileSync(path, createLaunchScriptText(opts), "utf8");
|
|
108
|
-
return path;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// ── Exec args builder ───────────────────────────────────────────
|
|
112
|
-
|
|
113
|
-
// CTO boundary: this adapter assembles Codex prompts outside
|
|
114
|
-
// scripts/tfx-route.sh. Keep north-star injection in tfx-route.sh until
|
|
115
|
-
// launcher workdir contracts have focused coverage.
|
|
116
|
-
export function buildExecArgs(opts = {}) {
|
|
117
|
-
const prompt = typeof opts.prompt === "string" ? opts.prompt : "";
|
|
118
|
-
const command = buildExecCommand(prompt, opts.resultFile || null, {
|
|
119
|
-
profile: opts.profile,
|
|
120
|
-
codexHome: opts.codexHome,
|
|
121
|
-
disallowUltra: opts.disallowUltra,
|
|
122
|
-
enforceCanonicalProfile: opts.enforceCanonicalProfile,
|
|
123
|
-
skipGitRepoCheck: true,
|
|
124
|
-
sandboxBypass: true,
|
|
125
|
-
cwd: opts.cwd,
|
|
126
|
-
// stdinPrompt 가 명시되면 buildExecCommand 로 forward.
|
|
127
|
-
// headless 워커 spawn (backend.mjs) 은 default (stdin on, macOS hook fix).
|
|
128
|
-
stdinPrompt: opts.stdinPrompt,
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
if (!prompt) return command.replace(/\s+""$/u, "");
|
|
132
|
-
|
|
133
|
-
let result;
|
|
134
|
-
const quotedPrompt = JSON.stringify(prompt);
|
|
135
|
-
// PowerShell: (Get-Content -Raw '...'), bash: "$(cat '...')"
|
|
136
|
-
if (
|
|
137
|
-
(/^\(Get-Content\b[\s\S]*\)$/u.test(prompt) ||
|
|
138
|
-
/^"\$\(cat\b[\s\S]*\)"$/u.test(prompt)) &&
|
|
139
|
-
command.endsWith(quotedPrompt)
|
|
140
|
-
) {
|
|
141
|
-
result = `${command.slice(0, -quotedPrompt.length)}${prompt}`;
|
|
142
|
-
} else {
|
|
143
|
-
result = command;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// stderr 캡처: codex 실패 시에도 원인 추적 가능 (resultFile.err)
|
|
147
|
-
if (opts.resultFile) {
|
|
148
|
-
result += ` 2>'${opts.resultFile}.err'`;
|
|
149
|
-
}
|
|
150
|
-
return result;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// ── Codex execution ─────────────────────────────────────────────
|
|
154
|
-
|
|
155
|
-
async function runCodex(prompt, workdir, preflight, attempt) {
|
|
156
|
-
const dir = join(tmpdir(), "triflux-codex-exec");
|
|
157
|
-
mkdirSync(dir, { recursive: true });
|
|
158
|
-
const resultFile = join(
|
|
159
|
-
dir,
|
|
160
|
-
`codex-${Date.now()}-${Math.random().toString(36).slice(2)}.txt`,
|
|
161
|
-
);
|
|
162
|
-
const command = commandWithOverrides(
|
|
163
|
-
buildExecCommand(prompt, resultFile, {
|
|
164
|
-
profile: attempt.profile,
|
|
165
|
-
skipGitRepoCheck: true,
|
|
166
|
-
sandboxBypass: attempt.forceBypass,
|
|
167
|
-
}),
|
|
168
|
-
prompt,
|
|
169
|
-
preflight.codexPath,
|
|
170
|
-
buildOverrides(attempt.requested, attempt.excluded),
|
|
171
|
-
);
|
|
172
|
-
return runProcess(command, workdir, attempt.timeout, {
|
|
173
|
-
resultFile,
|
|
174
|
-
inferStallMode,
|
|
175
|
-
cli: "codex",
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// ── Public API ──────────────────────────────────────────────────
|
|
180
|
-
|
|
181
|
-
export function execute(opts = {}) {
|
|
182
|
-
return executeWithAttempts({
|
|
183
|
-
runFn: runCodex,
|
|
184
|
-
preflightFn: (o) =>
|
|
185
|
-
runPreflight({
|
|
186
|
-
mcpServers: o.mcpServers,
|
|
187
|
-
subcommand: "exec",
|
|
188
|
-
workdir: o.workdir,
|
|
189
|
-
}),
|
|
190
|
-
buildAttemptsFn: buildAttempts,
|
|
191
|
-
opts,
|
|
192
|
-
});
|
|
193
|
-
}
|