@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
package/hub/codex-preflight.mjs
DELETED
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
import { execSync } from "node:child_process";
|
|
2
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
|
|
6
|
-
import { whichCommandAsync } from "./platform.mjs";
|
|
7
|
-
|
|
8
|
-
let _cachedVersion = null;
|
|
9
|
-
|
|
10
|
-
function escapeRegExp(value) {
|
|
11
|
-
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function readConfigText(configPath = join(homedir(), ".codex", "config.toml")) {
|
|
15
|
-
if (!existsSync(configPath)) return "";
|
|
16
|
-
try {
|
|
17
|
-
return readFileSync(configPath, "utf8");
|
|
18
|
-
} catch {
|
|
19
|
-
return "";
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function readTomlString(text, key) {
|
|
24
|
-
const match = String(text).match(
|
|
25
|
-
new RegExp(`^\\s*${escapeRegExp(key)}\\s*=\\s*"([^"]*)"\\s*$`, "mu"),
|
|
26
|
-
);
|
|
27
|
-
return match?.[1] ?? null;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function readSection(text, name) {
|
|
31
|
-
const lines = String(text).split(/\r?\n/u);
|
|
32
|
-
const header = `[${name}]`;
|
|
33
|
-
const start = lines.findIndex((line) => line.trim() === header);
|
|
34
|
-
if (start < 0) return "";
|
|
35
|
-
const body = [];
|
|
36
|
-
for (const line of lines.slice(start + 1)) {
|
|
37
|
-
if (/^\s*\[[^\]]+\]\s*$/u.test(line)) break;
|
|
38
|
-
body.push(line);
|
|
39
|
-
}
|
|
40
|
-
return body.join("\n");
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* `codex --version` 실행 결과를 파싱하여 마이너 버전 숫자 반환.
|
|
45
|
-
* 파싱 실패 시 0 반환 (구버전으로 간주).
|
|
46
|
-
* @returns {number} 마이너 버전 (예: 0.117.0 → 117)
|
|
47
|
-
*/
|
|
48
|
-
export function getCodexVersion() {
|
|
49
|
-
if (_cachedVersion !== null) return _cachedVersion;
|
|
50
|
-
try {
|
|
51
|
-
const out = execSync("codex --version", {
|
|
52
|
-
encoding: "utf8",
|
|
53
|
-
timeout: 5000,
|
|
54
|
-
}).trim();
|
|
55
|
-
// "codex 0.117.0" 또는 "0.117.0" 형식 대응
|
|
56
|
-
const match = out.match(/(\d+)\.(\d+)\.(\d+)/);
|
|
57
|
-
_cachedVersion = match ? parseInt(match[2], 10) : 0;
|
|
58
|
-
} catch {
|
|
59
|
-
_cachedVersion = 0;
|
|
60
|
-
}
|
|
61
|
-
return _cachedVersion;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async function checkCodexInstalled() {
|
|
65
|
-
const codexPath = await whichCommandAsync("codex");
|
|
66
|
-
if (codexPath) return { codexPath, ok: true, warnings: [] };
|
|
67
|
-
return {
|
|
68
|
-
codexPath: null,
|
|
69
|
-
ok: false,
|
|
70
|
-
warnings: [
|
|
71
|
-
"Codex CLI not found. Install Codex and ensure `codex` is available on PATH.",
|
|
72
|
-
],
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function checkApprovalMode(configText, opts = {}) {
|
|
77
|
-
const approvalPolicy = readTomlString(configText, "approval_policy");
|
|
78
|
-
const subcommand = opts.subcommand || "exec";
|
|
79
|
-
return {
|
|
80
|
-
needsBypass: subcommand === "exec" || approvalPolicy !== "never",
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
async function verifyServerHealth(name, configText) {
|
|
85
|
-
const section = readSection(configText, `mcp_servers.${name}`);
|
|
86
|
-
if (!section)
|
|
87
|
-
return { ok: false, warning: `MCP server '${name}' is not configured.` };
|
|
88
|
-
if (/^\s*enabled\s*=\s*false\s*$/mu.test(section)) {
|
|
89
|
-
return {
|
|
90
|
-
ok: false,
|
|
91
|
-
warning: `MCP server '${name}' is disabled in config.toml.`,
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const command = readTomlString(section, "command");
|
|
96
|
-
if (command) {
|
|
97
|
-
const resolved = await whichCommandAsync(command);
|
|
98
|
-
return resolved
|
|
99
|
-
? { ok: true, warning: "" }
|
|
100
|
-
: {
|
|
101
|
-
ok: false,
|
|
102
|
-
warning: `MCP server '${name}' command not found: ${command}`,
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const url = readTomlString(section, "url");
|
|
107
|
-
if (!url || !/^https?:\/\//u.test(url)) return { ok: true, warning: "" };
|
|
108
|
-
try {
|
|
109
|
-
const response = await fetch(url, { signal: AbortSignal.timeout(2000) });
|
|
110
|
-
return response.status < 500
|
|
111
|
-
? { ok: true, warning: "" }
|
|
112
|
-
: {
|
|
113
|
-
ok: false,
|
|
114
|
-
warning: `MCP server '${name}' returned HTTP ${response.status}.`,
|
|
115
|
-
};
|
|
116
|
-
} catch {
|
|
117
|
-
return {
|
|
118
|
-
ok: false,
|
|
119
|
-
warning: `MCP server '${name}' is unreachable at ${url}.`,
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
async function checkMcpHealth(mcpServers, configText) {
|
|
125
|
-
const excludeMcpServers = [];
|
|
126
|
-
const warnings = [];
|
|
127
|
-
|
|
128
|
-
for (const name of Array.isArray(mcpServers) ? mcpServers : []) {
|
|
129
|
-
const server = String(name ?? "").trim();
|
|
130
|
-
if (!server) continue;
|
|
131
|
-
const result = await verifyServerHealth(server, configText);
|
|
132
|
-
if (!result.ok) excludeMcpServers.push(server);
|
|
133
|
-
if (result.warning) warnings.push(result.warning);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return { excludeMcpServers, warnings };
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Detects codex config drift between global (~/.codex/config.toml) and the
|
|
141
|
-
* target workdir. Covers:
|
|
142
|
-
* 1. workdir-local `.codex/config.toml` overriding global approval_policy/sandbox_mode
|
|
143
|
-
* 2. instruction-file asymmetry (AGENTS.md vs CLAUDE.md)
|
|
144
|
-
*
|
|
145
|
-
* @param {string} [workdir] absolute path to the invocation workdir
|
|
146
|
-
* @param {string} [globalConfigText] already-read ~/.codex/config.toml content
|
|
147
|
-
* @returns {{ warnings: string[] }}
|
|
148
|
-
*/
|
|
149
|
-
export function detectWorkdirDrift(workdir, globalConfigText = "") {
|
|
150
|
-
if (!workdir || typeof workdir !== "string") return { warnings: [] };
|
|
151
|
-
const warnings = [];
|
|
152
|
-
|
|
153
|
-
const localCodexConfig = join(workdir, ".codex", "config.toml");
|
|
154
|
-
if (existsSync(localCodexConfig)) {
|
|
155
|
-
let localText = "";
|
|
156
|
-
try {
|
|
157
|
-
localText = readFileSync(localCodexConfig, "utf8");
|
|
158
|
-
} catch {
|
|
159
|
-
localText = "";
|
|
160
|
-
}
|
|
161
|
-
const keys = ["approval_policy", "sandbox_mode", "model"];
|
|
162
|
-
for (const key of keys) {
|
|
163
|
-
const local = readTomlString(localText, key);
|
|
164
|
-
const global = readTomlString(globalConfigText, key);
|
|
165
|
-
if (local && local !== global) {
|
|
166
|
-
warnings.push(
|
|
167
|
-
`workdir '.codex/config.toml' overrides ${key}='${local}' (global='${global || "unset"}').`,
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const hasAgentsMd = existsSync(join(workdir, "AGENTS.md"));
|
|
174
|
-
const hasClaudeMd = existsSync(join(workdir, "CLAUDE.md"));
|
|
175
|
-
if (hasAgentsMd !== hasClaudeMd) {
|
|
176
|
-
const present = hasAgentsMd ? "AGENTS.md" : "CLAUDE.md";
|
|
177
|
-
const missing = hasAgentsMd ? "CLAUDE.md" : "AGENTS.md";
|
|
178
|
-
warnings.push(
|
|
179
|
-
`workdir has ${present} but not ${missing}; codex and other CLIs may read different instruction sources.`,
|
|
180
|
-
);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
return { warnings };
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
export async function runPreflight(opts = {}) {
|
|
187
|
-
const install = await checkCodexInstalled();
|
|
188
|
-
if (!install.ok) {
|
|
189
|
-
return {
|
|
190
|
-
codexPath: null,
|
|
191
|
-
version: 0,
|
|
192
|
-
needsBypass: true,
|
|
193
|
-
excludeMcpServers: [],
|
|
194
|
-
warnings: install.warnings,
|
|
195
|
-
ok: false,
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const warnings = [...install.warnings];
|
|
200
|
-
const version = getCodexVersion();
|
|
201
|
-
|
|
202
|
-
const configText = readConfigText(opts.configPath);
|
|
203
|
-
const approval = checkApprovalMode(configText, opts);
|
|
204
|
-
|
|
205
|
-
const mcp = await checkMcpHealth(opts.mcpServers, configText);
|
|
206
|
-
warnings.push(...mcp.warnings);
|
|
207
|
-
|
|
208
|
-
const drift = detectWorkdirDrift(opts.workdir, configText);
|
|
209
|
-
warnings.push(...drift.warnings);
|
|
210
|
-
|
|
211
|
-
return {
|
|
212
|
-
codexPath: install.codexPath,
|
|
213
|
-
version,
|
|
214
|
-
needsBypass: approval.needsBypass,
|
|
215
|
-
excludeMcpServers: mcp.excludeMcpServers,
|
|
216
|
-
warnings,
|
|
217
|
-
ok: true,
|
|
218
|
-
};
|
|
219
|
-
}
|
package/hub/lib/cto-env.mjs
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
const OFF_VALUES = new Set(["0", "false", "off", "no"]);
|
|
2
|
-
const ON_VALUES = new Set(["1", "true", "on", "yes"]);
|
|
3
|
-
|
|
4
|
-
function normalizeEnvValue(name) {
|
|
5
|
-
return String(process.env[name] ?? "")
|
|
6
|
-
.trim()
|
|
7
|
-
.toLowerCase();
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
function isExplicitlyOff(name) {
|
|
11
|
-
return OFF_VALUES.has(normalizeEnvValue(name));
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function isExplicitlyOn(name) {
|
|
15
|
-
return ON_VALUES.has(normalizeEnvValue(name));
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function normalizeEnv(env, name) {
|
|
19
|
-
return String(env?.[name] ?? "")
|
|
20
|
-
.trim()
|
|
21
|
-
.toLowerCase();
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function isEnvOff(env, name) {
|
|
25
|
-
return OFF_VALUES.has(normalizeEnv(env, name));
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function isEnvOn(env, name) {
|
|
29
|
-
return ON_VALUES.has(normalizeEnv(env, name));
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Resolve the C6a control plane without mutating environment or runtime
|
|
34
|
-
* state. Callers own storing and incrementing the returned generation.
|
|
35
|
-
*/
|
|
36
|
-
export function resolveRoleControlSnapshot(env = process.env, options = {}) {
|
|
37
|
-
const masterEnabled = !isEnvOff(env, "TFX_CTO");
|
|
38
|
-
const ctoManagerEnabled = masterEnabled && isEnvOn(env, "TFX_CTO_MANAGER");
|
|
39
|
-
const leadManagerEnabled =
|
|
40
|
-
ctoManagerEnabled && isEnvOn(env, "TFX_LEAD_MANAGER");
|
|
41
|
-
const northStarEnabled = masterEnabled && isEnvOn(env, "TFX_CTO_NORTH_STAR");
|
|
42
|
-
const autoCollectEnabled =
|
|
43
|
-
masterEnabled && isEnvOn(env, "TFX_CTO_AUTO_COLLECT");
|
|
44
|
-
const values = {
|
|
45
|
-
master_enabled: masterEnabled,
|
|
46
|
-
cto_manager_enabled: ctoManagerEnabled,
|
|
47
|
-
lead_manager_enabled: leadManagerEnabled,
|
|
48
|
-
north_star_enabled: northStarEnabled,
|
|
49
|
-
auto_collect_enabled: autoCollectEnabled,
|
|
50
|
-
};
|
|
51
|
-
const previous = options.previous;
|
|
52
|
-
const changed = Object.keys(values).some(
|
|
53
|
-
(field) => previous?.[field] !== values[field],
|
|
54
|
-
);
|
|
55
|
-
const generation = previous
|
|
56
|
-
? Number(previous.generation || 0) + (changed ? 1 : 0)
|
|
57
|
-
: Number.isSafeInteger(options.generation) && options.generation >= 0
|
|
58
|
-
? options.generation
|
|
59
|
-
: 0;
|
|
60
|
-
return { generation, ...values };
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function isCtoDisabled() {
|
|
64
|
-
return isExplicitlyOff("TFX_CTO");
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function isCtoRetentionEnabled() {
|
|
68
|
-
if (isExplicitlyOff("TFX_CTO")) return false;
|
|
69
|
-
return isExplicitlyOn("TFX_CTO_RETENTION");
|
|
70
|
-
}
|
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync, statSync } from "fs";
|
|
2
|
-
import { basename, dirname, isAbsolute, resolve } from "path";
|
|
3
|
-
|
|
4
|
-
const URL_LIKE_RE = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//u;
|
|
5
|
-
const WINDOWS_ABS_RE = /^[a-zA-Z]:[\\/]/u;
|
|
6
|
-
|
|
7
|
-
function normalizeMarkdownTarget(raw) {
|
|
8
|
-
const trimmed = String(raw || "").trim();
|
|
9
|
-
const match = /^<?([^>\s]+)>?(?:\s+["'][^"']*["'])?$/u.exec(trimmed);
|
|
10
|
-
return match ? match[1] : trimmed;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function sanitizeToken(raw) {
|
|
14
|
-
let token = String(raw || "").trim();
|
|
15
|
-
if (!token) return "";
|
|
16
|
-
token = token
|
|
17
|
-
.replace(/^[<(]+/u, "")
|
|
18
|
-
.replace(/[)>.,;:!?]+$/u, "")
|
|
19
|
-
.trim();
|
|
20
|
-
return token;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function isLikelyPathToken(token) {
|
|
24
|
-
if (!token || URL_LIKE_RE.test(token)) return false;
|
|
25
|
-
if (
|
|
26
|
-
token.startsWith("app://") ||
|
|
27
|
-
token.startsWith("plugin://") ||
|
|
28
|
-
token.startsWith("mention://")
|
|
29
|
-
)
|
|
30
|
-
return false;
|
|
31
|
-
if (token === "~") return false;
|
|
32
|
-
|
|
33
|
-
const hasPathSeparator = token.includes("/") || token.includes("\\");
|
|
34
|
-
const hasDotPrefix =
|
|
35
|
-
token.startsWith("./") || token.startsWith("../") || token.startsWith(".");
|
|
36
|
-
const isHomeRelative = token.startsWith("~/") || token.startsWith("~\\");
|
|
37
|
-
const isWindowsAbs = WINDOWS_ABS_RE.test(token);
|
|
38
|
-
const hasFileLikeSuffix = /\.[a-zA-Z0-9]{1,16}$/u.test(token);
|
|
39
|
-
|
|
40
|
-
return (
|
|
41
|
-
hasPathSeparator ||
|
|
42
|
-
hasDotPrefix ||
|
|
43
|
-
isHomeRelative ||
|
|
44
|
-
isWindowsAbs ||
|
|
45
|
-
hasFileLikeSuffix
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function extractExplicitFileTokens(text) {
|
|
50
|
-
const content = String(text || "");
|
|
51
|
-
const candidates = [];
|
|
52
|
-
|
|
53
|
-
for (const match of content.matchAll(/\[[^\]]+\]\(([^)\n]+)\)/gu)) {
|
|
54
|
-
candidates.push(normalizeMarkdownTarget(match[1]));
|
|
55
|
-
}
|
|
56
|
-
for (const match of content.matchAll(/`([^`\n]+)`/gu)) {
|
|
57
|
-
candidates.push(match[1]);
|
|
58
|
-
}
|
|
59
|
-
for (const match of content.matchAll(/"([^"\n]+)"/gu)) {
|
|
60
|
-
candidates.push(match[1]);
|
|
61
|
-
}
|
|
62
|
-
for (const match of content.matchAll(/'([^'\n]+)'/gu)) {
|
|
63
|
-
candidates.push(match[1]);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const unique = new Set();
|
|
67
|
-
for (const raw of candidates) {
|
|
68
|
-
const token = sanitizeToken(raw);
|
|
69
|
-
if (!isLikelyPathToken(token)) continue;
|
|
70
|
-
unique.add(token);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return Array.from(unique);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function validateTransferFile(filePath, maxBytes) {
|
|
77
|
-
if (!existsSync(filePath)) {
|
|
78
|
-
throw new Error(`referenced file not found: ${filePath}`);
|
|
79
|
-
}
|
|
80
|
-
const stats = statSync(filePath);
|
|
81
|
-
if (!stats.isFile()) {
|
|
82
|
-
throw new Error(`referenced path is not a file: ${filePath}`);
|
|
83
|
-
}
|
|
84
|
-
if (stats.size > maxBytes) {
|
|
85
|
-
throw new Error(
|
|
86
|
-
`referenced file too large: ${stats.size} bytes (max ${maxBytes}) for ${filePath}`,
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
return stats.size;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function resolveReferencePath(token, handoffAbsPath, cwd) {
|
|
93
|
-
if (WINDOWS_ABS_RE.test(token) || isAbsolute(token)) {
|
|
94
|
-
return resolve(token);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (token.startsWith("~/") || token.startsWith("~\\")) {
|
|
98
|
-
if (!process.env.HOME && !process.env.USERPROFILE) {
|
|
99
|
-
return null;
|
|
100
|
-
}
|
|
101
|
-
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
102
|
-
return resolve(homeDir, token.slice(2));
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const handoffDir = dirname(handoffAbsPath);
|
|
106
|
-
const primary = resolve(handoffDir, token);
|
|
107
|
-
if (existsSync(primary)) {
|
|
108
|
-
return primary;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const fallback = resolve(cwd, token);
|
|
112
|
-
return existsSync(fallback) ? fallback : primary;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function toRemotePath(stageRoot, relativePath) {
|
|
116
|
-
const root = String(stageRoot).replace(/\\/gu, "/").replace(/\/+$/u, "");
|
|
117
|
-
const rel = String(relativePath).replace(/\\/gu, "/").replace(/^\/+/, "");
|
|
118
|
-
return `${root}/${rel}`;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
export function buildRemoteTransferPlan(options = {}) {
|
|
122
|
-
const {
|
|
123
|
-
cwd = process.cwd(),
|
|
124
|
-
handoffPath = null,
|
|
125
|
-
maxBytes,
|
|
126
|
-
remoteStageRoot,
|
|
127
|
-
userPrompt = null,
|
|
128
|
-
maxReferenceFiles = 32,
|
|
129
|
-
} = options;
|
|
130
|
-
|
|
131
|
-
if (!handoffPath) {
|
|
132
|
-
return {
|
|
133
|
-
prompt: userPrompt || "",
|
|
134
|
-
replacements: [],
|
|
135
|
-
stagedHandoffPath: null,
|
|
136
|
-
transfers: [],
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (!remoteStageRoot) {
|
|
141
|
-
throw new Error("remoteStageRoot is required when handoffPath is provided");
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (!Number.isFinite(maxBytes) || maxBytes <= 0) {
|
|
145
|
-
throw new Error("maxBytes must be a positive number");
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const absoluteHandoffPath = resolve(cwd, handoffPath);
|
|
149
|
-
validateTransferFile(absoluteHandoffPath, maxBytes);
|
|
150
|
-
const handoffContent = readFileSync(absoluteHandoffPath, "utf8").trim();
|
|
151
|
-
|
|
152
|
-
const stagedHandoffPath = toRemotePath(
|
|
153
|
-
remoteStageRoot,
|
|
154
|
-
`handoff/${basename(absoluteHandoffPath)}`,
|
|
155
|
-
);
|
|
156
|
-
const transfers = [
|
|
157
|
-
{
|
|
158
|
-
localPath: absoluteHandoffPath,
|
|
159
|
-
remotePath: stagedHandoffPath,
|
|
160
|
-
type: "handoff",
|
|
161
|
-
},
|
|
162
|
-
];
|
|
163
|
-
|
|
164
|
-
const tokens = extractExplicitFileTokens(handoffContent);
|
|
165
|
-
if (tokens.length > maxReferenceFiles) {
|
|
166
|
-
throw new Error(
|
|
167
|
-
`too many referenced files: ${tokens.length} (max ${maxReferenceFiles})`,
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const stagedByLocalPath = new Map();
|
|
172
|
-
const replacements = [];
|
|
173
|
-
let fileIndex = 0;
|
|
174
|
-
|
|
175
|
-
for (const token of tokens) {
|
|
176
|
-
const resolvedPath = resolveReferencePath(token, absoluteHandoffPath, cwd);
|
|
177
|
-
validateTransferFile(resolvedPath, maxBytes);
|
|
178
|
-
|
|
179
|
-
if (!stagedByLocalPath.has(resolvedPath)) {
|
|
180
|
-
fileIndex += 1;
|
|
181
|
-
const stagedPath = toRemotePath(
|
|
182
|
-
remoteStageRoot,
|
|
183
|
-
`refs/${String(fileIndex).padStart(2, "0")}-${basename(resolvedPath)}`,
|
|
184
|
-
);
|
|
185
|
-
stagedByLocalPath.set(resolvedPath, stagedPath);
|
|
186
|
-
transfers.push({
|
|
187
|
-
localPath: resolvedPath,
|
|
188
|
-
remotePath: stagedPath,
|
|
189
|
-
type: "reference",
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
replacements.push({
|
|
194
|
-
token,
|
|
195
|
-
stagedPath: stagedByLocalPath.get(resolvedPath),
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const replacementEntries = Array.from(
|
|
200
|
-
replacements
|
|
201
|
-
.reduce((map, entry) => map.set(entry.token, entry.stagedPath), new Map())
|
|
202
|
-
.entries(),
|
|
203
|
-
).sort((a, b) => b[0].length - a[0].length);
|
|
204
|
-
|
|
205
|
-
let rewrittenHandoff = handoffContent;
|
|
206
|
-
for (const [token, stagedPath] of replacementEntries) {
|
|
207
|
-
rewrittenHandoff = rewrittenHandoff.split(token).join(stagedPath);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const prefix = `Staged handoff file: ${stagedHandoffPath}`;
|
|
211
|
-
let prompt = rewrittenHandoff ? `${prefix}\n\n${rewrittenHandoff}` : prefix;
|
|
212
|
-
|
|
213
|
-
if (userPrompt) {
|
|
214
|
-
prompt = `${prompt}\n\n---\n\n${userPrompt}`;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
return {
|
|
218
|
-
prompt,
|
|
219
|
-
replacements: replacementEntries.map(([token, stagedPath]) => ({
|
|
220
|
-
stagedPath,
|
|
221
|
-
token,
|
|
222
|
-
})),
|
|
223
|
-
stagedHandoffPath,
|
|
224
|
-
transfers,
|
|
225
|
-
};
|
|
226
|
-
}
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
// Codex와 Antigravity 세션 hook이 공유하는 presence 기록 함수.
|
|
2
|
-
import { execFileSync } from "node:child_process";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { resolveRoleControlSnapshot } from "../../hub/lib/cto-env.mjs";
|
|
5
|
-
|
|
6
|
-
function parseStartPayload(stdinData) {
|
|
7
|
-
try {
|
|
8
|
-
const raw = typeof stdinData === "string" ? stdinData : "";
|
|
9
|
-
return raw.trim() ? JSON.parse(raw) : {};
|
|
10
|
-
} catch {
|
|
11
|
-
return {};
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function inferParticipantCli(payload) {
|
|
16
|
-
const direct = String(
|
|
17
|
-
payload?.actor_cli || payload?.cli || payload?.actor?.cli || "",
|
|
18
|
-
)
|
|
19
|
-
.trim()
|
|
20
|
-
.toLowerCase();
|
|
21
|
-
if (direct) return direct;
|
|
22
|
-
|
|
23
|
-
const eventName = String(payload?.hook_event_name || "").trim();
|
|
24
|
-
if (eventName === "SessionStart") return "claude";
|
|
25
|
-
if (eventName.toLowerCase().replace(/-/g, "_") === "session_start") {
|
|
26
|
-
return "codex";
|
|
27
|
-
}
|
|
28
|
-
return "participant";
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
async function defaultAppendParticipantCtoEvent(lakeRoot, event) {
|
|
32
|
-
const { appendCtoEvent } = await import("../../cto/events.mjs");
|
|
33
|
-
return appendCtoEvent(lakeRoot, event, {
|
|
34
|
-
lockRetries: 1,
|
|
35
|
-
lockRetryDelayMs: 0,
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async function defaultResolveParticipantLakeRoot(cwd) {
|
|
40
|
-
const { resolveLakeRootDir } = await import("../../cto/lake-root.mjs");
|
|
41
|
-
const projectRoot = resolveLakeRootDir(cwd, {
|
|
42
|
-
execFileSync: (cmd, args, options = {}) =>
|
|
43
|
-
execFileSync(cmd, args, {
|
|
44
|
-
...options,
|
|
45
|
-
timeout: 500,
|
|
46
|
-
killSignal: "SIGKILL",
|
|
47
|
-
}),
|
|
48
|
-
});
|
|
49
|
-
return {
|
|
50
|
-
projectRoot,
|
|
51
|
-
lakeRoot: projectRoot ? join(projectRoot, ".triflux", "lake") : "",
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** 참가 세션의 시작 이벤트만 기록한다. */
|
|
56
|
-
export async function emitParticipantSessionStarted(stdinData, seams = {}) {
|
|
57
|
-
try {
|
|
58
|
-
if (
|
|
59
|
-
!resolveRoleControlSnapshot(seams.env || process.env).auto_collect_enabled
|
|
60
|
-
) {
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
|
-
const payload = parseStartPayload(stdinData);
|
|
64
|
-
const sessionId = String(payload?.session_id || "").trim();
|
|
65
|
-
if (!sessionId) return null;
|
|
66
|
-
const cwd = typeof payload?.cwd === "string" ? payload.cwd : process.cwd();
|
|
67
|
-
const resolveLakeRoot =
|
|
68
|
-
seams.resolveLakeRoot || defaultResolveParticipantLakeRoot;
|
|
69
|
-
const resolved = await resolveLakeRoot(cwd);
|
|
70
|
-
const projectRoot =
|
|
71
|
-
typeof resolved === "string"
|
|
72
|
-
? resolved
|
|
73
|
-
: String(resolved?.projectRoot || "");
|
|
74
|
-
const lakeRoot =
|
|
75
|
-
typeof resolved === "string"
|
|
76
|
-
? join(resolved, ".triflux", "lake")
|
|
77
|
-
: String(resolved?.lakeRoot || "");
|
|
78
|
-
if (!lakeRoot) return null;
|
|
79
|
-
|
|
80
|
-
const event = {
|
|
81
|
-
event: "session_started",
|
|
82
|
-
source: "tfx_participant_hook",
|
|
83
|
-
session_id: sessionId,
|
|
84
|
-
project_root: projectRoot || cwd,
|
|
85
|
-
worktree_path: cwd,
|
|
86
|
-
branch: typeof payload?.branch === "string" ? payload.branch : "",
|
|
87
|
-
status: "active",
|
|
88
|
-
actor: {
|
|
89
|
-
cli: inferParticipantCli(payload),
|
|
90
|
-
session_id: sessionId,
|
|
91
|
-
host: typeof payload?.host === "string" ? payload.host : "local",
|
|
92
|
-
},
|
|
93
|
-
summary: `${inferParticipantCli(payload)} session_started ${sessionId}`,
|
|
94
|
-
now: seams.now,
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
const append = seams.ctoAppend || defaultAppendParticipantCtoEvent;
|
|
98
|
-
return await append(lakeRoot, event);
|
|
99
|
-
} catch {
|
|
100
|
-
return null;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function readAncestorCommands(pid = process.ppid, maxDepth = 6) {
|
|
105
|
-
if (process.platform === "win32") return [];
|
|
106
|
-
const commands = [];
|
|
107
|
-
let currentPid = Number(pid);
|
|
108
|
-
for (let depth = 0; depth < maxDepth; depth++) {
|
|
109
|
-
if (!Number.isInteger(currentPid) || currentPid <= 1) break;
|
|
110
|
-
try {
|
|
111
|
-
const output = execFileSync(
|
|
112
|
-
"ps",
|
|
113
|
-
["-o", "ppid=", "-o", "command=", "-p", String(currentPid)],
|
|
114
|
-
{
|
|
115
|
-
encoding: "utf8",
|
|
116
|
-
timeout: 200,
|
|
117
|
-
windowsHide: true,
|
|
118
|
-
},
|
|
119
|
-
).trim();
|
|
120
|
-
const match = output.match(/^(\d+)\s+([\s\S]+)$/);
|
|
121
|
-
if (!match) break;
|
|
122
|
-
commands.push(match[2]);
|
|
123
|
-
currentPid = Number(match[1]);
|
|
124
|
-
} catch {
|
|
125
|
-
break;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
return commands;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function commandUsesClaudePrintMode(command) {
|
|
132
|
-
return /\bclaude(?:\s+\S+)*\s+(?:--print|-p)(?:\s|=|$)/u.test(
|
|
133
|
-
String(command || ""),
|
|
134
|
-
);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
export function shouldSkipInteractiveRegistration(payload, seams = {}) {
|
|
138
|
-
const declaredKind = String(
|
|
139
|
-
payload?.sessionKind || payload?.session_kind || "",
|
|
140
|
-
)
|
|
141
|
-
.trim()
|
|
142
|
-
.toLowerCase();
|
|
143
|
-
if (declaredKind === "headless") return true;
|
|
144
|
-
|
|
145
|
-
const ancestorCommands = Array.isArray(seams.ancestorCommands)
|
|
146
|
-
? seams.ancestorCommands
|
|
147
|
-
: readAncestorCommands(seams.parentPid);
|
|
148
|
-
return ancestorCommands.some((command) =>
|
|
149
|
-
commandUsesClaudePrintMode(command),
|
|
150
|
-
);
|
|
151
|
-
}
|