triflux 10.57.2 → 10.58.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/bin/triflux.mjs +14 -7
- package/hub/team/claude-daemon-control.mjs +5 -5
- package/hud/hud-qos-status.mjs +7 -7
- package/hud/prompt-cache.mjs +22 -0
- package/hud/providers/claude.mjs +12 -3
- package/hud/providers/codex.mjs +144 -14
- package/hud/renderers.mjs +40 -25
- package/hud/terminal.mjs +4 -4
- package/package.json +1 -1
package/bin/triflux.mjs
CHANGED
|
@@ -3217,6 +3217,14 @@ function resolveUpdateTargets({ installMode, pluginPath }) {
|
|
|
3217
3217
|
return [];
|
|
3218
3218
|
}
|
|
3219
3219
|
|
|
3220
|
+
// 키는 "<플러그인>@<마켓플레이스>"다. 접두어로 비교하면 triflux-mods@triflux 를
|
|
3221
|
+
// triflux 본체로 잡아 npm 설치본에서 git pull 을 시도한다.
|
|
3222
|
+
export function findTrifluxPluginEntry(installedPlugins) {
|
|
3223
|
+
for (const [key, entries] of Object.entries(installedPlugins?.plugins || {}))
|
|
3224
|
+
if (key.split("@")[0] === "triflux") return entries[0] ?? null;
|
|
3225
|
+
return null;
|
|
3226
|
+
}
|
|
3227
|
+
|
|
3220
3228
|
async function cmdUpdate(args = []) {
|
|
3221
3229
|
if (args.some(isHelpArg)) {
|
|
3222
3230
|
printCommandHelp("update");
|
|
@@ -3233,13 +3241,12 @@ async function cmdUpdate(args = []) {
|
|
|
3233
3241
|
// 플러그인 모드 감지
|
|
3234
3242
|
if (existsSync(pluginsFile)) {
|
|
3235
3243
|
try {
|
|
3236
|
-
const
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
}
|
|
3244
|
+
const entry = findTrifluxPluginEntry(
|
|
3245
|
+
JSON.parse(readFileSync(pluginsFile, "utf8")),
|
|
3246
|
+
);
|
|
3247
|
+
if (entry) {
|
|
3248
|
+
pluginPath = entry.installPath;
|
|
3249
|
+
installMode = "plugin";
|
|
3243
3250
|
}
|
|
3244
3251
|
} catch {}
|
|
3245
3252
|
}
|
|
@@ -34,7 +34,7 @@ export function resolveClaudeHomeDir(
|
|
|
34
34
|
const userProfile = nullableEnv(env.USERPROFILE);
|
|
35
35
|
return userProfile ? path.resolve(userProfile) : os.homedir();
|
|
36
36
|
}
|
|
37
|
-
// POSIX 는 HOME
|
|
37
|
+
// POSIX 는 HOME 우선, sandbox HOME 오버라이드를 추적하는 provenance
|
|
38
38
|
// 해석(#382)과 일치 (os.homedir() 도 POSIX 에서는 HOME 을 우선한다).
|
|
39
39
|
const home = nullableEnv(env.HOME);
|
|
40
40
|
return home ? path.resolve(home) : os.homedir();
|
|
@@ -223,7 +223,7 @@ export function publicClaudeDaemonCandidate(candidate) {
|
|
|
223
223
|
|
|
224
224
|
function sessionsFromDaemonList(listResponse) {
|
|
225
225
|
// probe 경로도 list/find 경로와 같은 정규화 row 를 내보내야 한다
|
|
226
|
-
// (jobs/sessions, snake_case, dispatch 중첩 변형
|
|
226
|
+
// (jobs/sessions, snake_case, dispatch 중첩 변형 흡수, 8016b724).
|
|
227
227
|
return extractClaudeAgentSessions(listResponse);
|
|
228
228
|
}
|
|
229
229
|
|
|
@@ -501,7 +501,7 @@ export function sendClaudeControlRequest(
|
|
|
501
501
|
// 발급한다. control.sock 은 runtime config hash 를 쓰더라도 auth 는 발급 config
|
|
502
502
|
// 에서 읽어야 한다. 일반 daemon 은 <configDir>/daemon/control.key 를 사용한다.
|
|
503
503
|
// 어느 경로에도 파일이 없으면 인증 미강제 daemon 으로 보고 auth 를 생략한다
|
|
504
|
-
// (fail-open
|
|
504
|
+
// (fail-open, 구버전 호환).
|
|
505
505
|
export async function readDaemonControlKey(
|
|
506
506
|
configDir = resolveClaudeConfigDir(),
|
|
507
507
|
{ diagnostics } = {},
|
|
@@ -933,7 +933,7 @@ function isClaudeAttachChromeLine(line) {
|
|
|
933
933
|
) ||
|
|
934
934
|
/^[-─━▔╭╮╰╯│┌┐└┘├┤┬┴┼\s]+$/.test(trimmed) ||
|
|
935
935
|
/^(c|x|g):\s/.test(trimmed) ||
|
|
936
|
-
/
|
|
936
|
+
/ctx:(?:\d+|--)/i.test(trimmed) ||
|
|
937
937
|
/auto mode on/.test(trimmed)
|
|
938
938
|
);
|
|
939
939
|
}
|
|
@@ -1720,7 +1720,7 @@ export async function dispatchClaudeDaemonJob({
|
|
|
1720
1720
|
{ timeoutMs: dispatchTimeoutMs },
|
|
1721
1721
|
);
|
|
1722
1722
|
if (dispatch?.ok !== true) {
|
|
1723
|
-
// daemon 거부 사유를
|
|
1723
|
+
// daemon 거부 사유를 보존한다, generic 메시지만 던지면 EAUTH 같은
|
|
1724
1724
|
// 실원인이 묻혀 진단이 어려워진다.
|
|
1725
1725
|
const reason = dispatch?.error ? `: ${dispatch.error}` : "";
|
|
1726
1726
|
throw new Error(
|
package/hud/hud-qos-status.mjs
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
import { buildContextUsageView } from "./context-monitor.mjs";
|
|
30
30
|
import {
|
|
31
31
|
fetchClaudeUsage,
|
|
32
|
+
getClaudeEmail,
|
|
32
33
|
readClaudeUsageSnapshot,
|
|
33
34
|
scheduleClaudeUsageRefresh,
|
|
34
35
|
} from "./providers/claude.mjs";
|
|
@@ -128,12 +129,10 @@ async function main() {
|
|
|
128
129
|
const rows =
|
|
129
130
|
bandState === "claude"
|
|
130
131
|
? []
|
|
131
|
-
: getClaudeRows(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
Number(stdin?.cost?.total_cost_usd ?? Number.NaN),
|
|
136
|
-
);
|
|
132
|
+
: getClaudeRows(currentTier, contextView, claudeUsage, {
|
|
133
|
+
accountName: getClaudeEmail(),
|
|
134
|
+
promptCache: stdin?.prompt_cache,
|
|
135
|
+
});
|
|
137
136
|
let codexRowIndex = -1;
|
|
138
137
|
if (showCodex) {
|
|
139
138
|
codexRowIndex = rows.length;
|
|
@@ -145,6 +144,7 @@ async function main() {
|
|
|
145
144
|
codexWhite,
|
|
146
145
|
codexBuckets ? { type: "codex", buckets: codexBuckets } : null,
|
|
147
146
|
getCodexEmail(),
|
|
147
|
+
{ promptCache: codexSnapshot.promptCache, loggedOut: codexLoggedOut },
|
|
148
148
|
),
|
|
149
149
|
);
|
|
150
150
|
}
|
|
@@ -179,7 +179,7 @@ async function main() {
|
|
|
179
179
|
|
|
180
180
|
main().catch(() => {
|
|
181
181
|
process.stdout.write(
|
|
182
|
-
`\x1b[0m${bold(claudeOrange("c"))}: ${dim(`5h:--% ${formatTimeCell("")} 1w:--% ${formatTimeCellDH("")} |
|
|
182
|
+
`\x1b[0m${bold(claudeOrange("c"))}: ${dim(`5h:--% ${formatTimeCell("")} 1w:--% ${formatTimeCellDH("")} | ctx:--%`)}\n`,
|
|
183
183
|
);
|
|
184
184
|
});
|
|
185
185
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { dim } from "./colors.mjs";
|
|
2
|
+
|
|
3
|
+
export function getPromptCacheState(expiresAt, ttlMs, now) {
|
|
4
|
+
const remaining = expiresAt - now;
|
|
5
|
+
if (!(remaining > 0)) return "cold";
|
|
6
|
+
return remaining <= Math.min(5 * 60_000, ttlMs * 0.2) ? "cooling" : "warm";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// full 게이지와 퍼센트 사이 공백 한 칸을 캐시 표시로 쓴다. 폭은 늘지 않는다.
|
|
10
|
+
export function cacheGap(cache, color, now = Date.now()) {
|
|
11
|
+
const state = getPromptCacheState(cache?.expiresAt, cache?.ttlMs, now);
|
|
12
|
+
if (state === "cold") return " ";
|
|
13
|
+
return state === "cooling" ? dim("+") : color("+");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function claudePromptCache(cache) {
|
|
17
|
+
if (!cache?.warm || cache.caching_observed === false) return null;
|
|
18
|
+
return {
|
|
19
|
+
expiresAt: cache.expires_at * 1000,
|
|
20
|
+
ttlMs: cache.ttl === "1h" ? 60 * 60_000 : 5 * 60_000,
|
|
21
|
+
};
|
|
22
|
+
}
|
package/hud/providers/claude.mjs
CHANGED
|
@@ -6,7 +6,8 @@ import { execFileSync, spawn } from "node:child_process";
|
|
|
6
6
|
import { createHash } from "node:crypto";
|
|
7
7
|
import { existsSync, writeFileSync } from "node:fs";
|
|
8
8
|
import https from "node:https";
|
|
9
|
-
import { userInfo } from "node:os";
|
|
9
|
+
import { homedir, userInfo } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
10
11
|
import {
|
|
11
12
|
CLAUDE_API_TIMEOUT_MS,
|
|
12
13
|
CLAUDE_CREDENTIALS_PATH,
|
|
@@ -34,6 +35,14 @@ import {
|
|
|
34
35
|
writeJsonSafe,
|
|
35
36
|
} from "../utils.mjs";
|
|
36
37
|
|
|
38
|
+
export function getClaudeEmail() {
|
|
39
|
+
const config = readJson(
|
|
40
|
+
join(process.env.CLAUDE_CONFIG_DIR || homedir(), ".claude.json"),
|
|
41
|
+
null,
|
|
42
|
+
);
|
|
43
|
+
return config?.oauthAccount?.emailAddress || null;
|
|
44
|
+
}
|
|
45
|
+
|
|
37
46
|
export const CLAUDE_USAGE_POLL_BASE_MS = 5_000;
|
|
38
47
|
export const CLAUDE_USAGE_POLL_JITTER_RATIO = 0.2;
|
|
39
48
|
const CLAUDE_KEYCHAIN_SERVICE = "Claude Code-credentials";
|
|
@@ -559,7 +568,7 @@ export function readClaudeUsageSnapshot() {
|
|
|
559
568
|
return { data, shouldRefresh: schedule.shouldRefresh, isStale: false };
|
|
560
569
|
}
|
|
561
570
|
|
|
562
|
-
// 2차: 에러 backoff
|
|
571
|
+
// 2차: 에러 backoff, 최근 에러 시 재시도 억제 (무한 spawn 방지)
|
|
563
572
|
if (cache?.error && Number.isFinite(ts)) {
|
|
564
573
|
if (!schedule.shouldRefresh) {
|
|
565
574
|
const omcCache = readJson(OMC_PLUGIN_USAGE_CACHE_PATH, null);
|
|
@@ -748,7 +757,7 @@ export function scheduleClaudeUsageRefresh() {
|
|
|
748
757
|
try {
|
|
749
758
|
if (!acquireSpawnLock(CLAUDE_REFRESH_LOCK_PATH, SPAWN_LOCK_TTL_MS)) return;
|
|
750
759
|
} catch {
|
|
751
|
-
/* 락 실패
|
|
760
|
+
/* 락 실패 무시, 스폰 진행 */
|
|
752
761
|
}
|
|
753
762
|
|
|
754
763
|
try {
|
package/hud/providers/codex.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
readdirSync,
|
|
12
12
|
readFileSync,
|
|
13
13
|
readSync,
|
|
14
|
+
statSync,
|
|
14
15
|
} from "node:fs";
|
|
15
16
|
import { join } from "node:path";
|
|
16
17
|
import {
|
|
@@ -130,13 +131,13 @@ function positiveWindowMinutes(bucket) {
|
|
|
130
131
|
}
|
|
131
132
|
|
|
132
133
|
// 계정 하나에는 같은 종류의 창이 한 번에 하나만 열린다. Codex 홈(CODEX_HOME)의
|
|
133
|
-
// sessions
|
|
134
|
-
//
|
|
135
|
-
// 옛 창은 조기 리셋(2026-09-23 실측)이나 계정 전환으로 끝난 창이다.
|
|
136
|
-
//
|
|
137
|
-
// 계정이므로 max-used 후보로 남긴다.
|
|
134
|
+
// sessions, 프로브, HUD 라벨은 모두 auth.json 계정 하나의 것이다. 그래서
|
|
135
|
+
// 더 늦게 리셋되는 창이 나타났고 옛 창이 그 창이 열리기 전에만 관측됐다면,
|
|
136
|
+
// 옛 창은 조기 리셋(2026-09-23, 2026-10-11 실측)이나 계정 전환으로 끝난 창이다.
|
|
137
|
+
// 리셋 뒤 세션을 열기 전에는 새 창이 프로브에만 보이므로 프로브도 근거로 쓴다.
|
|
138
|
+
// 새 창이 열린 뒤에도 관측된 창은 함께 도는 계정이므로 max-used 후보로 남긴다.
|
|
138
139
|
function isReplacedWindowGroup(group, other) {
|
|
139
|
-
if (other === group
|
|
140
|
+
if (other === group) return false;
|
|
140
141
|
if (other.minResetAt <= group.maxResetAt || !other.windowMinutes) {
|
|
141
142
|
return false;
|
|
142
143
|
}
|
|
@@ -169,14 +170,12 @@ function selectCodexWindowSnapshot(snapshots, nowSec, windowKey) {
|
|
|
169
170
|
minResetAt: resetAt,
|
|
170
171
|
maxResetAt: resetAt,
|
|
171
172
|
latest: snapshot,
|
|
172
|
-
seenInSessions: !snapshot.probe,
|
|
173
173
|
windowMinutes: positiveWindowMinutes(snapshot[windowKey]),
|
|
174
174
|
};
|
|
175
175
|
groups.push(group);
|
|
176
176
|
continue;
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
if (!snapshot.probe) group.seenInSessions = true;
|
|
180
179
|
group.windowMinutes ??= positiveWindowMinutes(snapshot[windowKey]);
|
|
181
180
|
group.minResetAt = Math.min(group.minResetAt, resetAt);
|
|
182
181
|
group.maxResetAt = Math.max(group.maxResetAt, resetAt);
|
|
@@ -370,6 +369,90 @@ export function createReverseLineReader(
|
|
|
370
369
|
return reader;
|
|
371
370
|
}
|
|
372
371
|
|
|
372
|
+
// GPT-5.6 이후 캐시는 마지막 write/reuse 부터 30분 동안 재사용할 수 있다.
|
|
373
|
+
const CODEX_PROMPT_CACHE_TTL_MS = 30 * 60_000;
|
|
374
|
+
|
|
375
|
+
// 재개한 세션은 생성일 디렉터리의 파일에 계속 쓰므로 날짜가 아니라 수정 시각으로 고른다.
|
|
376
|
+
// TTL 보다 오래 안 바뀐 파일의 캐시는 이미 식었으니 후보에서 뺀다.
|
|
377
|
+
function recentRollouts(sessionsRoot, sinceMs) {
|
|
378
|
+
const found = [];
|
|
379
|
+
const walk = (dir, depth) => {
|
|
380
|
+
let names;
|
|
381
|
+
try {
|
|
382
|
+
names = readdirSync(dir);
|
|
383
|
+
} catch {
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
for (const name of names) {
|
|
387
|
+
const path = join(dir, name);
|
|
388
|
+
if (depth < 3) {
|
|
389
|
+
walk(path, depth + 1);
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
if (!name.endsWith(".jsonl")) continue;
|
|
393
|
+
try {
|
|
394
|
+
const mtimeMs = statSync(path).mtimeMs;
|
|
395
|
+
if (mtimeMs >= sinceMs) found.push({ path, mtimeMs });
|
|
396
|
+
} catch {
|
|
397
|
+
/* 순회 중 사라진 파일 */
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
walk(sessionsRoot, 0);
|
|
402
|
+
return found.sort((a, b) => b.mtimeMs - a.mtimeMs).map((f) => f.path);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// 응답마다 한 줄인 token_usage_record 의 마지막 값으로 판정한다. token_count 는 쿼터 갱신 때
|
|
406
|
+
// 이전 사용량을 다시 보내므로 쓰지 않는다. Claude Code 의 prompt_cache.warm 처럼 마지막 응답이
|
|
407
|
+
// 캐시 토큰을 보고했을 때만 살아 있다. 응답 기록이 없으면 undefined.
|
|
408
|
+
function lastResponseCache(path, maxLines = 200) {
|
|
409
|
+
let reader;
|
|
410
|
+
try {
|
|
411
|
+
reader = createReverseLineReader(path);
|
|
412
|
+
} catch {
|
|
413
|
+
return undefined;
|
|
414
|
+
}
|
|
415
|
+
try {
|
|
416
|
+
for (let i = 0; i < maxLines; i++) {
|
|
417
|
+
const line = reader.next();
|
|
418
|
+
if (line === null) return undefined;
|
|
419
|
+
let evt;
|
|
420
|
+
try {
|
|
421
|
+
evt = JSON.parse(line);
|
|
422
|
+
} catch {
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
const usage =
|
|
426
|
+
evt?.type === "token_usage_record" ? evt.payload?.usage : null;
|
|
427
|
+
if (!usage) continue;
|
|
428
|
+
const cached =
|
|
429
|
+
(usage.cached_input_tokens ?? 0) +
|
|
430
|
+
(usage.cache_write_input_tokens ?? 0);
|
|
431
|
+
const at = Date.parse(evt.timestamp);
|
|
432
|
+
if (cached <= 0 || !Number.isFinite(at)) return null;
|
|
433
|
+
return {
|
|
434
|
+
expiresAt: at + CODEX_PROMPT_CACHE_TTL_MS,
|
|
435
|
+
ttlMs: CODEX_PROMPT_CACHE_TTL_MS,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
return undefined;
|
|
439
|
+
} finally {
|
|
440
|
+
reader.close();
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// 모델 호출이 없는 부모 세션(하위 세션만 띄운 경우)은 건너뛰고 가장 최근 응답을 쓴다.
|
|
445
|
+
export function readCodexPromptCache(sessionsRoot, now = Date.now()) {
|
|
446
|
+
for (const path of recentRollouts(
|
|
447
|
+
sessionsRoot,
|
|
448
|
+
now - CODEX_PROMPT_CACHE_TTL_MS,
|
|
449
|
+
)) {
|
|
450
|
+
const cache = lastResponseCache(path);
|
|
451
|
+
if (cache !== undefined) return cache;
|
|
452
|
+
}
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
|
|
373
456
|
// ============================================================================
|
|
374
457
|
// Codex 세션 JSONL에서 실제 rate limits 추출
|
|
375
458
|
// 한계: rate_limits는 세션별 스냅샷이므로 여러 세션 간 토큰 합산은 불가.
|
|
@@ -504,13 +587,21 @@ export function getCodexRateLimits({
|
|
|
504
587
|
export function readCodexRateLimitSnapshot() {
|
|
505
588
|
const cache = readJson(CODEX_QUOTA_CACHE_PATH, null);
|
|
506
589
|
if (!cache?.buckets) {
|
|
507
|
-
return {
|
|
590
|
+
return {
|
|
591
|
+
buckets: null,
|
|
592
|
+
promptCache: cache?.promptCache ?? null,
|
|
593
|
+
shouldRefresh: true,
|
|
594
|
+
};
|
|
508
595
|
}
|
|
509
596
|
const buckets = expireStaleCodexBuckets(cache.buckets);
|
|
510
597
|
const ts = Number(cache.timestamp);
|
|
511
598
|
const ageMs = Number.isFinite(ts) ? Date.now() - ts : Number.MAX_SAFE_INTEGER;
|
|
512
599
|
const isFresh = ageMs < CODEX_QUOTA_STALE_MS;
|
|
513
|
-
return {
|
|
600
|
+
return {
|
|
601
|
+
buckets,
|
|
602
|
+
promptCache: cache.promptCache ?? null,
|
|
603
|
+
shouldRefresh: !isFresh,
|
|
604
|
+
};
|
|
514
605
|
}
|
|
515
606
|
|
|
516
607
|
export async function collectProbeSnapshots(now = new Date()) {
|
|
@@ -542,12 +633,51 @@ export async function collectProbeSnapshots(now = new Date()) {
|
|
|
542
633
|
);
|
|
543
634
|
}
|
|
544
635
|
|
|
545
|
-
|
|
636
|
+
function getCodexProbeOwner() {
|
|
637
|
+
try {
|
|
638
|
+
const auth = JSON.parse(readFileSync(getCodexAuthPath(), "utf-8"));
|
|
639
|
+
return `${getCodexHome()}|${auth?.tokens?.account_id ?? ""}`;
|
|
640
|
+
} catch {
|
|
641
|
+
return null;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// 프로브는 TTL 마다 한 번만 돈다. 그 사이 갱신도 마지막 프로브를 근거로 써야
|
|
646
|
+
// 조기 리셋으로 밀려난 창이 다시 살아나지 않는다. 다른 계정의 결과나, 프로브가
|
|
647
|
+
// 계속 실패해 낡은 결과가 더 최신 세션 로그를 가리지 않도록 이월 범위를 좁힌다.
|
|
648
|
+
function carryProbe(cached, owner, nowMs) {
|
|
649
|
+
if (!owner || cached?.owner !== owner) return null;
|
|
650
|
+
if (!(nowMs - Number(cached.at) < CODEX_PROBE_TTL_MS * 2)) return null;
|
|
651
|
+
return Array.isArray(cached.snapshots) ? cached : null;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
export async function refreshCodexRateLimitsCache({
|
|
655
|
+
cachePath = CODEX_QUOTA_CACHE_PATH,
|
|
656
|
+
sessionsRoot,
|
|
657
|
+
collect = collectProbeSnapshots,
|
|
658
|
+
owner = getCodexProbeOwner(),
|
|
659
|
+
} = {}) {
|
|
546
660
|
const now = new Date();
|
|
547
|
-
const
|
|
548
|
-
const
|
|
661
|
+
const probed = await collect(now);
|
|
662
|
+
const probe =
|
|
663
|
+
probed.length > 0
|
|
664
|
+
? { owner, at: now.getTime(), snapshots: probed }
|
|
665
|
+
: carryProbe(readJson(cachePath, null)?.probe, owner, now.getTime());
|
|
666
|
+
const buckets = getCodexRateLimits({
|
|
667
|
+
sessionsRoot,
|
|
668
|
+
now,
|
|
669
|
+
extraSnapshots: probe?.snapshots || [],
|
|
670
|
+
});
|
|
671
|
+
const promptCache = readCodexPromptCache(
|
|
672
|
+
sessionsRoot || join(getCodexHome(), "sessions"),
|
|
673
|
+
);
|
|
549
674
|
// buckets가 null이어도 캐시 갱신 (stale 데이터 제거)
|
|
550
|
-
writeJsonSafe(
|
|
675
|
+
writeJsonSafe(cachePath, {
|
|
676
|
+
timestamp: Date.now(),
|
|
677
|
+
buckets,
|
|
678
|
+
promptCache,
|
|
679
|
+
probe,
|
|
680
|
+
});
|
|
551
681
|
return buckets;
|
|
552
682
|
}
|
|
553
683
|
|
package/hud/renderers.mjs
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
TIME_CELL_INNER_WIDTH,
|
|
26
26
|
} from "./constants.mjs";
|
|
27
27
|
import { buildContextUsageView } from "./context-monitor.mjs";
|
|
28
|
+
import { cacheGap, claudePromptCache } from "./prompt-cache.mjs";
|
|
28
29
|
import { getTerminalColumns, tierBar, tierDimBar } from "./terminal.mjs";
|
|
29
30
|
import {
|
|
30
31
|
clampPercent,
|
|
@@ -143,7 +144,7 @@ export function getMicroLine(
|
|
|
143
144
|
: agyText;
|
|
144
145
|
segments.push(`${marker}${dim(":")}${agyValue}`);
|
|
145
146
|
}
|
|
146
|
-
segments.push(`${dim("
|
|
147
|
+
segments.push(`${dim("ctx:")}${contextPercentText(ctxView)}`);
|
|
147
148
|
return truncateAnsi(segments.join(" "), cols);
|
|
148
149
|
}
|
|
149
150
|
|
|
@@ -181,16 +182,16 @@ export function getClaudeRows(
|
|
|
181
182
|
currentTier,
|
|
182
183
|
contextView,
|
|
183
184
|
claudeUsage,
|
|
184
|
-
|
|
185
|
+
{ accountName, promptCache } = {},
|
|
185
186
|
) {
|
|
186
187
|
const ctxView = contextView || buildContextUsageView({});
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
(
|
|
192
|
-
|
|
193
|
-
|
|
188
|
+
const markerColor = claudeOrange;
|
|
189
|
+
const prefix = `${bold(markerColor("c"))}:`;
|
|
190
|
+
// 계정을 ctx 앞에 둬서 x, a 행의 계정 라벨과 같은 열에서 시작하게 한다.
|
|
191
|
+
const accountText = accountName
|
|
192
|
+
? `${markerColor(fitText(getAccountLabel("claude", accountName), ACCOUNT_LABEL_WIDTH))} `
|
|
193
|
+
: "";
|
|
194
|
+
const contextSection = `${dim("ctx:")}${contextPercentText(ctxView)}`;
|
|
194
195
|
const spend = spendOnlyPercent(claudeUsage);
|
|
195
196
|
if (spend != null) {
|
|
196
197
|
if (currentTier === "nano" || currentTier === "micro") {
|
|
@@ -199,7 +200,7 @@ export function getClaudeRows(
|
|
|
199
200
|
}
|
|
200
201
|
const pct = colorByProvider(spend, formatPercentCell(spend), claudeOrange);
|
|
201
202
|
const left = `${dim("$:")}${tierBar(currentTier, spend, CLAUDE_ORANGE)}${pct}`;
|
|
202
|
-
const right = `${
|
|
203
|
+
const right = `${accountText}${contextSection}`;
|
|
203
204
|
return [{ prefix, left, right }];
|
|
204
205
|
}
|
|
205
206
|
// API 실측 데이터
|
|
@@ -230,10 +231,11 @@ export function getClaudeRows(
|
|
|
230
231
|
claudeOrange,
|
|
231
232
|
)
|
|
232
233
|
: dim(formatPlaceholderPercentCell());
|
|
234
|
+
const gap = cacheGap(claudePromptCache(promptCache), claudeOrange);
|
|
233
235
|
const fBar =
|
|
234
236
|
hasData && fiveHourPercent != null
|
|
235
|
-
? tierBar(currentTier, fiveHourPercent, CLAUDE_ORANGE)
|
|
236
|
-
: tierDimBar(currentTier);
|
|
237
|
+
? tierBar(currentTier, fiveHourPercent, CLAUDE_ORANGE, gap)
|
|
238
|
+
: tierDimBar(currentTier, gap);
|
|
237
239
|
const wBar =
|
|
238
240
|
hasData && weeklyPercent != null
|
|
239
241
|
? tierBar(currentTier, weeklyPercent, CLAUDE_ORANGE)
|
|
@@ -257,7 +259,7 @@ export function getClaudeRows(
|
|
|
257
259
|
if (currentTier === "minimal") {
|
|
258
260
|
const staleTag = claudeUsage?.stale ? ` ${dim("[stale]")}` : "";
|
|
259
261
|
const quotaSection = `${dim("5h:")}${fStr} ${dim("1w:")}${wStr}${staleTag}`;
|
|
260
|
-
const right = `${
|
|
262
|
+
const right = `${accountText}${contextSection}`;
|
|
261
263
|
return [{ prefix, left: quotaSection, right }];
|
|
262
264
|
}
|
|
263
265
|
|
|
@@ -267,8 +269,13 @@ export function getClaudeRows(
|
|
|
267
269
|
const warning = ctxView.warningTag
|
|
268
270
|
? ` ${dim("|")} ${yellow(ctxView.warningTag)}`
|
|
269
271
|
: "";
|
|
270
|
-
|
|
271
|
-
|
|
272
|
+
return [
|
|
273
|
+
{
|
|
274
|
+
prefix,
|
|
275
|
+
left: quotaSection,
|
|
276
|
+
right: `${accountText}${contextSection}${warning}`,
|
|
277
|
+
},
|
|
278
|
+
];
|
|
272
279
|
}
|
|
273
280
|
|
|
274
281
|
// full tier (>= 120 cols)
|
|
@@ -277,8 +284,13 @@ export function getClaudeRows(
|
|
|
277
284
|
const warning = ctxView.warningTag
|
|
278
285
|
? ` ${dim("|")} ${yellow(ctxView.warningTag)}`
|
|
279
286
|
: "";
|
|
280
|
-
|
|
281
|
-
|
|
287
|
+
return [
|
|
288
|
+
{
|
|
289
|
+
prefix,
|
|
290
|
+
left: quotaSection,
|
|
291
|
+
right: `${accountText}${contextSection}${warning}`,
|
|
292
|
+
},
|
|
293
|
+
];
|
|
282
294
|
}
|
|
283
295
|
|
|
284
296
|
// ============================================================================
|
|
@@ -296,13 +308,16 @@ export function getProviderRow(
|
|
|
296
308
|
markerColor,
|
|
297
309
|
realQuota,
|
|
298
310
|
accountName,
|
|
311
|
+
{ promptCache, loggedOut = false } = {},
|
|
299
312
|
) {
|
|
300
313
|
const accountLabel = fitText(
|
|
301
314
|
getAccountLabel(provider, accountName),
|
|
302
315
|
ACCOUNT_LABEL_WIDTH,
|
|
303
316
|
);
|
|
304
317
|
|
|
305
|
-
const prefix =
|
|
318
|
+
const prefix = loggedOut
|
|
319
|
+
? dim(`${marker}:`)
|
|
320
|
+
: `${bold(markerColor(marker))}:`;
|
|
306
321
|
if (provider === "antigravity") {
|
|
307
322
|
return getAntigravityRow(
|
|
308
323
|
currentTier,
|
|
@@ -317,7 +332,6 @@ export function getProviderRow(
|
|
|
317
332
|
let quotaSection;
|
|
318
333
|
|
|
319
334
|
if (currentTier === "nano" || currentTier === "micro") {
|
|
320
|
-
const minPrefix = `${bold(markerColor(`${marker}`))}:`;
|
|
321
335
|
if (realQuota?.type === "codex") {
|
|
322
336
|
const main =
|
|
323
337
|
realQuota.buckets.codex ||
|
|
@@ -340,13 +354,13 @@ export function getProviderRow(
|
|
|
340
354
|
? colorByProvider(weekP, `${weekP}%`, provFn)
|
|
341
355
|
: dim("--%");
|
|
342
356
|
return {
|
|
343
|
-
prefix
|
|
357
|
+
prefix,
|
|
344
358
|
left: `${fCellN}${dim("/")}${wCellN}`,
|
|
345
359
|
right: "",
|
|
346
360
|
};
|
|
347
361
|
}
|
|
348
362
|
}
|
|
349
|
-
return { prefix
|
|
363
|
+
return { prefix, left: dim("--%/--%"), right: "" };
|
|
350
364
|
}
|
|
351
365
|
|
|
352
366
|
if (currentTier === "minimal") {
|
|
@@ -424,6 +438,7 @@ export function getProviderRow(
|
|
|
424
438
|
}
|
|
425
439
|
|
|
426
440
|
// full tier
|
|
441
|
+
const gap = cacheGap(promptCache, markerColor);
|
|
427
442
|
|
|
428
443
|
if (realQuota?.type === "codex") {
|
|
429
444
|
const main =
|
|
@@ -450,8 +465,8 @@ export function getProviderRow(
|
|
|
450
465
|
: dim(formatPlaceholderPercentCell());
|
|
451
466
|
const fBar =
|
|
452
467
|
fiveP != null
|
|
453
|
-
? tierBar(currentTier, fiveP, provAnsi)
|
|
454
|
-
: tierDimBar(currentTier);
|
|
468
|
+
? tierBar(currentTier, fiveP, provAnsi, gap)
|
|
469
|
+
: tierDimBar(currentTier, gap);
|
|
455
470
|
const wBar =
|
|
456
471
|
weekP != null
|
|
457
472
|
? tierBar(currentTier, weekP, provAnsi)
|
|
@@ -467,7 +482,7 @@ export function getProviderRow(
|
|
|
467
482
|
|
|
468
483
|
// 폴백
|
|
469
484
|
if (!quotaSection) {
|
|
470
|
-
quotaSection = `${dim("5h:")}${tierDimBar(currentTier)}${dim(formatPlaceholderPercentCell())} ${dim(formatTimeCell(""))} ${dim("1w:")}${tierDimBar(currentTier)}${dim(formatPlaceholderPercentCell())} ${dim(formatTimeCellDH(""))}`;
|
|
485
|
+
quotaSection = `${dim("5h:")}${tierDimBar(currentTier, gap)}${dim(formatPlaceholderPercentCell())} ${dim(formatTimeCell(""))} ${dim("1w:")}${tierDimBar(currentTier)}${dim(formatPlaceholderPercentCell())} ${dim(formatTimeCellDH(""))}`;
|
|
471
486
|
}
|
|
472
487
|
|
|
473
488
|
const accountSection = `${markerColor(accountLabel)}`;
|
|
@@ -515,7 +530,7 @@ function getAntigravityRow(
|
|
|
515
530
|
let slot;
|
|
516
531
|
if (project) {
|
|
517
532
|
// 쿼터 대신 과금 방식을 막대 자리(막대가 없는 표시에서는 퍼센트 자리)에 보인다.
|
|
518
|
-
const mark = isFull ? `${
|
|
533
|
+
const mark = isFull ? `${geminiBlue(" GCP ")} ` : "";
|
|
519
534
|
const cell = isFull ? dim(formatPlaceholderPercentCell()) : percent;
|
|
520
535
|
slot = `${dim("--:")}${mark}${cell}${timeCell}`;
|
|
521
536
|
} else {
|
package/hud/terminal.mjs
CHANGED
|
@@ -136,13 +136,13 @@ export function selectTier() {
|
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
// full tier 전용: 게이지 바 접두사 (normal 이하 tier에서는 빈 문자열)
|
|
139
|
-
export function tierBar(currentTier, percent, baseColor = null) {
|
|
139
|
+
export function tierBar(currentTier, percent, baseColor = null, gap = " ") {
|
|
140
140
|
return currentTier === "full"
|
|
141
|
-
? coloredBar(percent, GAUGE_WIDTH, baseColor) +
|
|
141
|
+
? coloredBar(percent, GAUGE_WIDTH, baseColor) + gap
|
|
142
142
|
: "";
|
|
143
143
|
}
|
|
144
|
-
export function tierDimBar(currentTier) {
|
|
144
|
+
export function tierDimBar(currentTier, gap = " ") {
|
|
145
145
|
return currentTier === "full"
|
|
146
|
-
? DIM + "░".repeat(GAUGE_WIDTH) + RESET +
|
|
146
|
+
? DIM + "░".repeat(GAUGE_WIDTH) + RESET + gap
|
|
147
147
|
: "";
|
|
148
148
|
}
|