triflux 2.4.6 → 2.5.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/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 +21 -1
- package/hud/hud-qos-status.mjs +208 -101
- package/package.json +1 -1
- package/scripts/cli-route.sh +236 -9
- package/scripts/notion-read.mjs +553 -0
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,21 +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 ? clampPercent(claudeUsage.fiveHourPercent
|
|
442
|
-
const cW = claudeUsage ? clampPercent(claudeUsage.weeklyPercent
|
|
443
|
-
const cVal =
|
|
444
|
-
? `${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("--")}`
|
|
445
456
|
: dim("--/--");
|
|
446
457
|
|
|
447
|
-
// Codex 5h/1w
|
|
458
|
+
// Codex 5h/1w (캐시된 값 그대로 표시)
|
|
448
459
|
let xVal = dim("--/--");
|
|
449
460
|
if (codexBuckets) {
|
|
450
461
|
const mb = codexBuckets.codex || codexBuckets[Object.keys(codexBuckets)[0]];
|
|
451
462
|
if (mb) {
|
|
452
|
-
const xF =
|
|
453
|
-
const xW =
|
|
454
|
-
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("--")}`;
|
|
455
466
|
}
|
|
456
467
|
}
|
|
457
468
|
|
|
@@ -663,31 +674,15 @@ function parseClaudeUsageResponse(response) {
|
|
|
663
674
|
// stale 캐시의 과거 resetsAt → 다음 주기로 순환 추정 (null 대신 다음 reset 시간 계산)
|
|
664
675
|
function stripStaleResets(data) {
|
|
665
676
|
if (!data) return data;
|
|
666
|
-
const now = Date.now();
|
|
667
677
|
const copy = { ...data };
|
|
668
|
-
|
|
669
|
-
// 5시간 주기: 과거 reset → 5시간씩 전진하여 미래 시점 추정
|
|
670
678
|
if (copy.fiveHourResetsAt) {
|
|
671
|
-
|
|
672
|
-
if (t
|
|
673
|
-
const cycle = 5 * 60 * 60 * 1000;
|
|
674
|
-
const elapsed = now - t;
|
|
675
|
-
t += Math.ceil(elapsed / cycle) * cycle;
|
|
676
|
-
copy.fiveHourResetsAt = new Date(t).toISOString();
|
|
677
|
-
}
|
|
679
|
+
const t = new Date(copy.fiveHourResetsAt).getTime();
|
|
680
|
+
if (!isNaN(t)) copy.fiveHourResetsAt = new Date(advanceToNextCycle(t, FIVE_HOUR_MS)).toISOString();
|
|
678
681
|
}
|
|
679
|
-
|
|
680
|
-
// 7일 주기: 과거 reset → 7일씩 전진하여 미래 시점 추정
|
|
681
682
|
if (copy.weeklyResetsAt) {
|
|
682
|
-
|
|
683
|
-
if (t
|
|
684
|
-
const cycle = 7 * 24 * 60 * 60 * 1000;
|
|
685
|
-
const elapsed = now - t;
|
|
686
|
-
t += Math.ceil(elapsed / cycle) * cycle;
|
|
687
|
-
copy.weeklyResetsAt = new Date(t).toISOString();
|
|
688
|
-
}
|
|
683
|
+
const t = new Date(copy.weeklyResetsAt).getTime();
|
|
684
|
+
if (!isNaN(t)) copy.weeklyResetsAt = new Date(advanceToNextCycle(t, SEVEN_DAY_MS)).toISOString();
|
|
689
685
|
}
|
|
690
|
-
|
|
691
686
|
return copy;
|
|
692
687
|
}
|
|
693
688
|
|
|
@@ -813,12 +808,15 @@ function scheduleClaudeUsageRefresh() {
|
|
|
813
808
|
|
|
814
809
|
try {
|
|
815
810
|
const child = spawn(process.execPath, [scriptPath, CLAUDE_REFRESH_FLAG], {
|
|
816
|
-
detached:
|
|
811
|
+
detached: true,
|
|
817
812
|
stdio: "ignore",
|
|
818
813
|
windowsHide: true,
|
|
819
814
|
});
|
|
820
815
|
child.unref();
|
|
821
|
-
} catch {
|
|
816
|
+
} catch (spawnErr) {
|
|
817
|
+
// spawn 실패 시 에러 유형을 캐시에 기록 (HUD에서 원인 힌트 표시 가능)
|
|
818
|
+
writeClaudeUsageCache(null, { type: "network", status: 0, hint: String(spawnErr?.message || spawnErr) });
|
|
819
|
+
}
|
|
822
820
|
}
|
|
823
821
|
|
|
824
822
|
function getContextPercent(stdin) {
|
|
@@ -833,12 +831,21 @@ function getContextPercent(stdin) {
|
|
|
833
831
|
return clampPercent((totalTokens / capacity) * 100);
|
|
834
832
|
}
|
|
835
833
|
|
|
836
|
-
|
|
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) {
|
|
837
843
|
if (!isoOrUnix) return "";
|
|
838
844
|
const d = typeof isoOrUnix === "string" ? new Date(isoOrUnix) : new Date(isoOrUnix * 1000);
|
|
839
845
|
if (isNaN(d.getTime())) return "";
|
|
840
|
-
const
|
|
841
|
-
|
|
846
|
+
const targetMs = advanceToNextCycle(d.getTime(), cycleMs);
|
|
847
|
+
const diffMs = targetMs - Date.now();
|
|
848
|
+
if (diffMs <= 0) return "";
|
|
842
849
|
const totalMinutes = Math.floor(diffMs / 60000);
|
|
843
850
|
const totalHours = Math.floor(totalMinutes / 60);
|
|
844
851
|
const minutes = totalMinutes % 60;
|
|
@@ -851,12 +858,13 @@ function isResetPast(isoOrUnix) {
|
|
|
851
858
|
return !isNaN(d.getTime()) && d.getTime() <= Date.now();
|
|
852
859
|
}
|
|
853
860
|
|
|
854
|
-
function formatResetRemainingDayHour(isoOrUnix) {
|
|
861
|
+
function formatResetRemainingDayHour(isoOrUnix, cycleMs = 0) {
|
|
855
862
|
if (!isoOrUnix) return "";
|
|
856
863
|
const d = typeof isoOrUnix === "string" ? new Date(isoOrUnix) : new Date(isoOrUnix * 1000);
|
|
857
864
|
if (isNaN(d.getTime())) return "";
|
|
858
|
-
const
|
|
859
|
-
|
|
865
|
+
const targetMs = advanceToNextCycle(d.getTime(), cycleMs);
|
|
866
|
+
const diffMs = targetMs - Date.now();
|
|
867
|
+
if (diffMs <= 0) return "";
|
|
860
868
|
const totalMinutes = Math.floor(diffMs / 60000);
|
|
861
869
|
const days = Math.floor(totalMinutes / (60 * 24));
|
|
862
870
|
const hours = Math.floor((totalMinutes % (60 * 24)) / 60);
|
|
@@ -902,20 +910,33 @@ function httpsPost(url, body, accessToken) {
|
|
|
902
910
|
});
|
|
903
911
|
}
|
|
904
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
|
+
|
|
905
933
|
// ============================================================================
|
|
906
934
|
// Codex JWT에서 이메일 추출
|
|
907
935
|
// ============================================================================
|
|
908
936
|
function getCodexEmail() {
|
|
909
937
|
try {
|
|
910
938
|
const auth = JSON.parse(readFileSync(CODEX_AUTH_PATH, "utf-8"));
|
|
911
|
-
|
|
912
|
-
if (!idToken) return null;
|
|
913
|
-
const parts = idToken.split(".");
|
|
914
|
-
if (parts.length < 2) return null;
|
|
915
|
-
let payload = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
916
|
-
while (payload.length % 4) payload += "=";
|
|
917
|
-
const decoded = JSON.parse(Buffer.from(payload, "base64").toString("utf-8"));
|
|
918
|
-
return decoded.email || null;
|
|
939
|
+
return decodeJwtEmail(auth?.tokens?.id_token);
|
|
919
940
|
} catch { return null; }
|
|
920
941
|
}
|
|
921
942
|
|
|
@@ -925,22 +946,19 @@ function getCodexEmail() {
|
|
|
925
946
|
function getGeminiEmail() {
|
|
926
947
|
try {
|
|
927
948
|
const oauth = readJson(GEMINI_OAUTH_PATH, null);
|
|
928
|
-
|
|
929
|
-
if (!idToken) return null;
|
|
930
|
-
const parts = idToken.split(".");
|
|
931
|
-
if (parts.length < 2) return null;
|
|
932
|
-
let payload = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
933
|
-
while (payload.length % 4) payload += "=";
|
|
934
|
-
const decoded = JSON.parse(Buffer.from(payload, "base64").toString("utf-8"));
|
|
935
|
-
return decoded.email || null;
|
|
949
|
+
return decodeJwtEmail(oauth?.id_token);
|
|
936
950
|
} catch { return null; }
|
|
937
951
|
}
|
|
938
952
|
|
|
939
953
|
// ============================================================================
|
|
940
954
|
// Codex 세션 JSONL에서 실제 rate limits 추출
|
|
955
|
+
// 한계: rate_limits는 세션별 스냅샷이므로 여러 세션 간 토큰 합산은 불가.
|
|
956
|
+
// 오늘 날짜의 모든 세션 파일을 스캔해 가장 최신 rate_limits 버킷을 수집한다.
|
|
957
|
+
// (단일 파일 즉시 return 방식에서 → 당일 전체 스캔 후 최신 데이터 우선 병합으로 변경)
|
|
941
958
|
// ============================================================================
|
|
942
959
|
function getCodexRateLimits() {
|
|
943
960
|
const now = new Date();
|
|
961
|
+
let todayHasFiles = false;
|
|
944
962
|
for (let dayOffset = 0; dayOffset <= 1; dayOffset++) {
|
|
945
963
|
const d = new Date(now.getTime() - dayOffset * 86_400_000);
|
|
946
964
|
const sessDir = join(
|
|
@@ -953,17 +971,22 @@ function getCodexRateLimits() {
|
|
|
953
971
|
let files;
|
|
954
972
|
try { files = readdirSync(sessDir).filter((f) => f.endsWith(".jsonl")).sort().reverse(); }
|
|
955
973
|
catch { continue; }
|
|
974
|
+
if (dayOffset === 0 && files.length > 0) todayHasFiles = true;
|
|
975
|
+
|
|
976
|
+
// 당일 모든 세션 파일을 스캔해 limit_id별 가장 최신 버킷을 병합
|
|
977
|
+
// (파일 목록은 이름 역순 정렬 → 최신 세션 우선)
|
|
978
|
+
const mergedBuckets = {};
|
|
956
979
|
for (const file of files) {
|
|
957
980
|
try {
|
|
958
981
|
const content = readFileSync(join(sessDir, file), "utf-8");
|
|
959
982
|
const lines = content.trim().split("\n").reverse();
|
|
960
|
-
const buckets = {};
|
|
961
983
|
for (const line of lines) {
|
|
962
984
|
try {
|
|
963
985
|
const evt = JSON.parse(line);
|
|
964
986
|
const rl = evt?.payload?.rate_limits;
|
|
965
|
-
if (rl?.limit_id && !
|
|
966
|
-
|
|
987
|
+
if (rl?.limit_id && !mergedBuckets[rl.limit_id]) {
|
|
988
|
+
// limit_id별로 첫 발견(=해당 세션의 가장 최신 이벤트)만 기록
|
|
989
|
+
mergedBuckets[rl.limit_id] = {
|
|
967
990
|
limitId: rl.limit_id, limitName: rl.limit_name,
|
|
968
991
|
primary: rl.primary, secondary: rl.secondary,
|
|
969
992
|
credits: rl.credits,
|
|
@@ -973,11 +996,14 @@ function getCodexRateLimits() {
|
|
|
973
996
|
};
|
|
974
997
|
}
|
|
975
998
|
} catch { /* 라인 파싱 실패 무시 */ }
|
|
976
|
-
if (Object.keys(
|
|
999
|
+
if (Object.keys(mergedBuckets).length >= CODEX_MIN_BUCKETS) break;
|
|
977
1000
|
}
|
|
978
|
-
if (Object.keys(buckets).length > 0) return buckets;
|
|
979
1001
|
} catch { /* 파일 읽기 실패 무시 */ }
|
|
980
1002
|
}
|
|
1003
|
+
if (Object.keys(mergedBuckets).length > 0) return mergedBuckets;
|
|
1004
|
+
|
|
1005
|
+
// 오늘 세션 파일이 존재하지만 rate_limits가 없으면 어제 stale 데이터로 폴백하지 않음
|
|
1006
|
+
if (todayHasFiles && dayOffset === 0) return null;
|
|
981
1007
|
}
|
|
982
1008
|
return null;
|
|
983
1009
|
}
|
|
@@ -999,8 +1025,28 @@ async function fetchGeminiQuota(accountId, options = {}) {
|
|
|
999
1025
|
return cache;
|
|
1000
1026
|
}
|
|
1001
1027
|
|
|
1002
|
-
if (!oauth?.access_token)
|
|
1003
|
-
|
|
1028
|
+
if (!oauth?.access_token) {
|
|
1029
|
+
// access_token 없음: 에러 힌트를 캐시에 기록하고 stale 캐시 반환
|
|
1030
|
+
writeJsonSafe(GEMINI_QUOTA_CACHE_PATH, {
|
|
1031
|
+
...(cache || {}),
|
|
1032
|
+
timestamp: cache?.timestamp || Date.now(),
|
|
1033
|
+
error: true,
|
|
1034
|
+
errorType: "auth",
|
|
1035
|
+
errorHint: "no access_token in oauth_creds.json",
|
|
1036
|
+
});
|
|
1037
|
+
return cache;
|
|
1038
|
+
}
|
|
1039
|
+
if (oauth.expiry_date && oauth.expiry_date < Date.now()) {
|
|
1040
|
+
// OAuth 토큰 만료: 에러 힌트를 캐시에 기록 (refresh_token 갱신은 Gemini CLI 담당)
|
|
1041
|
+
writeJsonSafe(GEMINI_QUOTA_CACHE_PATH, {
|
|
1042
|
+
...(cache || {}),
|
|
1043
|
+
timestamp: cache?.timestamp || Date.now(),
|
|
1044
|
+
error: true,
|
|
1045
|
+
errorType: "auth",
|
|
1046
|
+
errorHint: `token expired at ${new Date(oauth.expiry_date).toISOString()}`,
|
|
1047
|
+
});
|
|
1048
|
+
return cache;
|
|
1049
|
+
}
|
|
1004
1050
|
|
|
1005
1051
|
// 3. projectId (캐시 or API)
|
|
1006
1052
|
const fetchProjectId = async () => {
|
|
@@ -1037,7 +1083,18 @@ async function fetchGeminiQuota(accountId, options = {}) {
|
|
|
1037
1083
|
);
|
|
1038
1084
|
}
|
|
1039
1085
|
|
|
1040
|
-
if (!quotaRes?.buckets)
|
|
1086
|
+
if (!quotaRes?.buckets) {
|
|
1087
|
+
// API 응답에 buckets 없음: 에러 코드 또는 응답 내용을 캐시에 기록
|
|
1088
|
+
const apiError = quotaRes?.error?.message || quotaRes?.error?.code || quotaRes?.error || "no buckets in response";
|
|
1089
|
+
writeJsonSafe(GEMINI_QUOTA_CACHE_PATH, {
|
|
1090
|
+
...(cache || {}),
|
|
1091
|
+
timestamp: cache?.timestamp || Date.now(),
|
|
1092
|
+
error: true,
|
|
1093
|
+
errorType: "api",
|
|
1094
|
+
errorHint: String(apiError),
|
|
1095
|
+
});
|
|
1096
|
+
return cache;
|
|
1097
|
+
}
|
|
1041
1098
|
|
|
1042
1099
|
// 5. 캐시 저장
|
|
1043
1100
|
const result = {
|
|
@@ -1112,13 +1169,20 @@ function scheduleGeminiQuotaRefresh(accountId) {
|
|
|
1112
1169
|
process.execPath,
|
|
1113
1170
|
[scriptPath, GEMINI_REFRESH_FLAG, "--account", accountId || "gemini-main"],
|
|
1114
1171
|
{
|
|
1115
|
-
detached:
|
|
1172
|
+
detached: true,
|
|
1116
1173
|
stdio: "ignore",
|
|
1117
1174
|
windowsHide: true,
|
|
1118
1175
|
},
|
|
1119
1176
|
);
|
|
1120
1177
|
child.unref();
|
|
1121
|
-
} catch {
|
|
1178
|
+
} catch (spawnErr) {
|
|
1179
|
+
// spawn 실패 시 캐시에 에러 힌트 기록 (다음 HUD 렌더에서 원인 확인 가능)
|
|
1180
|
+
writeJsonSafe(GEMINI_QUOTA_CACHE_PATH, {
|
|
1181
|
+
timestamp: Date.now(),
|
|
1182
|
+
error: true,
|
|
1183
|
+
errorHint: String(spawnErr?.message || spawnErr),
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1122
1186
|
}
|
|
1123
1187
|
|
|
1124
1188
|
function readCodexRateLimitSnapshot() {
|
|
@@ -1134,7 +1198,7 @@ function readCodexRateLimitSnapshot() {
|
|
|
1134
1198
|
|
|
1135
1199
|
function refreshCodexRateLimitsCache() {
|
|
1136
1200
|
const buckets = getCodexRateLimits();
|
|
1137
|
-
|
|
1201
|
+
// buckets가 null이어도 캐시 갱신 (stale 데이터 제거)
|
|
1138
1202
|
writeJsonSafe(CODEX_QUOTA_CACHE_PATH, { timestamp: Date.now(), buckets });
|
|
1139
1203
|
return buckets;
|
|
1140
1204
|
}
|
|
@@ -1144,12 +1208,20 @@ function scheduleCodexRateLimitRefresh() {
|
|
|
1144
1208
|
if (!scriptPath) return;
|
|
1145
1209
|
try {
|
|
1146
1210
|
const child = spawn(process.execPath, [scriptPath, CODEX_REFRESH_FLAG], {
|
|
1147
|
-
detached:
|
|
1211
|
+
detached: true,
|
|
1148
1212
|
stdio: "ignore",
|
|
1149
1213
|
windowsHide: true,
|
|
1150
1214
|
});
|
|
1151
1215
|
child.unref();
|
|
1152
|
-
} catch {
|
|
1216
|
+
} catch (spawnErr) {
|
|
1217
|
+
// spawn 실패 시 캐시에 에러 힌트 기록
|
|
1218
|
+
writeJsonSafe(CODEX_QUOTA_CACHE_PATH, {
|
|
1219
|
+
timestamp: Date.now(),
|
|
1220
|
+
buckets: null,
|
|
1221
|
+
error: true,
|
|
1222
|
+
errorHint: String(spawnErr?.message || spawnErr),
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1153
1225
|
}
|
|
1154
1226
|
|
|
1155
1227
|
function readGeminiSessionSnapshot() {
|
|
@@ -1175,7 +1247,7 @@ function scheduleGeminiSessionRefresh() {
|
|
|
1175
1247
|
if (!scriptPath) return;
|
|
1176
1248
|
try {
|
|
1177
1249
|
const child = spawn(process.execPath, [scriptPath, GEMINI_SESSION_REFRESH_FLAG], {
|
|
1178
|
-
detached:
|
|
1250
|
+
detached: true,
|
|
1179
1251
|
stdio: "ignore",
|
|
1180
1252
|
windowsHide: true,
|
|
1181
1253
|
});
|
|
@@ -1257,13 +1329,14 @@ function getClaudeRows(stdin, claudeUsage, combinedSvPct) {
|
|
|
1257
1329
|
const svSuffix = `${dim("sv:")}${svStr}`;
|
|
1258
1330
|
|
|
1259
1331
|
// API 실측 데이터 사용 (없으면 플레이스홀더)
|
|
1260
|
-
|
|
1261
|
-
const
|
|
1332
|
+
// 캐시된 percent 그대로 사용 (시간 표시는 advanceToNextCycle이 처리)
|
|
1333
|
+
const fiveHourPercent = claudeUsage?.fiveHourPercent ?? null;
|
|
1334
|
+
const weeklyPercent = claudeUsage?.weeklyPercent ?? null;
|
|
1262
1335
|
const fiveHourReset = claudeUsage?.fiveHourResetsAt
|
|
1263
|
-
? formatResetRemaining(claudeUsage.fiveHourResetsAt)
|
|
1336
|
+
? formatResetRemaining(claudeUsage.fiveHourResetsAt, FIVE_HOUR_MS)
|
|
1264
1337
|
: "n/a";
|
|
1265
1338
|
const weeklyReset = claudeUsage?.weeklyResetsAt
|
|
1266
|
-
? formatResetRemainingDayHour(claudeUsage.weeklyResetsAt)
|
|
1339
|
+
? formatResetRemainingDayHour(claudeUsage.weeklyResetsAt, SEVEN_DAY_MS)
|
|
1267
1340
|
: "n/a";
|
|
1268
1341
|
|
|
1269
1342
|
const hasData = claudeUsage != null;
|
|
@@ -1277,14 +1350,23 @@ function getClaudeRows(stdin, claudeUsage, combinedSvPct) {
|
|
|
1277
1350
|
return [{ prefix, left: quotaSection, right: "" }];
|
|
1278
1351
|
}
|
|
1279
1352
|
if (cols < 40) {
|
|
1280
|
-
|
|
1281
|
-
|
|
1353
|
+
// null이면 '--' 플레이스홀더, 아니면 실제 값
|
|
1354
|
+
const fStr = fiveHourPercent != null ? `${colorByProvider(fiveHourPercent, `${fiveHourPercent}%`, claudeOrange)}` : dim("--");
|
|
1355
|
+
const wStr = weeklyPercent != null ? `${colorByProvider(weeklyPercent, `${weeklyPercent}%`, claudeOrange)}` : dim("--");
|
|
1356
|
+
const quotaSection = `${fStr}${dim("/")}${wStr} ` +
|
|
1282
1357
|
`${dim("ctx:")}${colorByPercent(contextPercent, `${contextPercent}%`)}`;
|
|
1283
1358
|
return [{ prefix, left: quotaSection, right: "" }];
|
|
1284
1359
|
}
|
|
1285
1360
|
// nano: c: 5h 12% 1w 95% sv: 191% ctx:90%
|
|
1286
|
-
|
|
1287
|
-
|
|
1361
|
+
// null이면 '--%' 플레이스홀더 표시
|
|
1362
|
+
const fCellNano = fiveHourPercent != null
|
|
1363
|
+
? colorByProvider(fiveHourPercent, formatPercentCell(fiveHourPercent), claudeOrange)
|
|
1364
|
+
: dim(formatPlaceholderPercentCell());
|
|
1365
|
+
const wCellNano = weeklyPercent != null
|
|
1366
|
+
? colorByProvider(weeklyPercent, formatPercentCell(weeklyPercent), claudeOrange)
|
|
1367
|
+
: dim(formatPlaceholderPercentCell());
|
|
1368
|
+
const quotaSection = `${dim("5h")} ${fCellNano} ` +
|
|
1369
|
+
`${dim("1w")} ${wCellNano} ` +
|
|
1288
1370
|
`${dim("sv:")}${svStr} ${dim("ctx:")}${colorByPercent(contextPercent, `${contextPercent}%`)}`;
|
|
1289
1371
|
return [{ prefix, left: quotaSection, right: "" }];
|
|
1290
1372
|
}
|
|
@@ -1297,8 +1379,15 @@ function getClaudeRows(stdin, claudeUsage, combinedSvPct) {
|
|
|
1297
1379
|
return [{ prefix, left: quotaSection, right: "" }];
|
|
1298
1380
|
}
|
|
1299
1381
|
// compact: c: 5h: 14% 1w: 96% | sv: 191% ctx:43%
|
|
1300
|
-
|
|
1301
|
-
|
|
1382
|
+
// null이면 '--%' 플레이스홀더 표시
|
|
1383
|
+
const fCellCmp = fiveHourPercent != null
|
|
1384
|
+
? colorByProvider(fiveHourPercent, formatPercentCell(fiveHourPercent), claudeOrange)
|
|
1385
|
+
: dim(formatPlaceholderPercentCell());
|
|
1386
|
+
const wCellCmp = weeklyPercent != null
|
|
1387
|
+
? colorByProvider(weeklyPercent, formatPercentCell(weeklyPercent), claudeOrange)
|
|
1388
|
+
: dim(formatPlaceholderPercentCell());
|
|
1389
|
+
const quotaSection = `${dim("5h:")}${fCellCmp} ` +
|
|
1390
|
+
`${dim("1w:")}${wCellCmp} ` +
|
|
1302
1391
|
`${dim("|")} ${svSuffix} ${dim("ctx:")}${colorByPercent(contextPercent, `${contextPercent}%`)}`;
|
|
1303
1392
|
return [{ prefix, left: quotaSection, right: "" }];
|
|
1304
1393
|
}
|
|
@@ -1313,13 +1402,20 @@ function getClaudeRows(stdin, claudeUsage, combinedSvPct) {
|
|
|
1313
1402
|
return [{ prefix, left: quotaSection, right: contextSection }];
|
|
1314
1403
|
}
|
|
1315
1404
|
|
|
1316
|
-
|
|
1317
|
-
const
|
|
1405
|
+
// null이면 dim 바 + '--%' 플레이스홀더, 아니면 실제 값 표시
|
|
1406
|
+
const fiveHourPercentCell = fiveHourPercent != null
|
|
1407
|
+
? colorByProvider(fiveHourPercent, formatPercentCell(fiveHourPercent), claudeOrange)
|
|
1408
|
+
: dim(formatPlaceholderPercentCell());
|
|
1409
|
+
const weeklyPercentCell = weeklyPercent != null
|
|
1410
|
+
? colorByProvider(weeklyPercent, formatPercentCell(weeklyPercent), claudeOrange)
|
|
1411
|
+
: dim(formatPlaceholderPercentCell());
|
|
1412
|
+
const fiveHourBar = fiveHourPercent != null ? tierBar(fiveHourPercent, CLAUDE_ORANGE) : tierDimBar();
|
|
1413
|
+
const weeklyBar = weeklyPercent != null ? tierBar(weeklyPercent, CLAUDE_ORANGE) : tierDimBar();
|
|
1318
1414
|
const fiveHourTimeCell = formatTimeCell(fiveHourReset);
|
|
1319
1415
|
const weeklyTimeCell = formatTimeCellDH(weeklyReset);
|
|
1320
|
-
const quotaSection = `${dim("5h:")}${
|
|
1416
|
+
const quotaSection = `${dim("5h:")}${fiveHourBar}${fiveHourPercentCell} ` +
|
|
1321
1417
|
`${dim(fiveHourTimeCell)} ` +
|
|
1322
|
-
`${dim("1w:")}${
|
|
1418
|
+
`${dim("1w:")}${weeklyBar}${weeklyPercentCell} ` +
|
|
1323
1419
|
`${dim(weeklyTimeCell)}`;
|
|
1324
1420
|
const contextSection = `${svSuffix} ${dim("|")} ${dim("ctx:")}${colorByPercent(contextPercent, `${contextPercent}%`)}`;
|
|
1325
1421
|
return [{ prefix, left: quotaSection, right: contextSection }];
|
|
@@ -1360,12 +1456,15 @@ function getProviderRow(provider, marker, markerColor, qosProfile, accountsConfi
|
|
|
1360
1456
|
if (realQuota?.type === "codex") {
|
|
1361
1457
|
const main = realQuota.buckets.codex || realQuota.buckets[Object.keys(realQuota.buckets)[0]];
|
|
1362
1458
|
if (main) {
|
|
1363
|
-
|
|
1364
|
-
const
|
|
1459
|
+
// 캐시된 값 그대로 표시 (시간은 advanceToNextCycle이 처리)
|
|
1460
|
+
const fiveP = main.primary?.used_percent != null ? clampPercent(main.primary.used_percent) : null;
|
|
1461
|
+
const weekP = main.secondary?.used_percent != null ? clampPercent(main.secondary.used_percent) : null;
|
|
1462
|
+
const fCellN = fiveP != null ? colorByProvider(fiveP, formatPercentCell(fiveP), provFn) : dim(formatPlaceholderPercentCell());
|
|
1463
|
+
const wCellN = weekP != null ? colorByProvider(weekP, formatPercentCell(weekP), provFn) : dim(formatPlaceholderPercentCell());
|
|
1365
1464
|
if (cols < 40) {
|
|
1366
|
-
return { prefix: minPrefix, left: `${
|
|
1465
|
+
return { prefix: minPrefix, left: `${fCellN}${dim("/")}${wCellN}${svCompact}`, right: "" };
|
|
1367
1466
|
}
|
|
1368
|
-
return { prefix: minPrefix, left: `${dim("5h")} ${
|
|
1467
|
+
return { prefix: minPrefix, left: `${dim("5h")} ${fCellN} ${dim("1w")} ${wCellN}${svCompact}`, right: "" };
|
|
1369
1468
|
}
|
|
1370
1469
|
}
|
|
1371
1470
|
if (realQuota?.type === "gemini") {
|
|
@@ -1385,10 +1484,13 @@ function getProviderRow(provider, marker, markerColor, qosProfile, accountsConfi
|
|
|
1385
1484
|
if (realQuota?.type === "codex") {
|
|
1386
1485
|
const main = realQuota.buckets.codex || realQuota.buckets[Object.keys(realQuota.buckets)[0]];
|
|
1387
1486
|
if (main) {
|
|
1388
|
-
|
|
1389
|
-
const
|
|
1390
|
-
|
|
1391
|
-
|
|
1487
|
+
// 캐시된 값 그대로 표시 (시간은 advanceToNextCycle이 처리)
|
|
1488
|
+
const fiveP = main.primary?.used_percent != null ? clampPercent(main.primary.used_percent) : null;
|
|
1489
|
+
const weekP = main.secondary?.used_percent != null ? clampPercent(main.secondary.used_percent) : null;
|
|
1490
|
+
const fCell = fiveP != null ? colorByProvider(fiveP, formatPercentCell(fiveP), provFn) : dim(formatPlaceholderPercentCell());
|
|
1491
|
+
const wCell = weekP != null ? colorByProvider(weekP, formatPercentCell(weekP), provFn) : dim(formatPlaceholderPercentCell());
|
|
1492
|
+
quotaSection = `${dim("5h:")}${fCell} ` +
|
|
1493
|
+
`${dim("1w:")}${wCell}`;
|
|
1392
1494
|
}
|
|
1393
1495
|
}
|
|
1394
1496
|
if (realQuota?.type === "gemini") {
|
|
@@ -1401,7 +1503,7 @@ function getProviderRow(provider, marker, markerColor, qosProfile, accountsConfi
|
|
|
1401
1503
|
}
|
|
1402
1504
|
}
|
|
1403
1505
|
if (!quotaSection) {
|
|
1404
|
-
quotaSection = `${dim("5h:")}${
|
|
1506
|
+
quotaSection = `${dim("5h:")}${dim(formatPlaceholderPercentCell())} ${dim("1w:")}${dim(formatPlaceholderPercentCell())}`;
|
|
1405
1507
|
}
|
|
1406
1508
|
const prefix = `${bold(markerColor(`${marker}`))}:`;
|
|
1407
1509
|
// compact: sv + 계정 (모델 라벨 제거)
|
|
@@ -1412,13 +1514,18 @@ function getProviderRow(provider, marker, markerColor, qosProfile, accountsConfi
|
|
|
1412
1514
|
if (realQuota?.type === "codex") {
|
|
1413
1515
|
const main = realQuota.buckets.codex || realQuota.buckets[Object.keys(realQuota.buckets)[0]];
|
|
1414
1516
|
if (main) {
|
|
1415
|
-
|
|
1416
|
-
const
|
|
1417
|
-
const
|
|
1418
|
-
const
|
|
1419
|
-
|
|
1517
|
+
// 캐시된 값 그대로 표시 (시간은 advanceToNextCycle이 처리)
|
|
1518
|
+
const fiveP = main.primary?.used_percent != null ? clampPercent(main.primary.used_percent) : null;
|
|
1519
|
+
const weekP = main.secondary?.used_percent != null ? clampPercent(main.secondary.used_percent) : null;
|
|
1520
|
+
const fiveReset = formatResetRemaining(main.primary?.resets_at, FIVE_HOUR_MS) || "n/a";
|
|
1521
|
+
const weekReset = formatResetRemainingDayHour(main.secondary?.resets_at, SEVEN_DAY_MS) || "n/a";
|
|
1522
|
+
const fCell = fiveP != null ? colorByProvider(fiveP, formatPercentCell(fiveP), provFn) : dim(formatPlaceholderPercentCell());
|
|
1523
|
+
const wCell = weekP != null ? colorByProvider(weekP, formatPercentCell(weekP), provFn) : dim(formatPlaceholderPercentCell());
|
|
1524
|
+
const fBar = fiveP != null ? tierBar(fiveP, provAnsi) : tierDimBar();
|
|
1525
|
+
const wBar = weekP != null ? tierBar(weekP, provAnsi) : tierDimBar();
|
|
1526
|
+
quotaSection = `${dim("5h:")}${fBar}${fCell} ` +
|
|
1420
1527
|
`${dim(formatTimeCell(fiveReset))} ` +
|
|
1421
|
-
`${dim("1w:")}${
|
|
1528
|
+
`${dim("1w:")}${wBar}${wCell} ` +
|
|
1422
1529
|
`${dim(formatTimeCellDH(weekReset))}`;
|
|
1423
1530
|
}
|
|
1424
1531
|
}
|
|
@@ -1427,7 +1534,7 @@ function getProviderRow(provider, marker, markerColor, qosProfile, accountsConfi
|
|
|
1427
1534
|
const bucket = realQuota.quotaBucket;
|
|
1428
1535
|
if (bucket) {
|
|
1429
1536
|
const usedP = clampPercent((1 - (bucket.remainingFraction ?? 1)) * 100);
|
|
1430
|
-
const rstRemaining = formatResetRemaining(bucket.resetTime) || "n/a";
|
|
1537
|
+
const rstRemaining = formatResetRemaining(bucket.resetTime, ONE_DAY_MS) || "n/a";
|
|
1431
1538
|
quotaSection = `${dim("1d:")}${tierBar(usedP, provAnsi)}${colorByProvider(usedP, formatPercentCell(usedP), provFn)} ${dim(formatTimeCell(rstRemaining))} ` +
|
|
1432
1539
|
`${dim("1w:")}${tierInfBar()}${dim("\u221E%".padStart(PERCENT_CELL_WIDTH))} ${dim(formatTimeCellDH("-d--h"))}`;
|
|
1433
1540
|
} else {
|