triflux 2.1.0 → 2.3.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.
@@ -7,7 +7,7 @@ import https from "node:https";
7
7
  import { createHash } from "node:crypto";
8
8
  import { spawn, execSync } from "node:child_process";
9
9
 
10
- const VERSION = "1.8";
10
+ const VERSION = "2.0";
11
11
 
12
12
  // ============================================================================
13
13
  // ANSI 색상 (OMC colors.js 스키마 일치)
@@ -19,7 +19,7 @@ const RED = "\x1b[31m";
19
19
  const GREEN = "\x1b[32m";
20
20
  const YELLOW = "\x1b[33m";
21
21
  const CYAN = "\x1b[36m";
22
- const CLAUDE_ORANGE = "\x1b[38;5;214m";
22
+ const CLAUDE_ORANGE = "\x1b[38;5;173m"; // #D87656 (Claude 테라코타)
23
23
  const CODEX_WHITE = "\x1b[97m"; // bright white (SGR 37은 Windows Terminal에서 연회색 매핑)
24
24
  const GEMINI_BLUE = "\x1b[38;5;39m";
25
25
 
@@ -52,15 +52,43 @@ function colorParallel(current, cap) {
52
52
  return red(`${current}/${cap}`);
53
53
  }
54
54
 
55
- function coloredBar(percent, width = 8, baseColor = null) {
55
+ const GAUGE_WIDTH = 5;
56
+ const GAUGE_BLOCKS = ["░", "▒", "▓", "█"]; // 밝기 0~3
57
+
58
+ function coloredBar(percent, width = GAUGE_WIDTH, baseColor = null) {
56
59
  const safePercent = Math.min(100, Math.max(0, percent));
57
- const filled = Math.round((safePercent / 100) * width);
58
- const empty = width - filled;
60
+ const perBlock = 100 / width;
61
+
62
+ // 상태별 색상
59
63
  let barColor;
60
64
  if (safePercent >= 85) barColor = RED;
61
65
  else if (safePercent >= 70) barColor = YELLOW;
62
66
  else barColor = baseColor || GREEN;
63
- return `${barColor}${"█".repeat(filled)}${DIM}${"░".repeat(empty)}${RESET}`;
67
+
68
+ let bar = "";
69
+ for (let i = 0; i < width; i++) {
70
+ const blockStart = i * perBlock;
71
+ const blockEnd = (i + 1) * perBlock;
72
+
73
+ if (safePercent >= blockEnd) {
74
+ bar += "█"; // 완전 채움
75
+ } else if (safePercent > blockStart) {
76
+ // 프론티어: 구간 내 진행률
77
+ const progress = (safePercent - blockStart) / perBlock;
78
+ if (progress >= 0.75) bar += "▓";
79
+ else if (progress >= 0.33) bar += "▒";
80
+ else bar += "░";
81
+ } else {
82
+ bar += "░"; // 미도달
83
+ }
84
+ }
85
+
86
+ // 채워진 부분 = barColor, 빈 부분 = DIM
87
+ const filledEnd = Math.ceil(safePercent / perBlock);
88
+ const coloredPart = barColor + bar.slice(0, filledEnd) + RESET;
89
+ const dimPart = filledEnd < width ? DIM + bar.slice(filledEnd) + RESET : "";
90
+
91
+ return coloredPart + dimPart;
64
92
  }
65
93
 
66
94
  // 프로바이더별 색상 % (< 70%: 프로바이더 색, ≥ 70%: 경고색)
@@ -81,7 +109,15 @@ const ACCOUNTS_STATE_PATH = join(homedir(), ".omc", "state", "cli_accounts_state
81
109
  const CLAUDE_CREDENTIALS_PATH = join(homedir(), ".claude", ".credentials.json");
82
110
  const CLAUDE_USAGE_CACHE_PATH = join(homedir(), ".claude", "cache", "claude-usage-cache.json");
83
111
  const OMC_PLUGIN_USAGE_CACHE_PATH = join(homedir(), ".claude", "plugins", "oh-my-claudecode", ".usage-cache.json");
84
- const CLAUDE_USAGE_STALE_MS = 5 * 60 * 1000; // 5분 캐시 (OMC 플러그인과 API 충돌 방지)
112
+ const CLAUDE_USAGE_STALE_MS_SOLO = 5 * 60 * 1000; // OMC 없을 때: 5분 캐시
113
+ const CLAUDE_USAGE_STALE_MS_WITH_OMC = 15 * 60 * 1000; // OMC 있을 때: 15분 (OMC가 30초마다 갱신)
114
+
115
+ // OMC 활성 여부에 따라 캐시 TTL 동적 결정
116
+ function getClaudeUsageStaleMs() {
117
+ return existsSync(OMC_PLUGIN_USAGE_CACHE_PATH)
118
+ ? CLAUDE_USAGE_STALE_MS_WITH_OMC
119
+ : CLAUDE_USAGE_STALE_MS_SOLO;
120
+ }
85
121
  const CLAUDE_USAGE_429_BACKOFF_MS = 10 * 60 * 1000; // 429 에러 시 10분 backoff
86
122
  const CLAUDE_USAGE_ERROR_BACKOFF_MS = 3 * 60 * 1000; // 기타 에러 시 3분 backoff
87
123
  const CLAUDE_API_TIMEOUT_MS = 10_000;
@@ -122,7 +158,7 @@ const GEMINI_SESSION_STALE_MS = 15 * 1000; // 15초
122
158
  const GEMINI_API_TIMEOUT_MS = 3000; // 3초
123
159
  const ACCOUNT_LABEL_WIDTH = 10;
124
160
  const PROVIDER_PREFIX_WIDTH = 2;
125
- const PERCENT_CELL_WIDTH = 4;
161
+ const PERCENT_CELL_WIDTH = 3;
126
162
  const TIME_CELL_INNER_WIDTH = 6;
127
163
  const CLAUDE_REFRESH_FLAG = "--refresh-claude-usage";
128
164
  const CODEX_REFRESH_FLAG = "--refresh-codex-rate-limits";
@@ -235,7 +271,13 @@ function selectTier(stdin, claudeUsage = null) {
235
271
 
236
272
  // 1) 명시적 tier 강제 설정
237
273
  const forcedTier = hudConfig?.tier;
238
- if (["full", "normal", "compact", "nano"].includes(forcedTier)) return forcedTier;
274
+ if (["full", "normal", "compact", "nano", "micro"].includes(forcedTier)) return forcedTier;
275
+
276
+ // 1.5) maxLines=1 → micro (1줄 모드: 알림 배너/분할화면 대응)
277
+ if (Number(hudConfig?.lines) === 1) return "micro";
278
+
279
+ // 1.6) 분할화면 감지: 열 < 80이면 micro (COMPACT_MODE보다 우선)
280
+ if ((getTerminalColumns() || 120) < 80) return "micro";
239
281
 
240
282
  // 2) 기존 모드 플래그 존중
241
283
  if (MINIMAL_MODE) return "nano";
@@ -247,6 +289,7 @@ function selectTier(stdin, claudeUsage = null) {
247
289
  // 4) 터미널 행/열에서 상태영역 예산 추정
248
290
  const rows = getTerminalRows();
249
291
  const cols = getTerminalColumns() || 120;
292
+
250
293
  let budget;
251
294
  if (rows >= 40) budget = 6;
252
295
  else if (rows >= 35) budget = 5;
@@ -257,6 +300,7 @@ function selectTier(stdin, claudeUsage = null) {
257
300
 
258
301
  // 5) 인디케이터 줄 추정
259
302
  let indicatorRows = 1; // bypass permissions (거의 항상 표시)
303
+ indicatorRows += 1; // 선행 개행 가드 (알림 배너 우회용 빈 줄)
260
304
  const contextPercent = getContextPercent(stdin);
261
305
  if (contextPercent >= 85) indicatorRows += 1; // "Context low" 배너
262
306
  // Claude Code 사용량 경고 (노란색 배너: "You've used X% of your ... limit")
@@ -266,26 +310,27 @@ function selectTier(stdin, claudeUsage = null) {
266
310
  if (fiveHourPct >= 80) indicatorRows += 1;
267
311
 
268
312
  // 6) 각 tier에서 줄바꿈 없이 3줄 가용한지 확인
269
- const tierWidths = { full: 75, normal: 60, compact: 40, nano: 34 };
313
+ const tierWidths = { full: 70, normal: 60, compact: 40, nano: 34 };
270
314
  for (const tier of ["full", "normal", "compact", "nano"]) {
271
315
  const lineWidth = tierWidths[tier];
272
316
  const visualRowsPerLine = Math.ceil(lineWidth / Math.max(cols, 1));
273
317
  const totalVisualRows = (3 * visualRowsPerLine) + indicatorRows;
274
318
  if (totalVisualRows <= budget) return tier;
275
319
  }
276
- return "nano";
320
+ // 어떤 tier도 budget에 안 맞으면 micro (1줄 모드)
321
+ return "micro";
277
322
  }
278
323
 
279
324
  // full tier 전용: 게이지 바 접두사 (normal 이하 tier에서는 빈 문자열)
280
325
  function tierBar(percent, baseColor = null) {
281
- return CURRENT_TIER === "full" ? `${coloredBar(percent, 8, baseColor)} ` : "";
326
+ return CURRENT_TIER === "full" ? coloredBar(percent, GAUGE_WIDTH, baseColor) + " " : "";
282
327
  }
283
328
  function tierDimBar() {
284
- return CURRENT_TIER === "full" ? `${dim("░".repeat(8))} ` : "";
329
+ return CURRENT_TIER === "full" ? DIM + "░".repeat(GAUGE_WIDTH) + RESET + " " : "";
285
330
  }
286
331
  // Gemini ∞% 전용: 무한 쿼터이므로 dim 회색 바
287
332
  function tierInfBar() {
288
- return CURRENT_TIER === "full" ? `${DIM}${"█".repeat(8)}${RESET} ` : "";
333
+ return CURRENT_TIER === "full" ? DIM + "█".repeat(GAUGE_WIDTH) + RESET + " " : "";
289
334
  }
290
335
 
291
336
  // ============================================================================
@@ -387,6 +432,50 @@ function renderAlignedRows(rows) {
387
432
  });
388
433
  }
389
434
 
435
+ // micro tier: 모든 프로바이더를 1줄로 압축 (알림 배너/분할화면 대응)
436
+ // 형식: c:16/3 x:5/2 g:∞ sv:143% ctx:53%
437
+ function getMicroLine(stdin, claudeUsage, codexBuckets, geminiSession, geminiBucket, combinedSvPct) {
438
+ const ctx = getContextPercent(stdin);
439
+
440
+ // Claude 5h/1w
441
+ const cF = claudeUsage ? clampPercent(claudeUsage.fiveHourPercent ?? 0) : null;
442
+ const cW = claudeUsage ? clampPercent(claudeUsage.weeklyPercent ?? 0) : null;
443
+ const cVal = cF != null
444
+ ? `${colorByProvider(cF, `${cF}`, claudeOrange)}${dim("/")}${colorByProvider(cW, `${cW}`, claudeOrange)}`
445
+ : dim("--/--");
446
+
447
+ // Codex 5h/1w
448
+ let xVal = dim("--/--");
449
+ if (codexBuckets) {
450
+ const mb = codexBuckets.codex || codexBuckets[Object.keys(codexBuckets)[0]];
451
+ if (mb) {
452
+ const xF = isResetPast(mb.primary?.resets_at) ? 0 : clampPercent(mb.primary?.used_percent ?? 0);
453
+ const xW = isResetPast(mb.secondary?.resets_at) ? 0 : clampPercent(mb.secondary?.used_percent ?? 0);
454
+ xVal = `${colorByProvider(xF, `${xF}`, codexWhite)}${dim("/")}${colorByProvider(xW, `${xW}`, codexWhite)}`;
455
+ }
456
+ }
457
+
458
+ // Gemini
459
+ let gVal;
460
+ if (geminiBucket) {
461
+ const gU = clampPercent(geminiBucket.usedPercent ?? 0);
462
+ gVal = colorByProvider(gU, `${gU}`, geminiBlue);
463
+ } else if ((geminiSession?.total || 0) > 0) {
464
+ gVal = geminiBlue("\u221E");
465
+ } else {
466
+ gVal = dim("--");
467
+ }
468
+
469
+ // sv (trimmed)
470
+ const sv = formatSvPct(combinedSvPct || 0).trim();
471
+
472
+ return `${bold(claudeOrange("c"))}${dim(":")}${cVal} ` +
473
+ `${bold(codexWhite("x"))}${dim(":")}${xVal} ` +
474
+ `${bold(geminiBlue("g"))}${dim(":")}${gVal} ` +
475
+ `${dim("sv:")}${sv} ` +
476
+ `${dim("ctx:")}${colorByPercent(ctx, `${ctx}%`)}`;
477
+ }
478
+
390
479
  function clampPercent(value) {
391
480
  const numeric = Number(value);
392
481
  if (!Number.isFinite(numeric)) return 0;
@@ -405,7 +494,7 @@ function normalizeTimeToken(value) {
405
494
  const text = String(value || "n/a");
406
495
  const hourMinute = text.match(/^(\d+)h(\d+)m$/);
407
496
  if (hourMinute) {
408
- return `${Number(hourMinute[1])}h${Number(hourMinute[2])}m`;
497
+ return `${Number(hourMinute[1])}h${String(Number(hourMinute[2])).padStart(2, "0")}m`;
409
498
  }
410
499
  const dayHour = text.match(/^(\d+)d(\d+)h$/);
411
500
  if (dayHour) {
@@ -416,7 +505,9 @@ function normalizeTimeToken(value) {
416
505
 
417
506
  function formatTimeCell(value) {
418
507
  const text = normalizeTimeToken(value);
419
- return `(${text.padStart(TIME_CELL_INNER_WIDTH, " ")})`;
508
+ // 시간값(숫자 포함)은 0패딩, 비시간값(n/a 등)은 공백패딩
509
+ const padChar = /\d/.test(text) ? "0" : " ";
510
+ return `(${text.padStart(TIME_CELL_INNER_WIDTH, padChar)})`;
420
511
  }
421
512
 
422
513
  // 주간(d/h) 전용 — 최대 7d00h(5자)이므로 공백 불필요
@@ -607,7 +698,7 @@ function readClaudeUsageSnapshot() {
607
698
 
608
699
  // 1차: 자체 캐시에 유효 데이터가 있는 경우
609
700
  if (cache?.data) {
610
- const isFresh = ageMs < CLAUDE_USAGE_STALE_MS;
701
+ const isFresh = ageMs < getClaudeUsageStaleMs();
611
702
  return { data: cache.data, shouldRefresh: !isFresh };
612
703
  }
613
704
 
@@ -623,10 +714,9 @@ function readClaudeUsageSnapshot() {
623
714
  writeClaudeUsageCache(omcCache.data);
624
715
  return { data: omcCache.data, shouldRefresh: false };
625
716
  }
626
- // stale OMC fallback 또는 기본 0%
717
+ // stale OMC fallback 또는 null (--% 플레이스홀더 표시, 가짜 0% 방지)
627
718
  const staleData = omcCache?.data?.fiveHourPercent != null ? stripStaleResets(omcCache.data) : null;
628
- const fallback = staleData || { fiveHourPercent: 0, weeklyPercent: 0, fiveHourResetsAt: null, weeklyResetsAt: null };
629
- return { data: fallback, shouldRefresh: false };
719
+ return { data: staleData, shouldRefresh: false };
630
720
  }
631
721
  }
632
722
 
@@ -637,14 +727,14 @@ function readClaudeUsageSnapshot() {
637
727
  const omcAge = Number.isFinite(omcCache.timestamp) ? Date.now() - omcCache.timestamp : Number.MAX_SAFE_INTEGER;
638
728
  if (omcAge < OMC_CACHE_MAX_AGE_MS) {
639
729
  writeClaudeUsageCache(omcCache.data);
640
- return { data: omcCache.data, shouldRefresh: omcAge > CLAUDE_USAGE_STALE_MS };
730
+ return { data: omcCache.data, shouldRefresh: omcAge > getClaudeUsageStaleMs() };
641
731
  }
642
732
  // stale이어도 data: null보다는 오래된 데이터를 fallback으로 표시
643
733
  return { data: stripStaleResets(omcCache.data), shouldRefresh: true };
644
734
  }
645
735
 
646
- // 캐시/fallback 모두 없음: 기본 0% 표시 + 리프레시 시도 (--% 방지)
647
- return { data: { fiveHourPercent: 0, weeklyPercent: 0, fiveHourResetsAt: null, weeklyResetsAt: null }, shouldRefresh: true };
736
+ // 캐시/fallback 모두 없음: null 반환 --% 플레이스홀더 + 리프레시 시도
737
+ return { data: null, shouldRefresh: true };
648
738
  }
649
739
 
650
740
  function writeClaudeUsageCache(data, errorInfo = null) {
@@ -704,7 +794,7 @@ function scheduleClaudeUsageRefresh() {
704
794
  const omcCache = readJson(OMC_PLUGIN_USAGE_CACHE_PATH, null);
705
795
  if (omcCache?.data?.fiveHourPercent != null) {
706
796
  const omcAge = Number.isFinite(omcCache.timestamp) ? Date.now() - omcCache.timestamp : Infinity;
707
- if (omcAge < CLAUDE_USAGE_STALE_MS) {
797
+ if (omcAge < getClaudeUsageStaleMs()) {
708
798
  writeClaudeUsageCache(omcCache.data); // HUD 캐시에 복사만
709
799
  return;
710
800
  }
@@ -723,7 +813,7 @@ function scheduleClaudeUsageRefresh() {
723
813
 
724
814
  try {
725
815
  const child = spawn(process.execPath, [scriptPath, CLAUDE_REFRESH_FLAG], {
726
- detached: true,
816
+ detached: process.platform !== "win32",
727
817
  stdio: "ignore",
728
818
  windowsHide: true,
729
819
  });
@@ -748,11 +838,11 @@ function formatResetRemaining(isoOrUnix) {
748
838
  const d = typeof isoOrUnix === "string" ? new Date(isoOrUnix) : new Date(isoOrUnix * 1000);
749
839
  if (isNaN(d.getTime())) return "";
750
840
  const diffMs = d.getTime() - Date.now();
751
- if (diffMs <= 0) return "0h0m";
841
+ if (diffMs <= 0) return "0h00m";
752
842
  const totalMinutes = Math.floor(diffMs / 60000);
753
843
  const totalHours = Math.floor(totalMinutes / 60);
754
844
  const minutes = totalMinutes % 60;
755
- return `${totalHours}h${minutes}m`;
845
+ return `${totalHours}h${String(minutes).padStart(2, "0")}m`;
756
846
  }
757
847
 
758
848
  function isResetPast(isoOrUnix) {
@@ -1022,7 +1112,7 @@ function scheduleGeminiQuotaRefresh(accountId) {
1022
1112
  process.execPath,
1023
1113
  [scriptPath, GEMINI_REFRESH_FLAG, "--account", accountId || "gemini-main"],
1024
1114
  {
1025
- detached: true,
1115
+ detached: process.platform !== "win32",
1026
1116
  stdio: "ignore",
1027
1117
  windowsHide: true,
1028
1118
  },
@@ -1054,7 +1144,7 @@ function scheduleCodexRateLimitRefresh() {
1054
1144
  if (!scriptPath) return;
1055
1145
  try {
1056
1146
  const child = spawn(process.execPath, [scriptPath, CODEX_REFRESH_FLAG], {
1057
- detached: true,
1147
+ detached: process.platform !== "win32",
1058
1148
  stdio: "ignore",
1059
1149
  windowsHide: true,
1060
1150
  });
@@ -1085,7 +1175,7 @@ function scheduleGeminiSessionRefresh() {
1085
1175
  if (!scriptPath) return;
1086
1176
  try {
1087
1177
  const child = spawn(process.execPath, [scriptPath, GEMINI_SESSION_REFRESH_FLAG], {
1088
- detached: true,
1178
+ detached: process.platform !== "win32",
1089
1179
  stdio: "ignore",
1090
1180
  windowsHide: true,
1091
1181
  });
@@ -1458,6 +1548,14 @@ async function main() {
1458
1548
  // 인디케이터 인식 tier 선택 (stdin + Claude 사용량 기반)
1459
1549
  CURRENT_TIER = selectTier(stdin, claudeUsageSnapshot.data);
1460
1550
 
1551
+ // micro tier: 1줄 모드 (알림 배너/분할화면 대응)
1552
+ if (CURRENT_TIER === "micro") {
1553
+ const microLine = getMicroLine(stdin, claudeUsageSnapshot.data, codexBuckets,
1554
+ geminiSession, geminiBucket, combinedSvPct);
1555
+ process.stdout.write(`\x1b[0m${microLine}\n`);
1556
+ return;
1557
+ }
1558
+
1461
1559
  const codexQuotaData = codexBuckets ? { type: "codex", buckets: codexBuckets } : null;
1462
1560
  const geminiQuotaData = { type: "gemini", quotaBucket: geminiBucket, session: geminiSession };
1463
1561
 
@@ -1481,12 +1579,13 @@ async function main() {
1481
1579
  if (!geminiActive) outputLines[2] = `${DIM}${stripAnsi(outputLines[2])}${RESET}`;
1482
1580
  }
1483
1581
 
1484
- // Context low 메시지 뒤에 HUD분리되도록 선행 개행 추가
1582
+ // 선행 개행: 알림 배너(노란 글씨) 줄에 오도록 → HUD 내용 보호
1583
+ // Context low(≥85%) 시 추가 개행으로 배너 분리
1485
1584
  const contextPercent = getContextPercent(stdin);
1486
- const contextLowPrefix = contextPercent >= 85 ? "\n" : "";
1585
+ const leadingBreaks = contextPercent >= 85 ? "\n\n" : "\n";
1487
1586
  // 줄별 RESET: Claude Code TUI 스타일 간섭 방지 (색상 밝기 버그 수정)
1488
1587
  const resetedLines = outputLines.map(line => `\x1b[0m${line}`);
1489
- process.stdout.write(`${contextLowPrefix}${resetedLines.join("\n")}\n`);
1588
+ process.stdout.write(`${leadingBreaks}${resetedLines.join("\n")}\n`);
1490
1589
  }
1491
1590
 
1492
1591
  main().catch(() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triflux",
3
- "version": "2.1.0",
3
+ "version": "2.3.0",
4
4
  "description": "CLI-first multi-model orchestrator for Claude Code — route tasks to Codex, Gemini, and Claude",
5
5
  "type": "module",
6
6
  "bin": {