@triflux/core 10.32.0 → 10.33.1
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/claude-cwd-projection-refresh.mjs +83 -0
- package/hooks/codex-session-hook.mjs +47 -18
- package/hooks/hook-orchestrator.mjs +22 -0
- package/hooks/hook-registry.json +35 -0
- package/hooks/hooks.json +12 -0
- package/hub/bridge.mjs +3 -1
- package/hub/codex-adapter.mjs +3 -0
- package/hub/lib/tfx-route-args.mjs +10 -3
- package/hub/team/claude-agent-session-normalizer.mjs +53 -0
- package/hub/team/claude-daemon-control.mjs +17 -13
- package/hub/team/claude-session-projection.mjs +57 -0
- package/package.json +1 -1
- package/scripts/lib/env-probe.mjs +35 -8
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
import { refreshClaudeSessionProjectionCwd } from "../hub/team/claude-session-projection.mjs";
|
|
8
|
+
|
|
9
|
+
function readStdin() {
|
|
10
|
+
try {
|
|
11
|
+
return fs.readFile(0, "utf8");
|
|
12
|
+
} catch {
|
|
13
|
+
return Promise.resolve("");
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function resolveCwdChangedPayload(input = {}, env = process.env) {
|
|
18
|
+
const sessionId = String(
|
|
19
|
+
input.session_id ??
|
|
20
|
+
input.sessionId ??
|
|
21
|
+
input.session?.id ??
|
|
22
|
+
env.CLAUDE_CODE_SESSION_ID ??
|
|
23
|
+
env.CLAUDE_SESSION_ID ??
|
|
24
|
+
"",
|
|
25
|
+
).trim();
|
|
26
|
+
const cwd = String(
|
|
27
|
+
input.cwd ??
|
|
28
|
+
input.new_cwd ??
|
|
29
|
+
input.newCwd ??
|
|
30
|
+
input.current_cwd ??
|
|
31
|
+
input.workspace?.cwd ??
|
|
32
|
+
input.source?.cwd ??
|
|
33
|
+
"",
|
|
34
|
+
).trim();
|
|
35
|
+
const configDir = path.resolve(
|
|
36
|
+
env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude"),
|
|
37
|
+
);
|
|
38
|
+
return {
|
|
39
|
+
eventName: String(input.hook_event_name || ""),
|
|
40
|
+
sessionId,
|
|
41
|
+
cwd,
|
|
42
|
+
sessionsDir: path.join(configDir, "sessions"),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function handleCwdChangedHook(stdinText, env = process.env) {
|
|
47
|
+
let input = {};
|
|
48
|
+
try {
|
|
49
|
+
input = stdinText.trim() ? JSON.parse(stdinText) : {};
|
|
50
|
+
} catch {
|
|
51
|
+
return { code: 0, stdout: "" };
|
|
52
|
+
}
|
|
53
|
+
const payload = resolveCwdChangedPayload(input, env);
|
|
54
|
+
if (payload.eventName && payload.eventName !== "CwdChanged") {
|
|
55
|
+
return { code: 0, stdout: "" };
|
|
56
|
+
}
|
|
57
|
+
if (!payload.sessionId || !payload.cwd) return { code: 0, stdout: "" };
|
|
58
|
+
|
|
59
|
+
const result = await refreshClaudeSessionProjectionCwd({
|
|
60
|
+
sessionsDir: payload.sessionsDir,
|
|
61
|
+
sessionId: payload.sessionId,
|
|
62
|
+
cwd: payload.cwd,
|
|
63
|
+
});
|
|
64
|
+
if (!result.updated) return { code: 0, stdout: "" };
|
|
65
|
+
return {
|
|
66
|
+
code: 0,
|
|
67
|
+
stdout: JSON.stringify({
|
|
68
|
+
hookSpecificOutput: {
|
|
69
|
+
hookEventName: "CwdChanged",
|
|
70
|
+
additionalContext: `Triflux projection cwd refreshed: ${payload.cwd}`,
|
|
71
|
+
},
|
|
72
|
+
}),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (
|
|
77
|
+
process.argv[1] &&
|
|
78
|
+
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
|
79
|
+
) {
|
|
80
|
+
const result = await handleCwdChangedHook(await readStdin());
|
|
81
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
82
|
+
process.exit(result.code);
|
|
83
|
+
}
|
|
@@ -51,6 +51,33 @@ function normalizeMode(mode, payload) {
|
|
|
51
51
|
return "";
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
function swallowStdoutWrite(_chunk, encodingOrCallback, callback) {
|
|
55
|
+
const done =
|
|
56
|
+
typeof encodingOrCallback === "function" ? encodingOrCallback : callback;
|
|
57
|
+
if (typeof done === "function") done();
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function runHookSideEffectsWithStdoutSuppressed(fn) {
|
|
62
|
+
const originalStdoutWrite = stdout.write;
|
|
63
|
+
const originalConsoleDebug = console.debug;
|
|
64
|
+
const originalConsoleInfo = console.info;
|
|
65
|
+
const originalConsoleLog = console.log;
|
|
66
|
+
|
|
67
|
+
stdout.write = swallowStdoutWrite;
|
|
68
|
+
console.debug = () => {};
|
|
69
|
+
console.info = () => {};
|
|
70
|
+
console.log = () => {};
|
|
71
|
+
try {
|
|
72
|
+
return await fn();
|
|
73
|
+
} finally {
|
|
74
|
+
stdout.write = originalStdoutWrite;
|
|
75
|
+
console.debug = originalConsoleDebug;
|
|
76
|
+
console.info = originalConsoleInfo;
|
|
77
|
+
console.log = originalConsoleLog;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
54
81
|
export async function runCodexSessionHook(stdinData, opts = {}) {
|
|
55
82
|
const output = "{}\n";
|
|
56
83
|
const parsed = parsePayload(stdinData);
|
|
@@ -66,24 +93,26 @@ export async function runCodexSessionHook(stdinData, opts = {}) {
|
|
|
66
93
|
opts.drainPendingSynapse || defaultDrainPendingSynapse;
|
|
67
94
|
|
|
68
95
|
try {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
96
|
+
await runHookSideEffectsWithStdoutSuppressed(async () => {
|
|
97
|
+
if (mode === "register") {
|
|
98
|
+
try {
|
|
99
|
+
await hubEnsureRun(stdinData);
|
|
100
|
+
} catch {}
|
|
101
|
+
try {
|
|
102
|
+
registerInteractiveSession(stdinData);
|
|
103
|
+
} catch {}
|
|
104
|
+
try {
|
|
105
|
+
await drainPendingSynapse(1000);
|
|
106
|
+
} catch {}
|
|
107
|
+
} else if (mode === "heartbeat") {
|
|
108
|
+
try {
|
|
109
|
+
heartbeatInteractiveSession(stdinData);
|
|
110
|
+
} catch {}
|
|
111
|
+
try {
|
|
112
|
+
await drainPendingSynapse(500);
|
|
113
|
+
} catch {}
|
|
114
|
+
}
|
|
115
|
+
});
|
|
87
116
|
} catch {
|
|
88
117
|
// Codex session hooks are observational and must never block the session.
|
|
89
118
|
}
|
|
@@ -465,6 +465,28 @@ async function main() {
|
|
|
465
465
|
}
|
|
466
466
|
}
|
|
467
467
|
|
|
468
|
+
// ── Stop: interactive 세션 liveness heartbeat (턴 종료 시) ──
|
|
469
|
+
// UserPromptSubmit 은 턴 *시작* 에, Stop 은 매 어시스턴트 턴 *종료* 에 heartbeat
|
|
470
|
+
// 한다. 둘이 함께 "대화 중이지만 매 분 새 프롬프트를 보내지는 않는" 세션을 5분
|
|
471
|
+
// interactive TTL 위로 유지해, 대화 도중 stale 로 떨어져 `cto status`
|
|
472
|
+
// live_sessions / tray 에서 사라지는 것을 막는다. 진짜 완전 idle(턴 자체가 없음)
|
|
473
|
+
// 은 여전히 TTL 후 stale 로 가며 — 그게 의도된 dead-session 신호다. UserPromptSubmit
|
|
474
|
+
// 과 동일하게 fire-and-forget POST 직후 짧은 상한으로 drain 한다.
|
|
475
|
+
if (eventName === "Stop") {
|
|
476
|
+
try {
|
|
477
|
+
const { heartbeatInteractiveSession } = await import(
|
|
478
|
+
"./session-start-fast.mjs"
|
|
479
|
+
);
|
|
480
|
+
heartbeatInteractiveSession(stdinRaw);
|
|
481
|
+
const { drainPendingSynapse } = await import(
|
|
482
|
+
"../hub/team/synapse-http.mjs"
|
|
483
|
+
);
|
|
484
|
+
await drainPendingSynapse(500);
|
|
485
|
+
} catch {
|
|
486
|
+
/* best-effort — heartbeat 실패가 Stop 을 막지 않는다 */
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
468
490
|
// 이벤트에 해당하는 훅 목록
|
|
469
491
|
const hooks = registry.events[eventName];
|
|
470
492
|
if (!hooks || hooks.length === 0) process.exit(0);
|
package/hooks/hook-registry.json
CHANGED
|
@@ -8,6 +8,19 @@
|
|
|
8
8
|
"external_priority": 100
|
|
9
9
|
},
|
|
10
10
|
"events": {
|
|
11
|
+
"CwdChanged": [
|
|
12
|
+
{
|
|
13
|
+
"id": "tfx-claude-cwd-projection-refresh",
|
|
14
|
+
"source": "triflux",
|
|
15
|
+
"matcher": "*",
|
|
16
|
+
"command": "node \"${PLUGIN_ROOT}/hooks/claude-cwd-projection-refresh.mjs\"",
|
|
17
|
+
"priority": 0,
|
|
18
|
+
"enabled": true,
|
|
19
|
+
"timeout": 2,
|
|
20
|
+
"blocking": false,
|
|
21
|
+
"description": "Claude /cd CwdChanged 이벤트를 Triflux native-bridge projection cwd에 반영"
|
|
22
|
+
}
|
|
23
|
+
],
|
|
11
24
|
"PermissionRequest": [
|
|
12
25
|
{
|
|
13
26
|
"id": "tfx-permission-safe-allow",
|
|
@@ -158,6 +171,17 @@
|
|
|
158
171
|
"timeout": 5,
|
|
159
172
|
"blocking": false,
|
|
160
173
|
"description": "키워드 매칭 → 스킬 자동 라우팅"
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
"id": "tfx-cto-north-star-brief",
|
|
177
|
+
"source": "triflux",
|
|
178
|
+
"matcher": "*",
|
|
179
|
+
"command": "node \"${PLUGIN_ROOT}/hooks/cto-north-star-brief.mjs\"",
|
|
180
|
+
"priority": 2,
|
|
181
|
+
"enabled": true,
|
|
182
|
+
"timeout": 2,
|
|
183
|
+
"blocking": false,
|
|
184
|
+
"description": "CTO north-star brief를 UserPromptSubmit additionalContext로 변경 시 주입"
|
|
161
185
|
}
|
|
162
186
|
],
|
|
163
187
|
"SessionStart": [
|
|
@@ -227,6 +251,17 @@
|
|
|
227
251
|
"blocking": false,
|
|
228
252
|
"description": "이전 세션의 stale tfx-multi 상태 파일 정리 (#62)"
|
|
229
253
|
},
|
|
254
|
+
{
|
|
255
|
+
"id": "tfx-session-lake",
|
|
256
|
+
"source": "triflux",
|
|
257
|
+
"matcher": "*",
|
|
258
|
+
"command": "node \"${PLUGIN_ROOT}/hooks/session-start-lake.mjs\"",
|
|
259
|
+
"priority": 6,
|
|
260
|
+
"enabled": true,
|
|
261
|
+
"timeout": 3,
|
|
262
|
+
"blocking": false,
|
|
263
|
+
"description": "CTO north-star brief를 SessionStart additionalContext로 주입"
|
|
264
|
+
},
|
|
230
265
|
{
|
|
231
266
|
"id": "ext-session-vault-start",
|
|
232
267
|
"source": "session-vault",
|
package/hooks/hooks.json
CHANGED
|
@@ -13,6 +13,18 @@
|
|
|
13
13
|
]
|
|
14
14
|
}
|
|
15
15
|
],
|
|
16
|
+
"CwdChanged": [
|
|
17
|
+
{
|
|
18
|
+
"matcher": "*",
|
|
19
|
+
"hooks": [
|
|
20
|
+
{
|
|
21
|
+
"type": "command",
|
|
22
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/hook-orchestrator.mjs\"",
|
|
23
|
+
"timeout": 5
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
],
|
|
16
28
|
"UserPromptSubmit": [
|
|
17
29
|
{
|
|
18
30
|
"matcher": "*",
|
package/hub/bridge.mjs
CHANGED
|
@@ -1207,6 +1207,7 @@ async function cmdDaemonProbe(args) {
|
|
|
1207
1207
|
const payload = readBridgePayload(args);
|
|
1208
1208
|
const {
|
|
1209
1209
|
deriveClaudeDaemonPaths,
|
|
1210
|
+
extractClaudeAgentSessions,
|
|
1210
1211
|
findDaemonJobBySessionId,
|
|
1211
1212
|
findDaemonJobByShort,
|
|
1212
1213
|
sendClaudeControlRequest,
|
|
@@ -1224,11 +1225,12 @@ async function cmdDaemonProbe(args) {
|
|
|
1224
1225
|
: payload.short
|
|
1225
1226
|
? findDaemonJobByShort(list, payload.short)
|
|
1226
1227
|
: null;
|
|
1228
|
+
const sessions = extractClaudeAgentSessions(list);
|
|
1227
1229
|
|
|
1228
1230
|
return emitJson({
|
|
1229
1231
|
ok: list?.ok !== false,
|
|
1230
1232
|
controlSock: daemonPaths.controlSock,
|
|
1231
|
-
sessions
|
|
1233
|
+
sessions,
|
|
1232
1234
|
target: target || undefined,
|
|
1233
1235
|
error: list?.ok === false ? list?.error : undefined,
|
|
1234
1236
|
});
|
package/hub/codex-adapter.mjs
CHANGED
|
@@ -101,6 +101,9 @@ export function buildLaunchScript(opts = {}) {
|
|
|
101
101
|
|
|
102
102
|
// ── Exec args builder ───────────────────────────────────────────
|
|
103
103
|
|
|
104
|
+
// CTO boundary: this adapter assembles Codex prompts outside
|
|
105
|
+
// scripts/tfx-route.sh. Keep north-star injection in tfx-route.sh until
|
|
106
|
+
// launcher/circuit-broker workdir contracts have focused coverage.
|
|
104
107
|
export function buildExecArgs(opts = {}) {
|
|
105
108
|
const prompt = typeof opts.prompt === "string" ? opts.prompt : "";
|
|
106
109
|
const command = buildExecCommand(prompt, opts.resultFile || null, {
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// maxIterations, task, warnings}.
|
|
8
8
|
//
|
|
9
9
|
// 기존 플래그 (Phase 2 v10.9.33+):
|
|
10
|
-
// --cli {auto|codex|
|
|
10
|
+
// --cli {auto|codex|antigravity|claude}
|
|
11
11
|
// --mode {quick|deep|consensus}
|
|
12
12
|
// --parallel {1|N|swarm}
|
|
13
13
|
// --retry {0|1|ralph|auto-escalate} (Phase 3 에서 ralph/auto-escalate 신규)
|
|
@@ -34,7 +34,7 @@ export const DEFAULT_OPTIONS = Object.freeze({
|
|
|
34
34
|
});
|
|
35
35
|
|
|
36
36
|
const VALID_VALUES = Object.freeze({
|
|
37
|
-
cli: ["auto", "codex", "
|
|
37
|
+
cli: ["auto", "codex", "antigravity", "claude"],
|
|
38
38
|
mode: ["quick", "deep", "consensus"],
|
|
39
39
|
retry: ["0", "1", "ralph", "auto-escalate"],
|
|
40
40
|
isolation: ["none", "worktree"],
|
|
@@ -158,7 +158,14 @@ function applyBool(opts, flag) {
|
|
|
158
158
|
function applyValue(opts, flag, value, warnings) {
|
|
159
159
|
switch (flag) {
|
|
160
160
|
case "--cli":
|
|
161
|
-
|
|
161
|
+
if (value === "gemini") {
|
|
162
|
+
opts.cli = "antigravity";
|
|
163
|
+
warnings.push(
|
|
164
|
+
"--cli gemini is deprecated; using --cli antigravity instead",
|
|
165
|
+
);
|
|
166
|
+
} else {
|
|
167
|
+
opts.cli = value;
|
|
168
|
+
}
|
|
162
169
|
break;
|
|
163
170
|
case "--mode":
|
|
164
171
|
opts.mode = value;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
function firstString(...values) {
|
|
2
|
+
for (const value of values) {
|
|
3
|
+
const text = String(value ?? "").trim();
|
|
4
|
+
if (text) return text;
|
|
5
|
+
}
|
|
6
|
+
return "";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function maybeAssign(target, key, value) {
|
|
10
|
+
if (value !== "" && value != null) target[key] = value;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function normalizeClaudeAgentSession(row = {}) {
|
|
14
|
+
const short = firstString(row.short, row.id, row.jobId, row.job_id);
|
|
15
|
+
const sessionId = firstString(
|
|
16
|
+
row.sessionId,
|
|
17
|
+
row.session_id,
|
|
18
|
+
row.dispatch?.sessionId,
|
|
19
|
+
row.dispatch?.session_id,
|
|
20
|
+
row.d?.sessionId,
|
|
21
|
+
row.d?.session_id,
|
|
22
|
+
row.session?.id,
|
|
23
|
+
);
|
|
24
|
+
const state = firstString(row.state, row.status, row.tempo, "unknown");
|
|
25
|
+
const status = firstString(row.status, row.state, row.tempo, "unknown");
|
|
26
|
+
|
|
27
|
+
const normalized = {
|
|
28
|
+
...row,
|
|
29
|
+
short,
|
|
30
|
+
id: firstString(row.id, short),
|
|
31
|
+
sessionId,
|
|
32
|
+
session_id: sessionId,
|
|
33
|
+
state,
|
|
34
|
+
status,
|
|
35
|
+
};
|
|
36
|
+
maybeAssign(normalized, "cwd", row.cwd);
|
|
37
|
+
maybeAssign(normalized, "name", row.name);
|
|
38
|
+
maybeAssign(normalized, "kind", row.kind);
|
|
39
|
+
maybeAssign(normalized, "waitingFor", row.waitingFor ?? row.waiting_for);
|
|
40
|
+
maybeAssign(normalized, "startedAt", row.startedAt ?? row.started_at);
|
|
41
|
+
maybeAssign(normalized, "updatedAt", row.updatedAt ?? row.updated_at);
|
|
42
|
+
maybeAssign(normalized, "pid", row.pid);
|
|
43
|
+
return normalized;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function extractClaudeAgentSessions(listResponse = {}) {
|
|
47
|
+
const rows = Array.isArray(listResponse.jobs)
|
|
48
|
+
? listResponse.jobs
|
|
49
|
+
: Array.isArray(listResponse.sessions)
|
|
50
|
+
? listResponse.sessions
|
|
51
|
+
: [];
|
|
52
|
+
return rows.map((row) => normalizeClaudeAgentSession(row));
|
|
53
|
+
}
|
|
@@ -4,6 +4,10 @@ import fs from "node:fs/promises";
|
|
|
4
4
|
import net from "node:net";
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import path from "node:path";
|
|
7
|
+
import {
|
|
8
|
+
extractClaudeAgentSessions,
|
|
9
|
+
normalizeClaudeAgentSession,
|
|
10
|
+
} from "./claude-agent-session-normalizer.mjs";
|
|
7
11
|
import {
|
|
8
12
|
buildClaudeSessionProjection,
|
|
9
13
|
removeClaudeSessionProjection,
|
|
@@ -1055,27 +1059,27 @@ export function buildClaudePromptDispatchPayload({
|
|
|
1055
1059
|
}
|
|
1056
1060
|
|
|
1057
1061
|
export function findDaemonJobByShort(listResponse, short) {
|
|
1058
|
-
|
|
1059
|
-
|
|
1062
|
+
const expected = String(short || "").trim();
|
|
1063
|
+
if (!expected) return null;
|
|
1064
|
+
return (
|
|
1065
|
+
extractClaudeAgentSessions(listResponse).find(
|
|
1066
|
+
(job) => job.short === expected || job.id === expected,
|
|
1067
|
+
) || null
|
|
1068
|
+
);
|
|
1060
1069
|
}
|
|
1061
1070
|
|
|
1062
1071
|
export function findDaemonJobBySessionId(listResponse, sessionId) {
|
|
1063
|
-
|
|
1064
|
-
const expected = String(sessionId || "");
|
|
1072
|
+
const expected = String(sessionId || "").trim();
|
|
1065
1073
|
if (!expected) return null;
|
|
1066
1074
|
return (
|
|
1067
|
-
listResponse.
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
job?.session_id ??
|
|
1071
|
-
job?.dispatch?.sessionId ??
|
|
1072
|
-
job?.d?.sessionId ??
|
|
1073
|
-
"";
|
|
1074
|
-
return String(candidate) === expected;
|
|
1075
|
-
}) || null
|
|
1075
|
+
extractClaudeAgentSessions(listResponse).find(
|
|
1076
|
+
(job) => String(job.sessionId || job.session_id || "") === expected,
|
|
1077
|
+
) || null
|
|
1076
1078
|
);
|
|
1077
1079
|
}
|
|
1078
1080
|
|
|
1081
|
+
export { extractClaudeAgentSessions, normalizeClaudeAgentSession };
|
|
1082
|
+
|
|
1079
1083
|
export async function waitForDaemonJobPid(
|
|
1080
1084
|
controlSock,
|
|
1081
1085
|
short,
|
|
@@ -69,6 +69,63 @@ export async function updateClaudeSessionProjection(sessionPath, patch) {
|
|
|
69
69
|
return next;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
function projectionSessionId(projection) {
|
|
73
|
+
return String(
|
|
74
|
+
projection?.sessionId ?? projection?.session_id ?? projection?.id ?? "",
|
|
75
|
+
).trim();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function findClaudeSessionProjectionBySessionId(
|
|
79
|
+
sessionsDir,
|
|
80
|
+
sessionId,
|
|
81
|
+
) {
|
|
82
|
+
const expected = String(sessionId || "").trim();
|
|
83
|
+
if (!sessionsDir || !expected) return null;
|
|
84
|
+
let entries;
|
|
85
|
+
try {
|
|
86
|
+
entries = await fs.readdir(sessionsDir, { withFileTypes: true });
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (error?.code === "ENOENT") return null;
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
for (const entry of entries) {
|
|
93
|
+
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
94
|
+
const filePath = path.join(sessionsDir, entry.name);
|
|
95
|
+
try {
|
|
96
|
+
const projection = JSON.parse(await fs.readFile(filePath, "utf8"));
|
|
97
|
+
if (projectionSessionId(projection) === expected) {
|
|
98
|
+
return { path: filePath, projection };
|
|
99
|
+
}
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if (error instanceof SyntaxError) continue;
|
|
102
|
+
if (error?.code === "ENOENT") continue;
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function refreshClaudeSessionProjectionCwd({
|
|
110
|
+
sessionsDir,
|
|
111
|
+
sessionId,
|
|
112
|
+
cwd,
|
|
113
|
+
updatedAt = Date.now(),
|
|
114
|
+
} = {}) {
|
|
115
|
+
const nextCwd = String(cwd || "").trim();
|
|
116
|
+
if (!nextCwd) return { updated: false, reason: "missing_cwd" };
|
|
117
|
+
const found = await findClaudeSessionProjectionBySessionId(
|
|
118
|
+
sessionsDir,
|
|
119
|
+
sessionId,
|
|
120
|
+
);
|
|
121
|
+
if (!found) return { updated: false, reason: "projection_not_found" };
|
|
122
|
+
const projection = await updateClaudeSessionProjection(found.path, {
|
|
123
|
+
cwd: nextCwd,
|
|
124
|
+
updatedAt,
|
|
125
|
+
});
|
|
126
|
+
return { updated: true, path: found.path, projection };
|
|
127
|
+
}
|
|
128
|
+
|
|
72
129
|
export async function removeClaudeSessionProjection(sessionPath) {
|
|
73
130
|
await fs.rm(sessionPath, { force: true });
|
|
74
131
|
}
|
package/package.json
CHANGED
|
@@ -4,6 +4,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { resolveHubPortForContext } from "../../hub/hub-lifecycle.mjs";
|
|
7
8
|
import { whichCommand, whichCommandAsync } from "../../hub/platform.mjs";
|
|
8
9
|
|
|
9
10
|
const HUB_DEFAULT_PORT = 27888;
|
|
@@ -37,13 +38,36 @@ function fetchHubStatus({
|
|
|
37
38
|
};
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
export function resolveDefaultStatusUrl(env = process.env) {
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
export function resolveDefaultStatusUrl(env = process.env, cwd = process.cwd()) {
|
|
42
|
+
const port = resolveHubPortForContext({
|
|
43
|
+
env,
|
|
44
|
+
cwd,
|
|
45
|
+
defaultPort: HUB_DEFAULT_PORT,
|
|
46
|
+
});
|
|
44
47
|
return `http://127.0.0.1:${port}/status`;
|
|
45
48
|
}
|
|
46
49
|
|
|
50
|
+
function resolveStatusUrlForContext({
|
|
51
|
+
statusUrl,
|
|
52
|
+
env = process.env,
|
|
53
|
+
cwd = process.cwd(),
|
|
54
|
+
} = {}) {
|
|
55
|
+
try {
|
|
56
|
+
const url = new URL(String(statusUrl));
|
|
57
|
+
url.port = String(
|
|
58
|
+
resolveHubPortForContext({
|
|
59
|
+
port: url.port,
|
|
60
|
+
env,
|
|
61
|
+
cwd,
|
|
62
|
+
defaultPort: HUB_DEFAULT_PORT,
|
|
63
|
+
}),
|
|
64
|
+
);
|
|
65
|
+
return url.toString();
|
|
66
|
+
} catch {
|
|
67
|
+
return resolveDefaultStatusUrl(env, cwd);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
47
71
|
function normalizeCliName(name) {
|
|
48
72
|
return String(name ?? "").trim() || null;
|
|
49
73
|
}
|
|
@@ -212,8 +236,10 @@ export function detectCodexPlan(options = {}) {
|
|
|
212
236
|
}
|
|
213
237
|
|
|
214
238
|
export function checkHub({
|
|
239
|
+
env = process.env,
|
|
240
|
+
cwd = process.cwd(),
|
|
215
241
|
pkgRoot = DEFAULT_PKG_ROOT,
|
|
216
|
-
statusUrl = resolveDefaultStatusUrl(),
|
|
242
|
+
statusUrl = resolveDefaultStatusUrl(env, cwd),
|
|
217
243
|
restart = true,
|
|
218
244
|
requestTimeoutMs = 3000,
|
|
219
245
|
pollAttempts = 8,
|
|
@@ -223,10 +249,11 @@ export function checkHub({
|
|
|
223
249
|
existsSyncFn = existsSync,
|
|
224
250
|
sleepSyncFn = sleepSync,
|
|
225
251
|
} = {}) {
|
|
252
|
+
const guardedStatusUrl = resolveStatusUrlForContext({ statusUrl, env, cwd });
|
|
226
253
|
try {
|
|
227
254
|
return fetchHubStatus({
|
|
228
255
|
execSyncFn,
|
|
229
|
-
statusUrl,
|
|
256
|
+
statusUrl: guardedStatusUrl,
|
|
230
257
|
timeout: requestTimeoutMs,
|
|
231
258
|
});
|
|
232
259
|
} catch {}
|
|
@@ -239,7 +266,7 @@ export function checkHub({
|
|
|
239
266
|
|
|
240
267
|
try {
|
|
241
268
|
const child = spawnFn(process.execPath, [serverPath], {
|
|
242
|
-
env: { ...
|
|
269
|
+
env: { ...env, TFX_HUB_PORT: String(new URL(guardedStatusUrl).port) },
|
|
243
270
|
detached: true,
|
|
244
271
|
stdio: "ignore",
|
|
245
272
|
windowsHide: true,
|
|
@@ -254,7 +281,7 @@ export function checkHub({
|
|
|
254
281
|
try {
|
|
255
282
|
const status = fetchHubStatus({
|
|
256
283
|
execSyncFn,
|
|
257
|
-
statusUrl,
|
|
284
|
+
statusUrl: guardedStatusUrl,
|
|
258
285
|
timeout: Math.min(requestTimeoutMs, 1000),
|
|
259
286
|
});
|
|
260
287
|
if (status.state === "healthy") {
|