triflux 2.4.7 → 3.0.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/README.ko.md +9 -7
- package/README.md +9 -7
- package/bin/tfx-doctor.mjs +1 -1
- package/bin/tfx-setup.mjs +1 -1
- package/bin/triflux.mjs +25 -13
- package/hud/hud-qos-status.mjs +221 -108
- package/package.json +1 -1
- package/scripts/cli-route.sh +2 -967
- package/scripts/mcp-check.mjs +2 -2
- package/scripts/notion-read.mjs +104 -49
- package/scripts/setup.mjs +10 -5
- package/scripts/tfx-batch-stats.mjs +96 -0
- package/scripts/tfx-route-post.mjs +366 -0
- package/scripts/tfx-route.sh +448 -0
- package/skills/tfx-auto/SKILL.md +31 -31
- package/skills/tfx-codex/SKILL.md +1 -1
- package/skills/tfx-doctor/SKILL.md +2 -2
- package/skills/tfx-gemini/SKILL.md +1 -1
- package/skills/tfx-setup/SKILL.md +1 -1
package/hud/hud-qos-status.mjs
CHANGED
|
@@ -121,6 +121,9 @@ function getClaudeUsageStaleMs() {
|
|
|
121
121
|
const CLAUDE_USAGE_429_BACKOFF_MS = 10 * 60 * 1000; // 429 에러 시 10분 backoff
|
|
122
122
|
const CLAUDE_USAGE_ERROR_BACKOFF_MS = 3 * 60 * 1000; // 기타 에러 시 3분 backoff
|
|
123
123
|
const CLAUDE_API_TIMEOUT_MS = 10_000;
|
|
124
|
+
const FIVE_HOUR_MS = 5 * 60 * 60 * 1000;
|
|
125
|
+
const SEVEN_DAY_MS = 7 * 24 * 60 * 60 * 1000;
|
|
126
|
+
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
|
|
124
127
|
const DEFAULT_OAUTH_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
|
|
125
128
|
const CODEX_AUTH_PATH = join(homedir(), ".codex", "auth.json");
|
|
126
129
|
const CODEX_QUOTA_CACHE_PATH = join(homedir(), ".claude", "cache", "codex-rate-limits-cache.json");
|
|
@@ -152,6 +155,14 @@ function getGeminiModelLabel(model) {
|
|
|
152
155
|
if (model.includes("flash")) return "[Flash3]";
|
|
153
156
|
return "";
|
|
154
157
|
}
|
|
158
|
+
// rows 임계값 상수 (selectTier 에서 tier 결정에 사용)
|
|
159
|
+
const ROWS_BUDGET_FULL = 40;
|
|
160
|
+
const ROWS_BUDGET_LARGE = 35;
|
|
161
|
+
const ROWS_BUDGET_MEDIUM = 28;
|
|
162
|
+
const ROWS_BUDGET_SMALL = 22;
|
|
163
|
+
// Codex rate_limits에서 최소 수집할 버킷 수
|
|
164
|
+
const CODEX_MIN_BUCKETS = 2;
|
|
165
|
+
|
|
155
166
|
const GEMINI_RPM_WINDOW_MS = 60 * 1000; // 60초 슬라이딩 윈도우
|
|
156
167
|
const GEMINI_QUOTA_STALE_MS = 5 * 60 * 1000; // 5분
|
|
157
168
|
const GEMINI_SESSION_STALE_MS = 15 * 1000; // 15초
|
|
@@ -291,10 +302,10 @@ function selectTier(stdin, claudeUsage = null) {
|
|
|
291
302
|
const cols = getTerminalColumns() || 120;
|
|
292
303
|
|
|
293
304
|
let budget;
|
|
294
|
-
if (rows >=
|
|
295
|
-
else if (rows >=
|
|
296
|
-
else if (rows >=
|
|
297
|
-
else if (rows >=
|
|
305
|
+
if (rows >= ROWS_BUDGET_FULL) budget = 6;
|
|
306
|
+
else if (rows >= ROWS_BUDGET_LARGE) budget = 5;
|
|
307
|
+
else if (rows >= ROWS_BUDGET_MEDIUM) budget = 4;
|
|
308
|
+
else if (rows >= ROWS_BUDGET_SMALL) budget = 3;
|
|
298
309
|
else if (rows > 0) budget = 2;
|
|
299
310
|
else budget = 5; // rows 감지 불가 → 넉넉하게
|
|
300
311
|
|
|
@@ -437,25 +448,21 @@ function renderAlignedRows(rows) {
|
|
|
437
448
|
function getMicroLine(stdin, claudeUsage, codexBuckets, geminiSession, geminiBucket, combinedSvPct) {
|
|
438
449
|
const ctx = getContextPercent(stdin);
|
|
439
450
|
|
|
440
|
-
// Claude 5h/1w (
|
|
441
|
-
const cF = claudeUsage
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
? (isResetPast(claudeUsage.weeklyResetsAt) ? 0 : clampPercent(claudeUsage.weeklyPercent ?? 0))
|
|
446
|
-
: null;
|
|
447
|
-
const cVal = cF != null
|
|
448
|
-
? `${colorByProvider(cF, `${cF}`, claudeOrange)}${dim("/")}${colorByProvider(cW, `${cW}`, claudeOrange)}`
|
|
451
|
+
// Claude 5h/1w (캐시된 값 그대로 표시, 시간은 advanceToNextCycle이 처리)
|
|
452
|
+
const cF = claudeUsage?.fiveHourPercent != null ? clampPercent(claudeUsage.fiveHourPercent) : null;
|
|
453
|
+
const cW = claudeUsage?.weeklyPercent != null ? clampPercent(claudeUsage.weeklyPercent) : null;
|
|
454
|
+
const cVal = claudeUsage != null
|
|
455
|
+
? `${cF != null ? colorByProvider(cF, `${cF}`, claudeOrange) : dim("--")}${dim("/")}${cW != null ? colorByProvider(cW, `${cW}`, claudeOrange) : dim("--")}`
|
|
449
456
|
: dim("--/--");
|
|
450
457
|
|
|
451
|
-
// Codex 5h/1w
|
|
458
|
+
// Codex 5h/1w (캐시된 값 그대로 표시)
|
|
452
459
|
let xVal = dim("--/--");
|
|
453
460
|
if (codexBuckets) {
|
|
454
461
|
const mb = codexBuckets.codex || codexBuckets[Object.keys(codexBuckets)[0]];
|
|
455
462
|
if (mb) {
|
|
456
|
-
const xF =
|
|
457
|
-
const xW =
|
|
458
|
-
xVal = `${colorByProvider(xF, `${xF}`, codexWhite)}${dim("/")}${colorByProvider(xW, `${xW}`, codexWhite)}`;
|
|
463
|
+
const xF = mb.primary?.used_percent != null ? clampPercent(mb.primary.used_percent) : null;
|
|
464
|
+
const xW = mb.secondary?.used_percent != null ? clampPercent(mb.secondary.used_percent) : null;
|
|
465
|
+
xVal = `${xF != null ? colorByProvider(xF, `${xF}`, codexWhite) : dim("--")}${dim("/")}${xW != null ? colorByProvider(xW, `${xW}`, codexWhite) : dim("--")}`;
|
|
459
466
|
}
|
|
460
467
|
}
|
|
461
468
|
|
|
@@ -502,7 +509,7 @@ function normalizeTimeToken(value) {
|
|
|
502
509
|
}
|
|
503
510
|
const dayHour = text.match(/^(\d+)d(\d+)h$/);
|
|
504
511
|
if (dayHour) {
|
|
505
|
-
return `${Number(dayHour[1])}d${Number(dayHour[2])}h`;
|
|
512
|
+
return `${Number(dayHour[1])}d${String(Number(dayHour[2])).padStart(2, "0")}h`;
|
|
506
513
|
}
|
|
507
514
|
return text;
|
|
508
515
|
}
|
|
@@ -667,31 +674,15 @@ function parseClaudeUsageResponse(response) {
|
|
|
667
674
|
// stale 캐시의 과거 resetsAt → 다음 주기로 순환 추정 (null 대신 다음 reset 시간 계산)
|
|
668
675
|
function stripStaleResets(data) {
|
|
669
676
|
if (!data) return data;
|
|
670
|
-
const now = Date.now();
|
|
671
677
|
const copy = { ...data };
|
|
672
|
-
|
|
673
|
-
// 5시간 주기: 과거 reset → 5시간씩 전진하여 미래 시점 추정
|
|
674
678
|
if (copy.fiveHourResetsAt) {
|
|
675
|
-
|
|
676
|
-
if (t
|
|
677
|
-
const cycle = 5 * 60 * 60 * 1000;
|
|
678
|
-
const elapsed = now - t;
|
|
679
|
-
t += Math.ceil(elapsed / cycle) * cycle;
|
|
680
|
-
copy.fiveHourResetsAt = new Date(t).toISOString();
|
|
681
|
-
}
|
|
679
|
+
const t = new Date(copy.fiveHourResetsAt).getTime();
|
|
680
|
+
if (!isNaN(t)) copy.fiveHourResetsAt = new Date(advanceToNextCycle(t, FIVE_HOUR_MS)).toISOString();
|
|
682
681
|
}
|
|
683
|
-
|
|
684
|
-
// 7일 주기: 과거 reset → 7일씩 전진하여 미래 시점 추정
|
|
685
682
|
if (copy.weeklyResetsAt) {
|
|
686
|
-
|
|
687
|
-
if (t
|
|
688
|
-
const cycle = 7 * 24 * 60 * 60 * 1000;
|
|
689
|
-
const elapsed = now - t;
|
|
690
|
-
t += Math.ceil(elapsed / cycle) * cycle;
|
|
691
|
-
copy.weeklyResetsAt = new Date(t).toISOString();
|
|
692
|
-
}
|
|
683
|
+
const t = new Date(copy.weeklyResetsAt).getTime();
|
|
684
|
+
if (!isNaN(t)) copy.weeklyResetsAt = new Date(advanceToNextCycle(t, SEVEN_DAY_MS)).toISOString();
|
|
693
685
|
}
|
|
694
|
-
|
|
695
686
|
return copy;
|
|
696
687
|
}
|
|
697
688
|
|
|
@@ -817,12 +808,15 @@ function scheduleClaudeUsageRefresh() {
|
|
|
817
808
|
|
|
818
809
|
try {
|
|
819
810
|
const child = spawn(process.execPath, [scriptPath, CLAUDE_REFRESH_FLAG], {
|
|
820
|
-
detached:
|
|
811
|
+
detached: true,
|
|
821
812
|
stdio: "ignore",
|
|
822
813
|
windowsHide: true,
|
|
823
814
|
});
|
|
824
815
|
child.unref();
|
|
825
|
-
} catch {
|
|
816
|
+
} catch (spawnErr) {
|
|
817
|
+
// spawn 실패 시 에러 유형을 캐시에 기록 (HUD에서 원인 힌트 표시 가능)
|
|
818
|
+
writeClaudeUsageCache(null, { type: "network", status: 0, hint: String(spawnErr?.message || spawnErr) });
|
|
819
|
+
}
|
|
826
820
|
}
|
|
827
821
|
|
|
828
822
|
function getContextPercent(stdin) {
|
|
@@ -837,11 +831,20 @@ function getContextPercent(stdin) {
|
|
|
837
831
|
return clampPercent((totalTokens / capacity) * 100);
|
|
838
832
|
}
|
|
839
833
|
|
|
840
|
-
|
|
834
|
+
// 과거 리셋 시간 → 다음 주기로 순환하여 미래 시점 반환
|
|
835
|
+
function advanceToNextCycle(epochMs, cycleMs) {
|
|
836
|
+
const now = Date.now();
|
|
837
|
+
if (epochMs >= now || !cycleMs) return epochMs;
|
|
838
|
+
const elapsed = now - epochMs;
|
|
839
|
+
return epochMs + Math.ceil(elapsed / cycleMs) * cycleMs;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function formatResetRemaining(isoOrUnix, cycleMs = 0) {
|
|
841
843
|
if (!isoOrUnix) return "";
|
|
842
844
|
const d = typeof isoOrUnix === "string" ? new Date(isoOrUnix) : new Date(isoOrUnix * 1000);
|
|
843
845
|
if (isNaN(d.getTime())) return "";
|
|
844
|
-
const
|
|
846
|
+
const targetMs = advanceToNextCycle(d.getTime(), cycleMs);
|
|
847
|
+
const diffMs = targetMs - Date.now();
|
|
845
848
|
if (diffMs <= 0) return "";
|
|
846
849
|
const totalMinutes = Math.floor(diffMs / 60000);
|
|
847
850
|
const totalHours = Math.floor(totalMinutes / 60);
|
|
@@ -855,11 +858,12 @@ function isResetPast(isoOrUnix) {
|
|
|
855
858
|
return !isNaN(d.getTime()) && d.getTime() <= Date.now();
|
|
856
859
|
}
|
|
857
860
|
|
|
858
|
-
function formatResetRemainingDayHour(isoOrUnix) {
|
|
861
|
+
function formatResetRemainingDayHour(isoOrUnix, cycleMs = 0) {
|
|
859
862
|
if (!isoOrUnix) return "";
|
|
860
863
|
const d = typeof isoOrUnix === "string" ? new Date(isoOrUnix) : new Date(isoOrUnix * 1000);
|
|
861
864
|
if (isNaN(d.getTime())) return "";
|
|
862
|
-
const
|
|
865
|
+
const targetMs = advanceToNextCycle(d.getTime(), cycleMs);
|
|
866
|
+
const diffMs = targetMs - Date.now();
|
|
863
867
|
if (diffMs <= 0) return "";
|
|
864
868
|
const totalMinutes = Math.floor(diffMs / 60000);
|
|
865
869
|
const days = Math.floor(totalMinutes / (60 * 24));
|
|
@@ -906,20 +910,33 @@ function httpsPost(url, body, accessToken) {
|
|
|
906
910
|
});
|
|
907
911
|
}
|
|
908
912
|
|
|
913
|
+
// ============================================================================
|
|
914
|
+
// JWT base64 디코딩 공통 헬퍼
|
|
915
|
+
// ============================================================================
|
|
916
|
+
/**
|
|
917
|
+
* JWT 파일에서 이메일을 추출하는 공통 헬퍼.
|
|
918
|
+
* @param {string|null} idToken - JWT 문자열
|
|
919
|
+
* @returns {string|null} 이메일 또는 null
|
|
920
|
+
*/
|
|
921
|
+
function decodeJwtEmail(idToken) {
|
|
922
|
+
if (!idToken) return null;
|
|
923
|
+
const parts = idToken.split(".");
|
|
924
|
+
if (parts.length < 2) return null;
|
|
925
|
+
let payload = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
926
|
+
while (payload.length % 4) payload += "=";
|
|
927
|
+
try {
|
|
928
|
+
const decoded = JSON.parse(Buffer.from(payload, "base64").toString("utf-8"));
|
|
929
|
+
return decoded.email || null;
|
|
930
|
+
} catch { return null; }
|
|
931
|
+
}
|
|
932
|
+
|
|
909
933
|
// ============================================================================
|
|
910
934
|
// Codex JWT에서 이메일 추출
|
|
911
935
|
// ============================================================================
|
|
912
936
|
function getCodexEmail() {
|
|
913
937
|
try {
|
|
914
938
|
const auth = JSON.parse(readFileSync(CODEX_AUTH_PATH, "utf-8"));
|
|
915
|
-
|
|
916
|
-
if (!idToken) return null;
|
|
917
|
-
const parts = idToken.split(".");
|
|
918
|
-
if (parts.length < 2) return null;
|
|
919
|
-
let payload = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
920
|
-
while (payload.length % 4) payload += "=";
|
|
921
|
-
const decoded = JSON.parse(Buffer.from(payload, "base64").toString("utf-8"));
|
|
922
|
-
return decoded.email || null;
|
|
939
|
+
return decodeJwtEmail(auth?.tokens?.id_token);
|
|
923
940
|
} catch { return null; }
|
|
924
941
|
}
|
|
925
942
|
|
|
@@ -929,23 +946,21 @@ function getCodexEmail() {
|
|
|
929
946
|
function getGeminiEmail() {
|
|
930
947
|
try {
|
|
931
948
|
const oauth = readJson(GEMINI_OAUTH_PATH, null);
|
|
932
|
-
|
|
933
|
-
if (!idToken) return null;
|
|
934
|
-
const parts = idToken.split(".");
|
|
935
|
-
if (parts.length < 2) return null;
|
|
936
|
-
let payload = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
937
|
-
while (payload.length % 4) payload += "=";
|
|
938
|
-
const decoded = JSON.parse(Buffer.from(payload, "base64").toString("utf-8"));
|
|
939
|
-
return decoded.email || null;
|
|
949
|
+
return decodeJwtEmail(oauth?.id_token);
|
|
940
950
|
} catch { return null; }
|
|
941
951
|
}
|
|
942
952
|
|
|
943
953
|
// ============================================================================
|
|
944
954
|
// Codex 세션 JSONL에서 실제 rate limits 추출
|
|
955
|
+
// 한계: rate_limits는 세션별 스냅샷이므로 여러 세션 간 토큰 합산은 불가.
|
|
956
|
+
// 오늘 날짜의 모든 세션 파일을 스캔해 가장 최신 rate_limits 버킷을 수집한다.
|
|
957
|
+
// (단일 파일 즉시 return 방식에서 → 당일 전체 스캔 후 최신 데이터 우선 병합으로 변경)
|
|
945
958
|
// ============================================================================
|
|
946
959
|
function getCodexRateLimits() {
|
|
947
960
|
const now = new Date();
|
|
948
|
-
let
|
|
961
|
+
let syntheticBucket = null; // 오늘 token_count에서 합성 (행 활성화 + 토큰 데이터용)
|
|
962
|
+
|
|
963
|
+
// 2일간 스캔: 실제 rate_limits 우선, 합성 버킷은 폴백
|
|
949
964
|
for (let dayOffset = 0; dayOffset <= 1; dayOffset++) {
|
|
950
965
|
const d = new Date(now.getTime() - dayOffset * 86_400_000);
|
|
951
966
|
const sessDir = join(
|
|
@@ -958,18 +973,19 @@ function getCodexRateLimits() {
|
|
|
958
973
|
let files;
|
|
959
974
|
try { files = readdirSync(sessDir).filter((f) => f.endsWith(".jsonl")).sort().reverse(); }
|
|
960
975
|
catch { continue; }
|
|
961
|
-
|
|
976
|
+
|
|
977
|
+
const mergedBuckets = {};
|
|
962
978
|
for (const file of files) {
|
|
963
979
|
try {
|
|
964
980
|
const content = readFileSync(join(sessDir, file), "utf-8");
|
|
965
981
|
const lines = content.trim().split("\n").reverse();
|
|
966
|
-
const buckets = {};
|
|
967
982
|
for (const line of lines) {
|
|
968
983
|
try {
|
|
969
984
|
const evt = JSON.parse(line);
|
|
970
985
|
const rl = evt?.payload?.rate_limits;
|
|
971
|
-
if (rl?.limit_id && !
|
|
972
|
-
|
|
986
|
+
if (rl?.limit_id && !mergedBuckets[rl.limit_id]) {
|
|
987
|
+
// 실제 rate_limits: limit_id별 최신 이벤트만 기록
|
|
988
|
+
mergedBuckets[rl.limit_id] = {
|
|
973
989
|
limitId: rl.limit_id, limitName: rl.limit_name,
|
|
974
990
|
primary: rl.primary, secondary: rl.secondary,
|
|
975
991
|
credits: rl.credits,
|
|
@@ -977,17 +993,33 @@ function getCodexRateLimits() {
|
|
|
977
993
|
contextWindow: evt.payload?.info?.model_context_window,
|
|
978
994
|
timestamp: evt.timestamp,
|
|
979
995
|
};
|
|
996
|
+
} else if (dayOffset === 0 && !rl && evt?.payload?.info?.total_token_usage && !syntheticBucket) {
|
|
997
|
+
// 오늘 token_count: 합성 버킷 (rate_limits가 null일 때 행 활성화용)
|
|
998
|
+
syntheticBucket = {
|
|
999
|
+
limitId: "codex", limitName: "codex-session",
|
|
1000
|
+
primary: null, secondary: null,
|
|
1001
|
+
credits: null,
|
|
1002
|
+
tokens: evt.payload.info.total_token_usage,
|
|
1003
|
+
contextWindow: evt.payload.info.model_context_window,
|
|
1004
|
+
timestamp: evt.timestamp,
|
|
1005
|
+
};
|
|
980
1006
|
}
|
|
981
1007
|
} catch { /* 라인 파싱 실패 무시 */ }
|
|
982
|
-
if (Object.keys(
|
|
1008
|
+
if (Object.keys(mergedBuckets).length >= CODEX_MIN_BUCKETS) break;
|
|
983
1009
|
}
|
|
984
|
-
if (Object.keys(buckets).length > 0) return buckets;
|
|
985
1010
|
} catch { /* 파일 읽기 실패 무시 */ }
|
|
986
1011
|
}
|
|
987
|
-
//
|
|
988
|
-
if (
|
|
1012
|
+
// 실제 rate_limits 발견 → 오늘 토큰 데이터 병합 후 즉시 반환
|
|
1013
|
+
if (Object.keys(mergedBuckets).length > 0) {
|
|
1014
|
+
if (syntheticBucket) {
|
|
1015
|
+
const main = mergedBuckets.codex || mergedBuckets[Object.keys(mergedBuckets)[0]];
|
|
1016
|
+
if (main && !main.tokens) main.tokens = syntheticBucket.tokens;
|
|
1017
|
+
}
|
|
1018
|
+
return mergedBuckets;
|
|
1019
|
+
}
|
|
989
1020
|
}
|
|
990
|
-
|
|
1021
|
+
// 실제 rate_limits 없음 → 합성 버킷이라도 반환 (행 활성화)
|
|
1022
|
+
return syntheticBucket ? { codex: syntheticBucket } : null;
|
|
991
1023
|
}
|
|
992
1024
|
|
|
993
1025
|
// ============================================================================
|
|
@@ -1007,8 +1039,28 @@ async function fetchGeminiQuota(accountId, options = {}) {
|
|
|
1007
1039
|
return cache;
|
|
1008
1040
|
}
|
|
1009
1041
|
|
|
1010
|
-
if (!oauth?.access_token)
|
|
1011
|
-
|
|
1042
|
+
if (!oauth?.access_token) {
|
|
1043
|
+
// access_token 없음: 에러 힌트를 캐시에 기록하고 stale 캐시 반환
|
|
1044
|
+
writeJsonSafe(GEMINI_QUOTA_CACHE_PATH, {
|
|
1045
|
+
...(cache || {}),
|
|
1046
|
+
timestamp: cache?.timestamp || Date.now(),
|
|
1047
|
+
error: true,
|
|
1048
|
+
errorType: "auth",
|
|
1049
|
+
errorHint: "no access_token in oauth_creds.json",
|
|
1050
|
+
});
|
|
1051
|
+
return cache;
|
|
1052
|
+
}
|
|
1053
|
+
if (oauth.expiry_date && oauth.expiry_date < Date.now()) {
|
|
1054
|
+
// OAuth 토큰 만료: 에러 힌트를 캐시에 기록 (refresh_token 갱신은 Gemini CLI 담당)
|
|
1055
|
+
writeJsonSafe(GEMINI_QUOTA_CACHE_PATH, {
|
|
1056
|
+
...(cache || {}),
|
|
1057
|
+
timestamp: cache?.timestamp || Date.now(),
|
|
1058
|
+
error: true,
|
|
1059
|
+
errorType: "auth",
|
|
1060
|
+
errorHint: `token expired at ${new Date(oauth.expiry_date).toISOString()}`,
|
|
1061
|
+
});
|
|
1062
|
+
return cache;
|
|
1063
|
+
}
|
|
1012
1064
|
|
|
1013
1065
|
// 3. projectId (캐시 or API)
|
|
1014
1066
|
const fetchProjectId = async () => {
|
|
@@ -1045,7 +1097,18 @@ async function fetchGeminiQuota(accountId, options = {}) {
|
|
|
1045
1097
|
);
|
|
1046
1098
|
}
|
|
1047
1099
|
|
|
1048
|
-
if (!quotaRes?.buckets)
|
|
1100
|
+
if (!quotaRes?.buckets) {
|
|
1101
|
+
// API 응답에 buckets 없음: 에러 코드 또는 응답 내용을 캐시에 기록
|
|
1102
|
+
const apiError = quotaRes?.error?.message || quotaRes?.error?.code || quotaRes?.error || "no buckets in response";
|
|
1103
|
+
writeJsonSafe(GEMINI_QUOTA_CACHE_PATH, {
|
|
1104
|
+
...(cache || {}),
|
|
1105
|
+
timestamp: cache?.timestamp || Date.now(),
|
|
1106
|
+
error: true,
|
|
1107
|
+
errorType: "api",
|
|
1108
|
+
errorHint: String(apiError),
|
|
1109
|
+
});
|
|
1110
|
+
return cache;
|
|
1111
|
+
}
|
|
1049
1112
|
|
|
1050
1113
|
// 5. 캐시 저장
|
|
1051
1114
|
const result = {
|
|
@@ -1120,13 +1183,20 @@ function scheduleGeminiQuotaRefresh(accountId) {
|
|
|
1120
1183
|
process.execPath,
|
|
1121
1184
|
[scriptPath, GEMINI_REFRESH_FLAG, "--account", accountId || "gemini-main"],
|
|
1122
1185
|
{
|
|
1123
|
-
detached:
|
|
1186
|
+
detached: true,
|
|
1124
1187
|
stdio: "ignore",
|
|
1125
1188
|
windowsHide: true,
|
|
1126
1189
|
},
|
|
1127
1190
|
);
|
|
1128
1191
|
child.unref();
|
|
1129
|
-
} catch {
|
|
1192
|
+
} catch (spawnErr) {
|
|
1193
|
+
// spawn 실패 시 캐시에 에러 힌트 기록 (다음 HUD 렌더에서 원인 확인 가능)
|
|
1194
|
+
writeJsonSafe(GEMINI_QUOTA_CACHE_PATH, {
|
|
1195
|
+
timestamp: Date.now(),
|
|
1196
|
+
error: true,
|
|
1197
|
+
errorHint: String(spawnErr?.message || spawnErr),
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1130
1200
|
}
|
|
1131
1201
|
|
|
1132
1202
|
function readCodexRateLimitSnapshot() {
|
|
@@ -1152,12 +1222,20 @@ function scheduleCodexRateLimitRefresh() {
|
|
|
1152
1222
|
if (!scriptPath) return;
|
|
1153
1223
|
try {
|
|
1154
1224
|
const child = spawn(process.execPath, [scriptPath, CODEX_REFRESH_FLAG], {
|
|
1155
|
-
detached:
|
|
1225
|
+
detached: true,
|
|
1156
1226
|
stdio: "ignore",
|
|
1157
1227
|
windowsHide: true,
|
|
1158
1228
|
});
|
|
1159
1229
|
child.unref();
|
|
1160
|
-
} catch {
|
|
1230
|
+
} catch (spawnErr) {
|
|
1231
|
+
// spawn 실패 시 캐시에 에러 힌트 기록
|
|
1232
|
+
writeJsonSafe(CODEX_QUOTA_CACHE_PATH, {
|
|
1233
|
+
timestamp: Date.now(),
|
|
1234
|
+
buckets: null,
|
|
1235
|
+
error: true,
|
|
1236
|
+
errorHint: String(spawnErr?.message || spawnErr),
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1161
1239
|
}
|
|
1162
1240
|
|
|
1163
1241
|
function readGeminiSessionSnapshot() {
|
|
@@ -1183,7 +1261,7 @@ function scheduleGeminiSessionRefresh() {
|
|
|
1183
1261
|
if (!scriptPath) return;
|
|
1184
1262
|
try {
|
|
1185
1263
|
const child = spawn(process.execPath, [scriptPath, GEMINI_SESSION_REFRESH_FLAG], {
|
|
1186
|
-
detached:
|
|
1264
|
+
detached: true,
|
|
1187
1265
|
stdio: "ignore",
|
|
1188
1266
|
windowsHide: true,
|
|
1189
1267
|
});
|
|
@@ -1265,13 +1343,14 @@ function getClaudeRows(stdin, claudeUsage, combinedSvPct) {
|
|
|
1265
1343
|
const svSuffix = `${dim("sv:")}${svStr}`;
|
|
1266
1344
|
|
|
1267
1345
|
// API 실측 데이터 사용 (없으면 플레이스홀더)
|
|
1268
|
-
|
|
1269
|
-
const
|
|
1346
|
+
// 캐시된 percent 그대로 사용 (시간 표시는 advanceToNextCycle이 처리)
|
|
1347
|
+
const fiveHourPercent = claudeUsage?.fiveHourPercent ?? null;
|
|
1348
|
+
const weeklyPercent = claudeUsage?.weeklyPercent ?? null;
|
|
1270
1349
|
const fiveHourReset = claudeUsage?.fiveHourResetsAt
|
|
1271
|
-
? formatResetRemaining(claudeUsage.fiveHourResetsAt)
|
|
1350
|
+
? formatResetRemaining(claudeUsage.fiveHourResetsAt, FIVE_HOUR_MS)
|
|
1272
1351
|
: "n/a";
|
|
1273
1352
|
const weeklyReset = claudeUsage?.weeklyResetsAt
|
|
1274
|
-
? formatResetRemainingDayHour(claudeUsage.weeklyResetsAt)
|
|
1353
|
+
? formatResetRemainingDayHour(claudeUsage.weeklyResetsAt, SEVEN_DAY_MS)
|
|
1275
1354
|
: "n/a";
|
|
1276
1355
|
|
|
1277
1356
|
const hasData = claudeUsage != null;
|
|
@@ -1285,14 +1364,23 @@ function getClaudeRows(stdin, claudeUsage, combinedSvPct) {
|
|
|
1285
1364
|
return [{ prefix, left: quotaSection, right: "" }];
|
|
1286
1365
|
}
|
|
1287
1366
|
if (cols < 40) {
|
|
1288
|
-
|
|
1289
|
-
|
|
1367
|
+
// null이면 '--' 플레이스홀더, 아니면 실제 값
|
|
1368
|
+
const fStr = fiveHourPercent != null ? `${colorByProvider(fiveHourPercent, `${fiveHourPercent}%`, claudeOrange)}` : dim("--");
|
|
1369
|
+
const wStr = weeklyPercent != null ? `${colorByProvider(weeklyPercent, `${weeklyPercent}%`, claudeOrange)}` : dim("--");
|
|
1370
|
+
const quotaSection = `${fStr}${dim("/")}${wStr} ` +
|
|
1290
1371
|
`${dim("ctx:")}${colorByPercent(contextPercent, `${contextPercent}%`)}`;
|
|
1291
1372
|
return [{ prefix, left: quotaSection, right: "" }];
|
|
1292
1373
|
}
|
|
1293
1374
|
// nano: c: 5h 12% 1w 95% sv: 191% ctx:90%
|
|
1294
|
-
|
|
1295
|
-
|
|
1375
|
+
// null이면 '--%' 플레이스홀더 표시
|
|
1376
|
+
const fCellNano = fiveHourPercent != null
|
|
1377
|
+
? colorByProvider(fiveHourPercent, formatPercentCell(fiveHourPercent), claudeOrange)
|
|
1378
|
+
: dim(formatPlaceholderPercentCell());
|
|
1379
|
+
const wCellNano = weeklyPercent != null
|
|
1380
|
+
? colorByProvider(weeklyPercent, formatPercentCell(weeklyPercent), claudeOrange)
|
|
1381
|
+
: dim(formatPlaceholderPercentCell());
|
|
1382
|
+
const quotaSection = `${dim("5h")} ${fCellNano} ` +
|
|
1383
|
+
`${dim("1w")} ${wCellNano} ` +
|
|
1296
1384
|
`${dim("sv:")}${svStr} ${dim("ctx:")}${colorByPercent(contextPercent, `${contextPercent}%`)}`;
|
|
1297
1385
|
return [{ prefix, left: quotaSection, right: "" }];
|
|
1298
1386
|
}
|
|
@@ -1305,8 +1393,15 @@ function getClaudeRows(stdin, claudeUsage, combinedSvPct) {
|
|
|
1305
1393
|
return [{ prefix, left: quotaSection, right: "" }];
|
|
1306
1394
|
}
|
|
1307
1395
|
// compact: c: 5h: 14% 1w: 96% | sv: 191% ctx:43%
|
|
1308
|
-
|
|
1309
|
-
|
|
1396
|
+
// null이면 '--%' 플레이스홀더 표시
|
|
1397
|
+
const fCellCmp = fiveHourPercent != null
|
|
1398
|
+
? colorByProvider(fiveHourPercent, formatPercentCell(fiveHourPercent), claudeOrange)
|
|
1399
|
+
: dim(formatPlaceholderPercentCell());
|
|
1400
|
+
const wCellCmp = weeklyPercent != null
|
|
1401
|
+
? colorByProvider(weeklyPercent, formatPercentCell(weeklyPercent), claudeOrange)
|
|
1402
|
+
: dim(formatPlaceholderPercentCell());
|
|
1403
|
+
const quotaSection = `${dim("5h:")}${fCellCmp} ` +
|
|
1404
|
+
`${dim("1w:")}${wCellCmp} ` +
|
|
1310
1405
|
`${dim("|")} ${svSuffix} ${dim("ctx:")}${colorByPercent(contextPercent, `${contextPercent}%`)}`;
|
|
1311
1406
|
return [{ prefix, left: quotaSection, right: "" }];
|
|
1312
1407
|
}
|
|
@@ -1321,13 +1416,20 @@ function getClaudeRows(stdin, claudeUsage, combinedSvPct) {
|
|
|
1321
1416
|
return [{ prefix, left: quotaSection, right: contextSection }];
|
|
1322
1417
|
}
|
|
1323
1418
|
|
|
1324
|
-
|
|
1325
|
-
const
|
|
1419
|
+
// null이면 dim 바 + '--%' 플레이스홀더, 아니면 실제 값 표시
|
|
1420
|
+
const fiveHourPercentCell = fiveHourPercent != null
|
|
1421
|
+
? colorByProvider(fiveHourPercent, formatPercentCell(fiveHourPercent), claudeOrange)
|
|
1422
|
+
: dim(formatPlaceholderPercentCell());
|
|
1423
|
+
const weeklyPercentCell = weeklyPercent != null
|
|
1424
|
+
? colorByProvider(weeklyPercent, formatPercentCell(weeklyPercent), claudeOrange)
|
|
1425
|
+
: dim(formatPlaceholderPercentCell());
|
|
1426
|
+
const fiveHourBar = fiveHourPercent != null ? tierBar(fiveHourPercent, CLAUDE_ORANGE) : tierDimBar();
|
|
1427
|
+
const weeklyBar = weeklyPercent != null ? tierBar(weeklyPercent, CLAUDE_ORANGE) : tierDimBar();
|
|
1326
1428
|
const fiveHourTimeCell = formatTimeCell(fiveHourReset);
|
|
1327
1429
|
const weeklyTimeCell = formatTimeCellDH(weeklyReset);
|
|
1328
|
-
const quotaSection = `${dim("5h:")}${
|
|
1430
|
+
const quotaSection = `${dim("5h:")}${fiveHourBar}${fiveHourPercentCell} ` +
|
|
1329
1431
|
`${dim(fiveHourTimeCell)} ` +
|
|
1330
|
-
`${dim("1w:")}${
|
|
1432
|
+
`${dim("1w:")}${weeklyBar}${weeklyPercentCell} ` +
|
|
1331
1433
|
`${dim(weeklyTimeCell)}`;
|
|
1332
1434
|
const contextSection = `${svSuffix} ${dim("|")} ${dim("ctx:")}${colorByPercent(contextPercent, `${contextPercent}%`)}`;
|
|
1333
1435
|
return [{ prefix, left: quotaSection, right: contextSection }];
|
|
@@ -1368,12 +1470,15 @@ function getProviderRow(provider, marker, markerColor, qosProfile, accountsConfi
|
|
|
1368
1470
|
if (realQuota?.type === "codex") {
|
|
1369
1471
|
const main = realQuota.buckets.codex || realQuota.buckets[Object.keys(realQuota.buckets)[0]];
|
|
1370
1472
|
if (main) {
|
|
1371
|
-
|
|
1372
|
-
const
|
|
1473
|
+
// 캐시된 값 그대로 표시 (시간은 advanceToNextCycle이 처리)
|
|
1474
|
+
const fiveP = main.primary?.used_percent != null ? clampPercent(main.primary.used_percent) : null;
|
|
1475
|
+
const weekP = main.secondary?.used_percent != null ? clampPercent(main.secondary.used_percent) : null;
|
|
1476
|
+
const fCellN = fiveP != null ? colorByProvider(fiveP, formatPercentCell(fiveP), provFn) : dim(formatPlaceholderPercentCell());
|
|
1477
|
+
const wCellN = weekP != null ? colorByProvider(weekP, formatPercentCell(weekP), provFn) : dim(formatPlaceholderPercentCell());
|
|
1373
1478
|
if (cols < 40) {
|
|
1374
|
-
return { prefix: minPrefix, left: `${
|
|
1479
|
+
return { prefix: minPrefix, left: `${fCellN}${dim("/")}${wCellN}${svCompact}`, right: "" };
|
|
1375
1480
|
}
|
|
1376
|
-
return { prefix: minPrefix, left: `${dim("5h")} ${
|
|
1481
|
+
return { prefix: minPrefix, left: `${dim("5h")} ${fCellN} ${dim("1w")} ${wCellN}${svCompact}`, right: "" };
|
|
1377
1482
|
}
|
|
1378
1483
|
}
|
|
1379
1484
|
if (realQuota?.type === "gemini") {
|
|
@@ -1393,10 +1498,13 @@ function getProviderRow(provider, marker, markerColor, qosProfile, accountsConfi
|
|
|
1393
1498
|
if (realQuota?.type === "codex") {
|
|
1394
1499
|
const main = realQuota.buckets.codex || realQuota.buckets[Object.keys(realQuota.buckets)[0]];
|
|
1395
1500
|
if (main) {
|
|
1396
|
-
|
|
1397
|
-
const
|
|
1398
|
-
|
|
1399
|
-
|
|
1501
|
+
// 캐시된 값 그대로 표시 (시간은 advanceToNextCycle이 처리)
|
|
1502
|
+
const fiveP = main.primary?.used_percent != null ? clampPercent(main.primary.used_percent) : null;
|
|
1503
|
+
const weekP = main.secondary?.used_percent != null ? clampPercent(main.secondary.used_percent) : null;
|
|
1504
|
+
const fCell = fiveP != null ? colorByProvider(fiveP, formatPercentCell(fiveP), provFn) : dim(formatPlaceholderPercentCell());
|
|
1505
|
+
const wCell = weekP != null ? colorByProvider(weekP, formatPercentCell(weekP), provFn) : dim(formatPlaceholderPercentCell());
|
|
1506
|
+
quotaSection = `${dim("5h:")}${fCell} ` +
|
|
1507
|
+
`${dim("1w:")}${wCell}`;
|
|
1400
1508
|
}
|
|
1401
1509
|
}
|
|
1402
1510
|
if (realQuota?.type === "gemini") {
|
|
@@ -1409,7 +1517,7 @@ function getProviderRow(provider, marker, markerColor, qosProfile, accountsConfi
|
|
|
1409
1517
|
}
|
|
1410
1518
|
}
|
|
1411
1519
|
if (!quotaSection) {
|
|
1412
|
-
quotaSection = `${dim("5h:")}${
|
|
1520
|
+
quotaSection = `${dim("5h:")}${dim(formatPlaceholderPercentCell())} ${dim("1w:")}${dim(formatPlaceholderPercentCell())}`;
|
|
1413
1521
|
}
|
|
1414
1522
|
const prefix = `${bold(markerColor(`${marker}`))}:`;
|
|
1415
1523
|
// compact: sv + 계정 (모델 라벨 제거)
|
|
@@ -1420,13 +1528,18 @@ function getProviderRow(provider, marker, markerColor, qosProfile, accountsConfi
|
|
|
1420
1528
|
if (realQuota?.type === "codex") {
|
|
1421
1529
|
const main = realQuota.buckets.codex || realQuota.buckets[Object.keys(realQuota.buckets)[0]];
|
|
1422
1530
|
if (main) {
|
|
1423
|
-
|
|
1424
|
-
const
|
|
1425
|
-
const
|
|
1426
|
-
const
|
|
1427
|
-
|
|
1531
|
+
// 캐시된 값 그대로 표시 (시간은 advanceToNextCycle이 처리)
|
|
1532
|
+
const fiveP = main.primary?.used_percent != null ? clampPercent(main.primary.used_percent) : null;
|
|
1533
|
+
const weekP = main.secondary?.used_percent != null ? clampPercent(main.secondary.used_percent) : null;
|
|
1534
|
+
const fiveReset = formatResetRemaining(main.primary?.resets_at, FIVE_HOUR_MS) || "n/a";
|
|
1535
|
+
const weekReset = formatResetRemainingDayHour(main.secondary?.resets_at, SEVEN_DAY_MS) || "n/a";
|
|
1536
|
+
const fCell = fiveP != null ? colorByProvider(fiveP, formatPercentCell(fiveP), provFn) : dim(formatPlaceholderPercentCell());
|
|
1537
|
+
const wCell = weekP != null ? colorByProvider(weekP, formatPercentCell(weekP), provFn) : dim(formatPlaceholderPercentCell());
|
|
1538
|
+
const fBar = fiveP != null ? tierBar(fiveP, provAnsi) : tierDimBar();
|
|
1539
|
+
const wBar = weekP != null ? tierBar(weekP, provAnsi) : tierDimBar();
|
|
1540
|
+
quotaSection = `${dim("5h:")}${fBar}${fCell} ` +
|
|
1428
1541
|
`${dim(formatTimeCell(fiveReset))} ` +
|
|
1429
|
-
`${dim("1w:")}${
|
|
1542
|
+
`${dim("1w:")}${wBar}${wCell} ` +
|
|
1430
1543
|
`${dim(formatTimeCellDH(weekReset))}`;
|
|
1431
1544
|
}
|
|
1432
1545
|
}
|
|
@@ -1435,7 +1548,7 @@ function getProviderRow(provider, marker, markerColor, qosProfile, accountsConfi
|
|
|
1435
1548
|
const bucket = realQuota.quotaBucket;
|
|
1436
1549
|
if (bucket) {
|
|
1437
1550
|
const usedP = clampPercent((1 - (bucket.remainingFraction ?? 1)) * 100);
|
|
1438
|
-
const rstRemaining = formatResetRemaining(bucket.resetTime) || "n/a";
|
|
1551
|
+
const rstRemaining = formatResetRemaining(bucket.resetTime, ONE_DAY_MS) || "n/a";
|
|
1439
1552
|
quotaSection = `${dim("1d:")}${tierBar(usedP, provAnsi)}${colorByProvider(usedP, formatPercentCell(usedP), provFn)} ${dim(formatTimeCell(rstRemaining))} ` +
|
|
1440
1553
|
`${dim("1w:")}${tierInfBar()}${dim("\u221E%".padStart(PERCENT_CELL_WIDTH))} ${dim(formatTimeCellDH("-d--h"))}`;
|
|
1441
1554
|
} else {
|