triflux 2.2.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.
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  // tfx-doctor — triflux doctor 바로가기
3
3
  import { dirname } from "path";
4
4
  import { fileURLToPath } from "url";
package/bin/tfx-setup.mjs CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  // tfx-setup — triflux setup 바로가기
3
3
  import { dirname } from "path";
4
4
  import { fileURLToPath } from "url";
package/bin/triflux.mjs CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  // triflux CLI — setup, doctor, version
3
3
  import { copyFileSync, existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync, readdirSync } from "fs";
4
4
  import { join, dirname } from "path";
@@ -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.9";
10
+ const VERSION = "2.0";
11
11
 
12
12
  // ============================================================================
13
13
  // ANSI 색상 (OMC colors.js 스키마 일치)
@@ -109,7 +109,15 @@ const ACCOUNTS_STATE_PATH = join(homedir(), ".omc", "state", "cli_accounts_state
109
109
  const CLAUDE_CREDENTIALS_PATH = join(homedir(), ".claude", ".credentials.json");
110
110
  const CLAUDE_USAGE_CACHE_PATH = join(homedir(), ".claude", "cache", "claude-usage-cache.json");
111
111
  const OMC_PLUGIN_USAGE_CACHE_PATH = join(homedir(), ".claude", "plugins", "oh-my-claudecode", ".usage-cache.json");
112
- 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
+ }
113
121
  const CLAUDE_USAGE_429_BACKOFF_MS = 10 * 60 * 1000; // 429 에러 시 10분 backoff
114
122
  const CLAUDE_USAGE_ERROR_BACKOFF_MS = 3 * 60 * 1000; // 기타 에러 시 3분 backoff
115
123
  const CLAUDE_API_TIMEOUT_MS = 10_000;
@@ -263,7 +271,13 @@ function selectTier(stdin, claudeUsage = null) {
263
271
 
264
272
  // 1) 명시적 tier 강제 설정
265
273
  const forcedTier = hudConfig?.tier;
266
- 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";
267
281
 
268
282
  // 2) 기존 모드 플래그 존중
269
283
  if (MINIMAL_MODE) return "nano";
@@ -275,6 +289,7 @@ function selectTier(stdin, claudeUsage = null) {
275
289
  // 4) 터미널 행/열에서 상태영역 예산 추정
276
290
  const rows = getTerminalRows();
277
291
  const cols = getTerminalColumns() || 120;
292
+
278
293
  let budget;
279
294
  if (rows >= 40) budget = 6;
280
295
  else if (rows >= 35) budget = 5;
@@ -285,6 +300,7 @@ function selectTier(stdin, claudeUsage = null) {
285
300
 
286
301
  // 5) 인디케이터 줄 추정
287
302
  let indicatorRows = 1; // bypass permissions (거의 항상 표시)
303
+ indicatorRows += 1; // 선행 개행 가드 (알림 배너 우회용 빈 줄)
288
304
  const contextPercent = getContextPercent(stdin);
289
305
  if (contextPercent >= 85) indicatorRows += 1; // "Context low" 배너
290
306
  // Claude Code 사용량 경고 (노란색 배너: "You've used X% of your ... limit")
@@ -301,7 +317,8 @@ function selectTier(stdin, claudeUsage = null) {
301
317
  const totalVisualRows = (3 * visualRowsPerLine) + indicatorRows;
302
318
  if (totalVisualRows <= budget) return tier;
303
319
  }
304
- return "nano";
320
+ // 어떤 tier도 budget에 안 맞으면 micro (1줄 모드)
321
+ return "micro";
305
322
  }
306
323
 
307
324
  // full tier 전용: 게이지 바 접두사 (normal 이하 tier에서는 빈 문자열)
@@ -415,6 +432,50 @@ function renderAlignedRows(rows) {
415
432
  });
416
433
  }
417
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
+
418
479
  function clampPercent(value) {
419
480
  const numeric = Number(value);
420
481
  if (!Number.isFinite(numeric)) return 0;
@@ -444,7 +505,9 @@ function normalizeTimeToken(value) {
444
505
 
445
506
  function formatTimeCell(value) {
446
507
  const text = normalizeTimeToken(value);
447
- return `(${text.padStart(TIME_CELL_INNER_WIDTH, "0")})`;
508
+ // 시간값(숫자 포함)은 0패딩, 비시간값(n/a )은 공백패딩
509
+ const padChar = /\d/.test(text) ? "0" : " ";
510
+ return `(${text.padStart(TIME_CELL_INNER_WIDTH, padChar)})`;
448
511
  }
449
512
 
450
513
  // 주간(d/h) 전용 — 최대 7d00h(5자)이므로 공백 불필요
@@ -635,7 +698,7 @@ function readClaudeUsageSnapshot() {
635
698
 
636
699
  // 1차: 자체 캐시에 유효 데이터가 있는 경우
637
700
  if (cache?.data) {
638
- const isFresh = ageMs < CLAUDE_USAGE_STALE_MS;
701
+ const isFresh = ageMs < getClaudeUsageStaleMs();
639
702
  return { data: cache.data, shouldRefresh: !isFresh };
640
703
  }
641
704
 
@@ -651,10 +714,9 @@ function readClaudeUsageSnapshot() {
651
714
  writeClaudeUsageCache(omcCache.data);
652
715
  return { data: omcCache.data, shouldRefresh: false };
653
716
  }
654
- // stale OMC fallback 또는 기본 0%
717
+ // stale OMC fallback 또는 null (--% 플레이스홀더 표시, 가짜 0% 방지)
655
718
  const staleData = omcCache?.data?.fiveHourPercent != null ? stripStaleResets(omcCache.data) : null;
656
- const fallback = staleData || { fiveHourPercent: 0, weeklyPercent: 0, fiveHourResetsAt: null, weeklyResetsAt: null };
657
- return { data: fallback, shouldRefresh: false };
719
+ return { data: staleData, shouldRefresh: false };
658
720
  }
659
721
  }
660
722
 
@@ -665,14 +727,14 @@ function readClaudeUsageSnapshot() {
665
727
  const omcAge = Number.isFinite(omcCache.timestamp) ? Date.now() - omcCache.timestamp : Number.MAX_SAFE_INTEGER;
666
728
  if (omcAge < OMC_CACHE_MAX_AGE_MS) {
667
729
  writeClaudeUsageCache(omcCache.data);
668
- return { data: omcCache.data, shouldRefresh: omcAge > CLAUDE_USAGE_STALE_MS };
730
+ return { data: omcCache.data, shouldRefresh: omcAge > getClaudeUsageStaleMs() };
669
731
  }
670
732
  // stale이어도 data: null보다는 오래된 데이터를 fallback으로 표시
671
733
  return { data: stripStaleResets(omcCache.data), shouldRefresh: true };
672
734
  }
673
735
 
674
- // 캐시/fallback 모두 없음: 기본 0% 표시 + 리프레시 시도 (--% 방지)
675
- return { data: { fiveHourPercent: 0, weeklyPercent: 0, fiveHourResetsAt: null, weeklyResetsAt: null }, shouldRefresh: true };
736
+ // 캐시/fallback 모두 없음: null 반환 --% 플레이스홀더 + 리프레시 시도
737
+ return { data: null, shouldRefresh: true };
676
738
  }
677
739
 
678
740
  function writeClaudeUsageCache(data, errorInfo = null) {
@@ -732,7 +794,7 @@ function scheduleClaudeUsageRefresh() {
732
794
  const omcCache = readJson(OMC_PLUGIN_USAGE_CACHE_PATH, null);
733
795
  if (omcCache?.data?.fiveHourPercent != null) {
734
796
  const omcAge = Number.isFinite(omcCache.timestamp) ? Date.now() - omcCache.timestamp : Infinity;
735
- if (omcAge < CLAUDE_USAGE_STALE_MS) {
797
+ if (omcAge < getClaudeUsageStaleMs()) {
736
798
  writeClaudeUsageCache(omcCache.data); // HUD 캐시에 복사만
737
799
  return;
738
800
  }
@@ -751,7 +813,7 @@ function scheduleClaudeUsageRefresh() {
751
813
 
752
814
  try {
753
815
  const child = spawn(process.execPath, [scriptPath, CLAUDE_REFRESH_FLAG], {
754
- detached: true,
816
+ detached: process.platform !== "win32",
755
817
  stdio: "ignore",
756
818
  windowsHide: true,
757
819
  });
@@ -1050,7 +1112,7 @@ function scheduleGeminiQuotaRefresh(accountId) {
1050
1112
  process.execPath,
1051
1113
  [scriptPath, GEMINI_REFRESH_FLAG, "--account", accountId || "gemini-main"],
1052
1114
  {
1053
- detached: true,
1115
+ detached: process.platform !== "win32",
1054
1116
  stdio: "ignore",
1055
1117
  windowsHide: true,
1056
1118
  },
@@ -1082,7 +1144,7 @@ function scheduleCodexRateLimitRefresh() {
1082
1144
  if (!scriptPath) return;
1083
1145
  try {
1084
1146
  const child = spawn(process.execPath, [scriptPath, CODEX_REFRESH_FLAG], {
1085
- detached: true,
1147
+ detached: process.platform !== "win32",
1086
1148
  stdio: "ignore",
1087
1149
  windowsHide: true,
1088
1150
  });
@@ -1113,7 +1175,7 @@ function scheduleGeminiSessionRefresh() {
1113
1175
  if (!scriptPath) return;
1114
1176
  try {
1115
1177
  const child = spawn(process.execPath, [scriptPath, GEMINI_SESSION_REFRESH_FLAG], {
1116
- detached: true,
1178
+ detached: process.platform !== "win32",
1117
1179
  stdio: "ignore",
1118
1180
  windowsHide: true,
1119
1181
  });
@@ -1486,6 +1548,14 @@ async function main() {
1486
1548
  // 인디케이터 인식 tier 선택 (stdin + Claude 사용량 기반)
1487
1549
  CURRENT_TIER = selectTier(stdin, claudeUsageSnapshot.data);
1488
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
+
1489
1559
  const codexQuotaData = codexBuckets ? { type: "codex", buckets: codexBuckets } : null;
1490
1560
  const geminiQuotaData = { type: "gemini", quotaBucket: geminiBucket, session: geminiSession };
1491
1561
 
@@ -1509,12 +1579,13 @@ async function main() {
1509
1579
  if (!geminiActive) outputLines[2] = `${DIM}${stripAnsi(outputLines[2])}${RESET}`;
1510
1580
  }
1511
1581
 
1512
- // Context low 메시지 뒤에 HUD분리되도록 선행 개행 추가
1582
+ // 선행 개행: 알림 배너(노란 글씨) 줄에 오도록 → HUD 내용 보호
1583
+ // Context low(≥85%) 시 추가 개행으로 배너 분리
1513
1584
  const contextPercent = getContextPercent(stdin);
1514
- const contextLowPrefix = contextPercent >= 85 ? "\n" : "";
1585
+ const leadingBreaks = contextPercent >= 85 ? "\n\n" : "\n";
1515
1586
  // 줄별 RESET: Claude Code TUI 스타일 간섭 방지 (색상 밝기 버그 수정)
1516
1587
  const resetedLines = outputLines.map(line => `\x1b[0m${line}`);
1517
- process.stdout.write(`${contextLowPrefix}${resetedLines.join("\n")}\n`);
1588
+ process.stdout.write(`${leadingBreaks}${resetedLines.join("\n")}\n`);
1518
1589
  }
1519
1590
 
1520
1591
  main().catch(() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triflux",
3
- "version": "2.2.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": {