@triflux/core 10.52.0 → 10.53.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/hub/bridge.mjs +4 -25
- package/hub/cli-adapter-base.mjs +4 -104
- package/hub/codex-adapter.mjs +7 -116
- package/hub/index.mjs +1 -2
- package/hub/team/claude-transcript.mjs +19 -0
- package/hud/constants.mjs +7 -22
- package/hud/hud-qos-status.mjs +9 -28
- package/hud/providers/codex-probe.mjs +2 -31
- package/hud/providers/codex.mjs +16 -58
- package/hud/renderers.mjs +22 -31
- package/hud/utils.mjs +0 -8
- package/package.json +3 -4
- package/scripts/lib/mcp-guard-engine.mjs +2 -3
- package/hub/account-broker.mjs +0 -1221
package/hub/bridge.mjs
CHANGED
|
@@ -465,9 +465,8 @@ async function cmdDaemonWait(args) {
|
|
|
465
465
|
if (!payload.short && !payload.sessionId)
|
|
466
466
|
throw new Error("short or sessionId is required");
|
|
467
467
|
const { probeClaudeDaemonCandidates } = await loadDaemonControl();
|
|
468
|
-
const { findClaudeTranscript, readClaudeTranscript } =
|
|
469
|
-
"./team/claude-transcript.mjs"
|
|
470
|
-
);
|
|
468
|
+
const { claudeWaitVerdict, findClaudeTranscript, readClaudeTranscript } =
|
|
469
|
+
await import("./team/claude-transcript.mjs");
|
|
471
470
|
const timeoutMs = numericOption(payload.timeoutMs, 30_000);
|
|
472
471
|
const pollIntervalMs = Math.max(
|
|
473
472
|
10,
|
|
@@ -535,28 +534,8 @@ async function cmdDaemonWait(args) {
|
|
|
535
534
|
target: probe.target,
|
|
536
535
|
...daemonProbeMetadata(probe),
|
|
537
536
|
};
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
...last,
|
|
541
|
-
ok: false,
|
|
542
|
-
status: "failed",
|
|
543
|
-
error: transcript.error,
|
|
544
|
-
});
|
|
545
|
-
}
|
|
546
|
-
if (
|
|
547
|
-
(idle || transcript?.sectionClosed) &&
|
|
548
|
-
transcript?.userSeen &&
|
|
549
|
-
transcript.turnEnded
|
|
550
|
-
) {
|
|
551
|
-
if (!transcript.response.trim())
|
|
552
|
-
return emitJson({
|
|
553
|
-
...last,
|
|
554
|
-
ok: false,
|
|
555
|
-
status: "failed",
|
|
556
|
-
error: "turn ended without assistant text",
|
|
557
|
-
});
|
|
558
|
-
return emitJson({ ...last, status: "completed", done: true });
|
|
559
|
-
}
|
|
537
|
+
const verdict = claudeWaitVerdict(transcript, idle);
|
|
538
|
+
if (verdict) return emitJson({ ...last, ...verdict });
|
|
560
539
|
if (Date.now() >= deadline) break;
|
|
561
540
|
await new Promise((resolve) =>
|
|
562
541
|
setTimeout(resolve, Math.min(pollIntervalMs, deadline - Date.now())),
|
package/hub/cli-adapter-base.mjs
CHANGED
|
@@ -15,61 +15,6 @@ import {
|
|
|
15
15
|
import { IS_WINDOWS, killProcess } from "./platform.mjs";
|
|
16
16
|
import { createInterventionLadder } from "./team/intervention.mjs";
|
|
17
17
|
|
|
18
|
-
// ── Quota retry-after 파싱 ──────────────────────────────────────
|
|
19
|
-
|
|
20
|
-
const FALLBACK_COOLDOWN_MS = { codex: 5 * 3600_000, gemini: 86400_000 };
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* 에러 텍스트에서 retry-after 시간을 동적 파싱.
|
|
24
|
-
* "try again at 2026-04-11T10:00:00Z", "retry after 3600 seconds",
|
|
25
|
-
* "resets in 7 days", "wait 5 hours" 등을 감지.
|
|
26
|
-
*/
|
|
27
|
-
export function parseRetryAfterMs(text, provider) {
|
|
28
|
-
// 1. ISO timestamp: "try again at 2026-04-11T10:00:00"
|
|
29
|
-
const isoMatch = text.match(
|
|
30
|
-
/(?:try again|retry|available|resets?)\s+(?:at|after)\s+(\d{4}-\d{2}-\d{2}T[\d:.]+Z?)/i,
|
|
31
|
-
);
|
|
32
|
-
if (isoMatch) {
|
|
33
|
-
const target = new Date(isoMatch[1]).getTime();
|
|
34
|
-
if (target > Date.now()) return target - Date.now();
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// 2. Duration: "N seconds/minutes/hours/days"
|
|
38
|
-
const durMatch = text.match(
|
|
39
|
-
/(?:retry|wait|resets?|again)\s+(?:in\s+|after\s+)?(\d+)\s*(second|minute|hour|day|week)/i,
|
|
40
|
-
);
|
|
41
|
-
if (durMatch) {
|
|
42
|
-
const n = Number(durMatch[1]);
|
|
43
|
-
const unit = durMatch[2].toLowerCase();
|
|
44
|
-
const multipliers = {
|
|
45
|
-
second: 1000,
|
|
46
|
-
minute: 60_000,
|
|
47
|
-
hour: 3600_000,
|
|
48
|
-
day: 86400_000,
|
|
49
|
-
week: 604800_000,
|
|
50
|
-
};
|
|
51
|
-
return n * (multipliers[unit] || 3600_000);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// 3. "7 days" / "24 hours" 단독 (context 없이)
|
|
55
|
-
const standaloneMatch = text.match(/(\d+)\s*(day|hour|week|minute)/i);
|
|
56
|
-
if (standaloneMatch) {
|
|
57
|
-
const n = Number(standaloneMatch[1]);
|
|
58
|
-
const unit = standaloneMatch[2].toLowerCase();
|
|
59
|
-
const multipliers = {
|
|
60
|
-
minute: 60_000,
|
|
61
|
-
hour: 3600_000,
|
|
62
|
-
day: 86400_000,
|
|
63
|
-
week: 604800_000,
|
|
64
|
-
};
|
|
65
|
-
const parsed = n * (multipliers[unit] || 3600_000);
|
|
66
|
-
if (parsed >= 3600_000) return parsed; // 1시간 이상만 신뢰
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// 4. fallback: provider 기본값
|
|
70
|
-
return FALLBACK_COOLDOWN_MS[provider] || 5 * 3600_000;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
18
|
// ── Shell utilities ─────────────────────────────────────────────
|
|
74
19
|
|
|
75
20
|
export function normalizePathForShell(value) {
|
|
@@ -195,47 +140,28 @@ export function appendWarnings(stderr, warnings = []) {
|
|
|
195
140
|
return [stderr, text].filter(Boolean).join("\n");
|
|
196
141
|
}
|
|
197
142
|
|
|
198
|
-
// ──
|
|
143
|
+
// ── Attempt execution ───────────────────────────────────────────
|
|
199
144
|
|
|
200
145
|
/**
|
|
201
|
-
*
|
|
202
|
-
* breaking instead of a global breaker.
|
|
146
|
+
* preflight 뒤 buildAttemptsFn 이 만든 시도를 순서대로 실행한다.
|
|
203
147
|
*
|
|
204
148
|
* @param {object} params
|
|
205
|
-
* @param {string} params.provider — 'codex' | 'gemini'
|
|
206
149
|
* @param {(prompt: string, workdir: string, preflight: object, attempt: object) => Promise<object>} params.runFn
|
|
207
150
|
* @param {(opts: object) => Promise<object>} params.preflightFn
|
|
208
151
|
* @param {(opts: object, preflight: object) => object[]} params.buildAttemptsFn
|
|
209
152
|
* @param {object} params.opts — caller-supplied execute options
|
|
210
153
|
* @returns {Promise<object>} createResult-shaped result
|
|
211
154
|
*/
|
|
212
|
-
export async function
|
|
213
|
-
provider,
|
|
155
|
+
export async function executeWithAttempts({
|
|
214
156
|
runFn,
|
|
215
157
|
preflightFn,
|
|
216
158
|
buildAttemptsFn,
|
|
217
159
|
opts = {},
|
|
218
160
|
}) {
|
|
219
|
-
// late-import to avoid circular dependency at module load time
|
|
220
|
-
const brokerMod = await import("./account-broker.mjs");
|
|
221
161
|
const { withRetry } = await import("./workers/worker-utils.mjs");
|
|
222
162
|
|
|
223
|
-
// access broker as live binding property (not destructured) so reloadBroker() propagates
|
|
224
|
-
const hasBroker = brokerMod.broker != null;
|
|
225
|
-
// Empty broker (TFX_DISABLE_ACCOUNT_BROKER=1 or zero accounts) must not
|
|
226
|
-
// gate execution. lease() is null because there are no accounts to lease,
|
|
227
|
-
// not because all accounts are busy/cooldown/circuit. Treat it like
|
|
228
|
-
// hasBroker=false and fall through to the default auth path.
|
|
229
|
-
const brokerDisabled = hasBroker && brokerMod.broker.isDisabled === true;
|
|
230
|
-
const effectiveBroker = hasBroker && !brokerDisabled;
|
|
231
|
-
const lease = effectiveBroker ? brokerMod.broker.lease({ provider }) : null;
|
|
232
|
-
if (effectiveBroker && !lease) {
|
|
233
|
-
return createResult(false, { fellBack: true, failureMode: "circuit_open" });
|
|
234
|
-
}
|
|
235
|
-
|
|
236
163
|
const preflight = await preflightFn(opts);
|
|
237
164
|
if (!preflight.ok) {
|
|
238
|
-
if (lease) brokerMod.broker.release(lease.id, { ok: false });
|
|
239
165
|
return createResult(false, {
|
|
240
166
|
stderr: appendWarnings("", preflight.warnings),
|
|
241
167
|
fellBack: opts.fallbackToClaude !== false,
|
|
@@ -250,15 +176,11 @@ export async function executeWithCircuitBroker({
|
|
|
250
176
|
try {
|
|
251
177
|
lastResult = await withRetry(
|
|
252
178
|
async () => {
|
|
253
|
-
// PRD A1: lease 메타데이터를 runFn 에 전달해서 adapter 가 실제 spawn 시
|
|
254
|
-
// 해당 account 의 authFile/env/profile 을 적용할 수 있게 한다. lease=null
|
|
255
|
-
// 이면 adapter 는 default 경로 (단일 ~/.codex/auth.json) 로 동작한다.
|
|
256
179
|
const result = await runFn(
|
|
257
180
|
opts.prompt || "",
|
|
258
181
|
opts.workdir || process.cwd(),
|
|
259
182
|
preflight,
|
|
260
183
|
attempts[attemptIndex],
|
|
261
|
-
lease,
|
|
262
184
|
);
|
|
263
185
|
const current = {
|
|
264
186
|
...result,
|
|
@@ -286,26 +208,7 @@ export async function executeWithCircuitBroker({
|
|
|
286
208
|
createResult(false, { stderr: String(error?.message || error) });
|
|
287
209
|
}
|
|
288
210
|
|
|
289
|
-
if (lastResult.ok)
|
|
290
|
-
if (lease) brokerMod.broker.release(lease.id, { ok: true });
|
|
291
|
-
return lastResult;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
if (lease) {
|
|
295
|
-
if (lastResult.failureMode === "rate_limited") {
|
|
296
|
-
const text = `${lastResult.output || ""}\n${lastResult.stderr || ""}`;
|
|
297
|
-
const coolMs = parseRetryAfterMs(text, provider);
|
|
298
|
-
brokerMod.broker.markRateLimited(lease.id, coolMs);
|
|
299
|
-
brokerMod.broker.emit("cooldown", {
|
|
300
|
-
id: lease.id,
|
|
301
|
-
provider,
|
|
302
|
-
coolMs,
|
|
303
|
-
reason: "quota_exhausted",
|
|
304
|
-
});
|
|
305
|
-
} else {
|
|
306
|
-
brokerMod.broker.release(lease.id, { ok: false });
|
|
307
|
-
}
|
|
308
|
-
}
|
|
211
|
+
if (lastResult.ok) return lastResult;
|
|
309
212
|
return {
|
|
310
213
|
...lastResult,
|
|
311
214
|
retried: attempts.length > 1,
|
|
@@ -374,13 +277,10 @@ export async function runProcess(command, workdir, timeout, opts = {}) {
|
|
|
374
277
|
let child;
|
|
375
278
|
|
|
376
279
|
try {
|
|
377
|
-
// PRD A1: opts.spawnEnv 가 있으면 그 env 로 spawn (lease 의 authFile/env 적용).
|
|
378
|
-
// undefined 면 spawn 의 default 동작 (부모 process env inherit) 유지.
|
|
379
280
|
child = spawnProcess(command, {
|
|
380
281
|
cwd: workdir,
|
|
381
282
|
shell: true,
|
|
382
283
|
windowsHide: true,
|
|
383
|
-
env: opts.spawnEnv,
|
|
384
284
|
});
|
|
385
285
|
} catch (error) {
|
|
386
286
|
return createResult(false, {
|
package/hub/codex-adapter.mjs
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
|
-
import {
|
|
3
|
+
import { join } from "node:path";
|
|
4
4
|
import { codexProfileConfigOverrides } from "../scripts/lib/codex-profile-config.mjs";
|
|
5
5
|
import {
|
|
6
6
|
buildExecCommand,
|
|
7
|
-
|
|
8
|
-
executeWithCircuitBroker,
|
|
7
|
+
executeWithAttempts,
|
|
9
8
|
normalizePathForShell,
|
|
10
9
|
runProcess,
|
|
11
10
|
shellQuote,
|
|
@@ -113,7 +112,7 @@ export function buildLaunchScript(opts = {}) {
|
|
|
113
112
|
|
|
114
113
|
// CTO boundary: this adapter assembles Codex prompts outside
|
|
115
114
|
// scripts/tfx-route.sh. Keep north-star injection in tfx-route.sh until
|
|
116
|
-
// launcher
|
|
115
|
+
// launcher workdir contracts have focused coverage.
|
|
117
116
|
export function buildExecArgs(opts = {}) {
|
|
118
117
|
const prompt = typeof opts.prompt === "string" ? opts.prompt : "";
|
|
119
118
|
const command = buildExecCommand(prompt, opts.resultFile || null, {
|
|
@@ -153,7 +152,7 @@ export function buildExecArgs(opts = {}) {
|
|
|
153
152
|
|
|
154
153
|
// ── Codex execution ─────────────────────────────────────────────
|
|
155
154
|
|
|
156
|
-
async function runCodex(prompt, workdir, preflight, attempt
|
|
155
|
+
async function runCodex(prompt, workdir, preflight, attempt) {
|
|
157
156
|
const dir = join(tmpdir(), "triflux-codex-exec");
|
|
158
157
|
mkdirSync(dir, { recursive: true });
|
|
159
158
|
const resultFile = join(
|
|
@@ -162,7 +161,7 @@ async function runCodex(prompt, workdir, preflight, attempt, lease) {
|
|
|
162
161
|
);
|
|
163
162
|
const command = commandWithOverrides(
|
|
164
163
|
buildExecCommand(prompt, resultFile, {
|
|
165
|
-
profile:
|
|
164
|
+
profile: attempt.profile,
|
|
166
165
|
skipGitRepoCheck: true,
|
|
167
166
|
sandboxBypass: attempt.forceBypass,
|
|
168
167
|
}),
|
|
@@ -170,125 +169,17 @@ async function runCodex(prompt, workdir, preflight, attempt, lease) {
|
|
|
170
169
|
preflight.codexPath,
|
|
171
170
|
buildOverrides(attempt.requested, attempt.excluded),
|
|
172
171
|
);
|
|
173
|
-
// PRD A1 — lease 메타데이터를 spawn env 에 적용한다. lease.authFile 이 있으면
|
|
174
|
-
// 해당 파일이 위치한 디렉토리를 CODEX_HOME 으로 export 해서 codex CLI 가 그
|
|
175
|
-
// account 의 auth.json 을 사용하게 한다. lease 가 null 이면 default ~/.codex
|
|
176
|
-
// 동작 유지 (회귀 없음). lease.env 는 추가 환경변수 (provider 고정 등) 주입용.
|
|
177
|
-
const spawnEnv = lease ? buildLeaseSpawnEnv(lease) : undefined;
|
|
178
|
-
const authCheck = validateLeaseAuth(lease, spawnEnv);
|
|
179
|
-
if (!authCheck.ok) {
|
|
180
|
-
return createResult(false, {
|
|
181
|
-
stderr: authCheck.error,
|
|
182
|
-
failureMode: "auth_sync",
|
|
183
|
-
skipCircuit: true,
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
172
|
return runProcess(command, workdir, attempt.timeout, {
|
|
187
173
|
resultFile,
|
|
188
174
|
inferStallMode,
|
|
189
|
-
spawnEnv,
|
|
190
175
|
cli: "codex",
|
|
191
|
-
codexHome: spawnEnv?.CODEX_HOME,
|
|
192
176
|
});
|
|
193
177
|
}
|
|
194
178
|
|
|
195
|
-
function buildLeaseSpawnEnv(lease) {
|
|
196
|
-
const extra = {};
|
|
197
|
-
if (lease.authFile) {
|
|
198
|
-
// lease.authFile 은 보통 ~/.claude/cache/tfx-hub/codex-auth-<account>.json 형식.
|
|
199
|
-
// codex CLI 는 CODEX_HOME 을 통해 auth.json 위치를 결정하므로, 이 cache 파일을
|
|
200
|
-
// 직접 CODEX_HOME 후보 디렉토리로 사용한다. cache 파일 자체가 auth.json 이름이
|
|
201
|
-
// 아니라면 후속 PR 에서 isolated dir + symlink/복사 처리한다 (PRD Open Question).
|
|
202
|
-
// 현재는 lease.authFile 의 dirname 을 export 하되, 그 dirname 안에 auth.json 이
|
|
203
|
-
// 실제로 있을 때만 적용한다 (false-positive 회피).
|
|
204
|
-
try {
|
|
205
|
-
const dir = dirnameOf(lease.authFile);
|
|
206
|
-
if (dir) extra.CODEX_HOME = dir;
|
|
207
|
-
} catch {}
|
|
208
|
-
}
|
|
209
|
-
if (lease.env && typeof lease.env === "object") {
|
|
210
|
-
Object.assign(extra, lease.env);
|
|
211
|
-
}
|
|
212
|
-
return Object.keys(extra).length ? { ...process.env, ...extra } : undefined;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function validateLeaseAuth(lease, spawnEnv) {
|
|
216
|
-
if (!lease?.authFile) return { ok: true };
|
|
217
|
-
if (basename(lease.authFile) !== "auth.json") {
|
|
218
|
-
return {
|
|
219
|
-
ok: false,
|
|
220
|
-
error: `lease authFile must resolve to auth.json for account ${lease.id}`,
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
if (!existsSync(lease.authFile)) {
|
|
224
|
-
return {
|
|
225
|
-
ok: false,
|
|
226
|
-
error: `lease authFile missing for account ${lease.id}: ${lease.authFile}`,
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
if (dirnameOf(lease.authFile) !== spawnEnv?.CODEX_HOME) {
|
|
230
|
-
return {
|
|
231
|
-
ok: false,
|
|
232
|
-
// This is currently expected to match because buildLeaseSpawnEnv derives
|
|
233
|
-
// CODEX_HOME from authFile. Keep the guard so future lease.env overrides
|
|
234
|
-
// cannot silently point Codex at a different account home.
|
|
235
|
-
error: `CODEX_HOME mismatch for account ${lease.id}`,
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
try {
|
|
239
|
-
const parsed = JSON.parse(readFileSync(lease.authFile, "utf8"));
|
|
240
|
-
const accountId =
|
|
241
|
-
parsed?.tokens?.account_id ??
|
|
242
|
-
parsed?.account_id ??
|
|
243
|
-
parsed?.accountId ??
|
|
244
|
-
null;
|
|
245
|
-
if (!accountId) {
|
|
246
|
-
return {
|
|
247
|
-
ok: false,
|
|
248
|
-
error: `lease authFile has no account_id for account ${lease.id}`,
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
if (accountId !== lease.id) {
|
|
252
|
-
return {
|
|
253
|
-
ok: false,
|
|
254
|
-
error: `lease authFile account mismatch: expected ${lease.id}, got ${accountId}`,
|
|
255
|
-
};
|
|
256
|
-
}
|
|
257
|
-
} catch (error) {
|
|
258
|
-
return {
|
|
259
|
-
ok: false,
|
|
260
|
-
error: `lease authFile unreadable for account ${lease.id}: ${error?.message || error}`,
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
return { ok: true };
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function dirnameOf(filePath) {
|
|
267
|
-
if (typeof filePath !== "string" || !filePath) return null;
|
|
268
|
-
const lastSep = Math.max(
|
|
269
|
-
filePath.lastIndexOf("/"),
|
|
270
|
-
filePath.lastIndexOf("\\"),
|
|
271
|
-
);
|
|
272
|
-
if (lastSep < 0) return null;
|
|
273
|
-
return filePath.slice(0, lastSep);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
179
|
// ── Public API ──────────────────────────────────────────────────
|
|
277
180
|
|
|
278
|
-
export async function getCircuitState() {
|
|
279
|
-
const brokerMod = await import("./account-broker.mjs");
|
|
280
|
-
if (!brokerMod.broker) return { state: "closed", failures: [] };
|
|
281
|
-
const snap = brokerMod.broker
|
|
282
|
-
.snapshot()
|
|
283
|
-
.filter((a) => a.provider === "codex");
|
|
284
|
-
return snap.length
|
|
285
|
-
? { state: snap[0].circuitState, accounts: snap }
|
|
286
|
-
: { state: "closed", failures: [] };
|
|
287
|
-
}
|
|
288
|
-
|
|
289
181
|
export function execute(opts = {}) {
|
|
290
|
-
return
|
|
291
|
-
provider: "codex",
|
|
182
|
+
return executeWithAttempts({
|
|
292
183
|
runFn: runCodex,
|
|
293
184
|
preflightFn: (o) =>
|
|
294
185
|
runPreflight({
|
package/hub/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @triflux/core — barrel export (auto-generated by pack.mjs)
|
|
2
2
|
// Adapters
|
|
3
3
|
export { buildExecCommand, sleep, CODEX_MCP_TRANSPORT_EXIT_CODE, normalizePathForShell, shellQuote } from './cli-adapter-base.mjs';
|
|
4
|
-
export { execute as executeCodex, buildExecArgs as buildCodexArgs, buildLaunchScript
|
|
4
|
+
export { execute as executeCodex, buildExecArgs as buildCodexArgs, buildLaunchScript } from './codex-adapter.mjs';
|
|
5
5
|
export { getCodexVersion, runPreflight } from './codex-preflight.mjs';
|
|
6
6
|
|
|
7
7
|
// Platform
|
|
@@ -9,4 +9,3 @@ export { IS_WINDOWS, IS_MAC, IS_LINUX, TEMP_DIR, normalizePath, whichCommand, ki
|
|
|
9
9
|
|
|
10
10
|
// Core services
|
|
11
11
|
export { parseArgs, parseJsonSafe } from './bridge.mjs';
|
|
12
|
-
export { broker as accountBroker } from './account-broker.mjs';
|
|
@@ -157,6 +157,25 @@ function acceptLine(state, line, marker) {
|
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
// daemon 과 tmux wait 가 같은 기준으로 끝을 판정한다. 아직이면 null.
|
|
161
|
+
export function claudeWaitVerdict(transcript, idle) {
|
|
162
|
+
if (transcript?.error)
|
|
163
|
+
return { ok: false, status: "failed", error: transcript.error };
|
|
164
|
+
if (
|
|
165
|
+
!transcript?.userSeen ||
|
|
166
|
+
!transcript.turnEnded ||
|
|
167
|
+
!(idle || transcript.sectionClosed)
|
|
168
|
+
)
|
|
169
|
+
return null;
|
|
170
|
+
if (!transcript.response.trim())
|
|
171
|
+
return {
|
|
172
|
+
ok: false,
|
|
173
|
+
status: "failed",
|
|
174
|
+
error: "turn ended without assistant text",
|
|
175
|
+
};
|
|
176
|
+
return { status: "completed", done: true };
|
|
177
|
+
}
|
|
178
|
+
|
|
160
179
|
export async function readClaudeTranscript(transcriptPath, { requestId } = {}) {
|
|
161
180
|
if (!transcriptPath) return null;
|
|
162
181
|
const marker = requestId ? `[tfx-live req=${requestId}]` : null;
|
package/hud/constants.mjs
CHANGED
|
@@ -6,21 +6,6 @@ import { join } from "node:path";
|
|
|
6
6
|
|
|
7
7
|
export const VERSION = "2.0";
|
|
8
8
|
|
|
9
|
-
export const ACCOUNTS_CONFIG_PATH = join(
|
|
10
|
-
homedir(),
|
|
11
|
-
".claude",
|
|
12
|
-
"cache",
|
|
13
|
-
"tfx-hub",
|
|
14
|
-
"accounts.json",
|
|
15
|
-
);
|
|
16
|
-
export const ACCOUNTS_STATE_PATH = join(
|
|
17
|
-
homedir(),
|
|
18
|
-
".claude",
|
|
19
|
-
"cache",
|
|
20
|
-
"tfx-hub",
|
|
21
|
-
"cli_accounts_state.json",
|
|
22
|
-
);
|
|
23
|
-
|
|
24
9
|
// triflux-mods 의 Claude band 가 세션마다 갱신하는 표식. 있으면 Claude 행을 뺀다.
|
|
25
10
|
export const CLAUDE_BAND_MARKER_DIR = join(
|
|
26
11
|
homedir(),
|
|
@@ -107,19 +92,19 @@ export const SEVEN_DAY_MS = 7 * 24 * 60 * 60 * 1000;
|
|
|
107
92
|
export const ONE_DAY_MS = 24 * 60 * 60 * 1000;
|
|
108
93
|
export const DEFAULT_OAUTH_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
|
|
109
94
|
|
|
110
|
-
|
|
95
|
+
// Codex CLI 는 CODEX_HOME 이 있으면 그 디렉터리를 쓴다. 호출 시점에 읽어 환경 변화를 따른다.
|
|
96
|
+
export function getCodexHome() {
|
|
97
|
+
return process.env.CODEX_HOME || join(homedir(), ".codex");
|
|
98
|
+
}
|
|
99
|
+
export function getCodexAuthPath() {
|
|
100
|
+
return join(getCodexHome(), "auth.json");
|
|
101
|
+
}
|
|
111
102
|
export const CODEX_PROBE_STATE_PATH = join(
|
|
112
103
|
homedir(),
|
|
113
104
|
".claude",
|
|
114
105
|
"cache",
|
|
115
106
|
"codex-probe-state.json",
|
|
116
107
|
);
|
|
117
|
-
export const CODEX_BROKER_AUTH_CACHE_DIR = join(
|
|
118
|
-
homedir(),
|
|
119
|
-
".claude",
|
|
120
|
-
"cache",
|
|
121
|
-
"tfx-hub",
|
|
122
|
-
);
|
|
123
108
|
export const CODEX_PROBE_TTL_MS = 5 * 60 * 1000;
|
|
124
109
|
export const CODEX_PROBE_TIMEOUT_MS = 10_000;
|
|
125
110
|
export const CODEX_QUOTA_CACHE_PATH = join(
|
package/hud/hud-qos-status.mjs
CHANGED
|
@@ -14,14 +14,13 @@ import {
|
|
|
14
14
|
RESET,
|
|
15
15
|
} from "./colors.mjs";
|
|
16
16
|
import {
|
|
17
|
-
ACCOUNTS_CONFIG_PATH,
|
|
18
|
-
ACCOUNTS_STATE_PATH,
|
|
19
17
|
ANTIGRAVITY_REFRESH_FLAG,
|
|
20
18
|
CLAUDE_BAND_MARKER_DIR,
|
|
21
19
|
CLAUDE_BAND_MARKER_TTL_MS,
|
|
22
20
|
CLAUDE_REFRESH_FLAG,
|
|
23
|
-
CODEX_AUTH_PATH,
|
|
24
21
|
CODEX_REFRESH_FLAG,
|
|
22
|
+
getCodexAuthPath,
|
|
23
|
+
getCodexHome,
|
|
25
24
|
} from "./constants.mjs";
|
|
26
25
|
import { buildContextUsageView } from "./context-monitor.mjs";
|
|
27
26
|
import {
|
|
@@ -31,7 +30,6 @@ import {
|
|
|
31
30
|
} from "./providers/claude.mjs";
|
|
32
31
|
import {
|
|
33
32
|
getCodexEmail,
|
|
34
|
-
hasBrokerCodexAccounts,
|
|
35
33
|
readCodexRateLimitSnapshot,
|
|
36
34
|
refreshCodexRateLimitsCache,
|
|
37
35
|
scheduleCodexRateLimitRefresh,
|
|
@@ -50,12 +48,7 @@ import {
|
|
|
50
48
|
renderAlignedRows,
|
|
51
49
|
} from "./renderers.mjs";
|
|
52
50
|
import { selectTier } from "./terminal.mjs";
|
|
53
|
-
import {
|
|
54
|
-
formatTimeCell,
|
|
55
|
-
formatTimeCellDH,
|
|
56
|
-
readJson,
|
|
57
|
-
readStdinJson,
|
|
58
|
-
} from "./utils.mjs";
|
|
51
|
+
import { formatTimeCell, formatTimeCellDH, readStdinJson } from "./utils.mjs";
|
|
59
52
|
|
|
60
53
|
async function main() {
|
|
61
54
|
if (process.argv.includes(CLAUDE_REFRESH_FLAG)) {
|
|
@@ -73,8 +66,6 @@ async function main() {
|
|
|
73
66
|
|
|
74
67
|
const stdinPromise = readStdinJson();
|
|
75
68
|
const { showCodex, antigravityAllowed } = resolveHudCliVisibility();
|
|
76
|
-
const accountsConfig = readJson(ACCOUNTS_CONFIG_PATH, { providers: {} });
|
|
77
|
-
const accountsState = readJson(ACCOUNTS_STATE_PATH, { providers: {} });
|
|
78
69
|
const claudeUsageSnapshot = readClaudeUsageSnapshot();
|
|
79
70
|
const codexSnapshot = readCodexRateLimitSnapshot();
|
|
80
71
|
const antigravitySnapshot = antigravityAllowed
|
|
@@ -88,11 +79,7 @@ async function main() {
|
|
|
88
79
|
) {
|
|
89
80
|
scheduleClaudeUsageRefresh();
|
|
90
81
|
}
|
|
91
|
-
if (
|
|
92
|
-
showCodex &&
|
|
93
|
-
codexSnapshot.shouldRefresh &&
|
|
94
|
-
(existsSync(join(homedir(), ".codex")) || hasBrokerCodexAccounts())
|
|
95
|
-
) {
|
|
82
|
+
if (showCodex && codexSnapshot.shouldRefresh && existsSync(getCodexHome())) {
|
|
96
83
|
scheduleCodexRateLimitRefresh();
|
|
97
84
|
}
|
|
98
85
|
|
|
@@ -106,9 +93,13 @@ async function main() {
|
|
|
106
93
|
? { ...antigravitySnapshot?.data, auth: getAntigravityAuthKind() }
|
|
107
94
|
: null;
|
|
108
95
|
const currentTier = selectTier();
|
|
96
|
+
// 회색은 로그인 안 된 경우에만 쓴다. 로그인 상태의 조회 공백은 --% 로만 보인다.
|
|
97
|
+
const codexLoggedOut =
|
|
98
|
+
showCodex && !codexBuckets && !existsSync(getCodexAuthPath());
|
|
109
99
|
if (currentTier === "nano") {
|
|
110
100
|
const microLine = getMicroLine(contextView, claudeUsage, codexBuckets, {
|
|
111
101
|
showCodex,
|
|
102
|
+
codexLoggedOut,
|
|
112
103
|
showAntigravity: antigravityAllowed,
|
|
113
104
|
antigravityQuota,
|
|
114
105
|
});
|
|
@@ -128,8 +119,6 @@ async function main() {
|
|
|
128
119
|
"codex",
|
|
129
120
|
"x",
|
|
130
121
|
codexWhite,
|
|
131
|
-
accountsConfig,
|
|
132
|
-
accountsState,
|
|
133
122
|
codexBuckets ? { type: "codex", buckets: codexBuckets } : null,
|
|
134
123
|
getCodexEmail(),
|
|
135
124
|
),
|
|
@@ -142,8 +131,6 @@ async function main() {
|
|
|
142
131
|
"antigravity",
|
|
143
132
|
"a",
|
|
144
133
|
geminiBlue,
|
|
145
|
-
accountsConfig,
|
|
146
|
-
accountsState,
|
|
147
134
|
antigravityQuota,
|
|
148
135
|
getAntigravityAccountLabel(),
|
|
149
136
|
),
|
|
@@ -151,13 +138,7 @@ async function main() {
|
|
|
151
138
|
}
|
|
152
139
|
|
|
153
140
|
const outputLines = renderAlignedRows(rows);
|
|
154
|
-
|
|
155
|
-
if (
|
|
156
|
-
outputLines[codexRowIndex] != null &&
|
|
157
|
-
!codexBuckets &&
|
|
158
|
-
!existsSync(CODEX_AUTH_PATH) &&
|
|
159
|
-
!hasBrokerCodexAccounts()
|
|
160
|
-
) {
|
|
141
|
+
if (outputLines[codexRowIndex] != null && codexLoggedOut) {
|
|
161
142
|
outputLines[codexRowIndex] = `${DIM}${outputLines[codexRowIndex]}${RESET}`;
|
|
162
143
|
}
|
|
163
144
|
// 알림 배너와 TUI 스타일이 HUD 내용에 겹치지 않도록 한다.
|
|
@@ -2,16 +2,8 @@
|
|
|
2
2
|
// 세션 이벤트(snake_case) 파이프라인에 합류 가능한 스냅샷을 만든다.
|
|
3
3
|
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
mkdirSync,
|
|
8
|
-
mkdtempSync,
|
|
9
|
-
readdirSync,
|
|
10
|
-
readFileSync,
|
|
11
|
-
writeFileSync,
|
|
12
|
-
} from "node:fs";
|
|
13
|
-
import { tmpdir } from "node:os";
|
|
14
|
-
import { dirname, join } from "node:path";
|
|
5
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { dirname } from "node:path";
|
|
15
7
|
|
|
16
8
|
function readJwtExpSec(token) {
|
|
17
9
|
try {
|
|
@@ -54,7 +46,6 @@ export function recordProbe(stateFilePath, key, nowMs) {
|
|
|
54
46
|
|
|
55
47
|
export function listProbeTargets({
|
|
56
48
|
codexAuthPath,
|
|
57
|
-
brokerCacheDir,
|
|
58
49
|
stateFilePath,
|
|
59
50
|
ttlMs,
|
|
60
51
|
nowMs,
|
|
@@ -68,26 +59,6 @@ export function listProbeTargets({
|
|
|
68
59
|
if (currentAuth && accessTokenUsable(currentAuth, nowMs) && due("current")) {
|
|
69
60
|
targets.push({ key: "current", codexHome: dirname(codexAuthPath) });
|
|
70
61
|
}
|
|
71
|
-
|
|
72
|
-
if (brokerCacheDir && existsSync(brokerCacheDir)) {
|
|
73
|
-
let files = [];
|
|
74
|
-
try {
|
|
75
|
-
files = readdirSync(brokerCacheDir).filter((file) =>
|
|
76
|
-
/^codex-auth-.+\.json$/.test(file),
|
|
77
|
-
);
|
|
78
|
-
} catch {
|
|
79
|
-
files = [];
|
|
80
|
-
}
|
|
81
|
-
for (const file of files) {
|
|
82
|
-
const key = file.replace(/^codex-auth-/, "").replace(/\.json$/, "");
|
|
83
|
-
if (!due(key)) continue;
|
|
84
|
-
const auth = readJsonFile(join(brokerCacheDir, file));
|
|
85
|
-
if (!auth || !accessTokenUsable(auth, nowMs)) continue;
|
|
86
|
-
const home = mkdtempSync(join(tmpdir(), `tfx-probe-home-${key}-`));
|
|
87
|
-
writeFileSync(join(home, "auth.json"), JSON.stringify(auth));
|
|
88
|
-
targets.push({ key, codexHome: home });
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
62
|
return targets;
|
|
92
63
|
}
|
|
93
64
|
|