triflux 8.4.1 → 8.5.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 CHANGED
@@ -61,6 +61,10 @@
61
61
 
62
62
  ## Tri-CLI 합의 엔진
63
63
 
64
+ <p align="center">
65
+ <img src="docs/assets/consensus-flow.svg" alt="Tri-CLI Consensus 플로우" width="680">
66
+ </p>
67
+
64
68
  triflux의 핵심 혁신입니다. 단일 모델을 맹신하는 대신, 모든 Deep 스킬은 다음 과정을 거칩니다:
65
69
 
66
70
  ```
@@ -175,6 +179,10 @@ Phase 3: Resolution (합의율 < 70%일 경우)
175
179
 
176
180
  모든 도메인에서 두 가지 모드를 제공합니다:
177
181
 
182
+ <p align="center">
183
+ <img src="docs/assets/deep-vs-light.svg" alt="Deep vs Light 비교" width="680">
184
+ </p>
185
+
178
186
  | 항목 | Light | Deep |
179
187
  |------|-------|------|
180
188
  | **CLI** | 단일 (주로 Codex) | 3자 (Claude + Codex + Gemini) |
@@ -188,6 +196,13 @@ Phase 3: Resolution (합의율 < 70%일 경우)
188
196
 
189
197
  ## 아키텍처
190
198
 
199
+ <p align="center">
200
+ <img src="docs/assets/architecture.svg" alt="triflux 아키텍처" width="680">
201
+ </p>
202
+
203
+ <details>
204
+ <summary>인터랙티브 다이어그램 (GitHub 전용)</summary>
205
+
191
206
  ```mermaid
192
207
  graph TD
193
208
  User([사용자 / Claude Code]) <-->|Skills & Slash Commands| TFX[tfx Skills Layer]
@@ -217,6 +232,8 @@ graph TD
217
232
  HUB -.->|MCP Bridge| External[외부 MCP 클라이언트]
218
233
  ```
219
234
 
235
+ </details>
236
+
220
237
  ---
221
238
 
222
239
  ## 빠른 시작
package/README.md CHANGED
@@ -61,6 +61,10 @@
61
61
 
62
62
  ## Tri-CLI Consensus
63
63
 
64
+ <p align="center">
65
+ <img src="docs/assets/consensus-flow.svg" alt="Tri-CLI Consensus Flow" width="680">
66
+ </p>
67
+
64
68
  The core innovation of triflux. Instead of trusting a single model, every Deep skill runs:
65
69
 
66
70
  ```
@@ -175,6 +179,10 @@ Phase 3: Resolution (if consensus < 70%)
175
179
 
176
180
  Every domain offers both modes:
177
181
 
182
+ <p align="center">
183
+ <img src="docs/assets/deep-vs-light.svg" alt="Deep vs Light comparison" width="680">
184
+ </p>
185
+
178
186
  | Dimension | Light | Deep |
179
187
  |-----------|-------|------|
180
188
  | **CLIs** | Single (usually Codex) | 3-party (Claude + Codex + Gemini) |
@@ -188,6 +196,13 @@ Every domain offers both modes:
188
196
 
189
197
  ## Architecture
190
198
 
199
+ <p align="center">
200
+ <img src="docs/assets/architecture.svg" alt="triflux architecture" width="680">
201
+ </p>
202
+
203
+ <details>
204
+ <summary>Interactive diagram (GitHub only)</summary>
205
+
191
206
  ```mermaid
192
207
  graph TD
193
208
  User([User / Claude Code]) <-->|Skills & Slash Commands| TFX[tfx Skills Layer]
@@ -217,6 +232,8 @@ graph TD
217
232
  HUB -.->|MCP Bridge| External[External MCP Clients]
218
233
  ```
219
234
 
235
+ </details>
236
+
220
237
  ---
221
238
 
222
239
  ## Quick Start
package/bin/triflux.mjs CHANGED
@@ -1403,7 +1403,7 @@ async function cmdDoctor(options = {}) {
1403
1403
  // --fix 없이는 개수만 보고
1404
1404
  const { execSync: execSyncDoctor } = await import("node:child_process");
1405
1405
  const countStr = execSyncDoctor(
1406
- `powershell -NoProfile -Command "(Get-Process node -ErrorAction SilentlyContinue).Count"`,
1406
+ `powershell -NoProfile -WindowStyle Hidden -Command "(Get-Process node -ErrorAction SilentlyContinue).Count"`,
1407
1407
  { encoding: "utf8", timeout: 5000 },
1408
1408
  ).trim();
1409
1409
  const count = Number.parseInt(countStr, 10) || 0;
@@ -1486,7 +1486,7 @@ async function cmdDoctor(options = {}) {
1486
1486
  // Claude Code 프로세스에서 세션 ID 검색
1487
1487
  if (process.platform === "win32") {
1488
1488
  const psOut = execSync(
1489
- `powershell -NoProfile -Command "$ErrorActionPreference='SilentlyContinue'; Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -match '${safeToken}' } | Select-Object ProcessId | ConvertTo-Json -Compress"`,
1489
+ `powershell -NoProfile -WindowStyle Hidden -Command "$ErrorActionPreference='SilentlyContinue'; Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -match '${safeToken}' } | Select-Object ProcessId | ConvertTo-Json -Compress"`,
1490
1490
  { encoding: "utf8", timeout: 8000, stdio: ["ignore", "pipe", "ignore"], windowsHide: true },
1491
1491
  ).trim();
1492
1492
  if (psOut && psOut !== "null") {
@@ -1497,7 +1497,7 @@ async function cmdDoctor(options = {}) {
1497
1497
  } else {
1498
1498
  const psOut = execSync(
1499
1499
  `ps -ax -o pid=,command= | grep -i '${safeToken}' | grep -v grep`,
1500
- { encoding: "utf8", timeout: 5000, stdio: ["ignore", "pipe", "ignore"] },
1500
+ { encoding: "utf8", timeout: 5000, stdio: ["ignore", "pipe", "ignore"], windowsHide: true },
1501
1501
  ).trim();
1502
1502
  hasActiveMember = psOut.length > 0;
1503
1503
  }
@@ -155,8 +155,8 @@ export function cleanupOrphanNodeProcesses() {
155
155
 
156
156
  try {
157
157
  const treeOutput = execSync(
158
- `powershell -NoProfile -ExecutionPolicy Bypass -File "${TREE_SCRIPT_PATH}" -StartPid ${myPid}`,
159
- { encoding: "utf8", timeout: 8000, stdio: ["pipe", "pipe", "pipe"] },
158
+ `powershell -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File "${TREE_SCRIPT_PATH}" -StartPid ${myPid}`,
159
+ { encoding: "utf8", timeout: 8000, stdio: ["pipe", "pipe", "pipe"], windowsHide: true },
160
160
  );
161
161
  for (const line of treeOutput.split(/\r?\n/)) {
162
162
  const pid = Number.parseInt(line.trim(), 10);
@@ -168,8 +168,8 @@ export function cleanupOrphanNodeProcesses() {
168
168
  const procMap = new Map();
169
169
  try {
170
170
  const output = execSync(
171
- `powershell -NoProfile -ExecutionPolicy Bypass -File "${SCAN_SCRIPT_PATH}"`,
172
- { encoding: "utf8", timeout: 15000, stdio: ["pipe", "pipe", "pipe"] },
171
+ `powershell -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File "${SCAN_SCRIPT_PATH}"`,
172
+ { encoding: "utf8", timeout: 15000, stdio: ["pipe", "pipe", "pipe"], windowsHide: true },
173
173
  );
174
174
 
175
175
  for (const line of output.split(/\r?\n/)) {
@@ -203,8 +203,8 @@ export function cleanupOrphanNodeProcesses() {
203
203
  let remaining = 0;
204
204
  try {
205
205
  const countOutput = execSync(
206
- `powershell -NoProfile -Command "(Get-Process node -ErrorAction SilentlyContinue).Count"`,
207
- { encoding: "utf8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] },
206
+ `powershell -NoProfile -WindowStyle Hidden -Command "(Get-Process node -ErrorAction SilentlyContinue).Count"`,
207
+ { encoding: "utf8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"], windowsHide: true },
208
208
  );
209
209
  remaining = Number.parseInt(countOutput.trim(), 10) || 0;
210
210
  } catch {}
@@ -514,8 +514,8 @@ function killOrphanPipeHelpers(sessionName) {
514
514
  const safeSession = sanitizePathPart(sessionName);
515
515
  try {
516
516
  const output = childProcess.execSync(
517
- `powershell -NoProfile -Command "$ErrorActionPreference='SilentlyContinue'; Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -match 'pipe-pane-capture' -and $_.CommandLine -match '${safeSession}' } | Select-Object -ExpandProperty ProcessId"`,
518
- { encoding: "utf8", timeout: 8000, stdio: ["pipe", "pipe", "pipe"] },
517
+ `powershell -NoProfile -WindowStyle Hidden -Command "$ErrorActionPreference='SilentlyContinue'; Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -match 'pipe-pane-capture' -and $_.CommandLine -match '${safeSession}' } | Select-Object -ExpandProperty ProcessId"`,
518
+ { encoding: "utf8", timeout: 8000, stdio: ["pipe", "pipe", "pipe"], windowsHide: true },
519
519
  );
520
520
  const pids = output.split(/\r?\n/).map((l) => Number.parseInt(l.trim(), 10)).filter((p) => Number.isFinite(p) && p > 0);
521
521
  for (const pid of pids) {
@@ -549,8 +549,8 @@ function killOrphanMcpProcesses(sessionName) {
549
549
  try {
550
550
  // 세션 결과 디렉토리 패턴으로 MCP 서버 프로세스 식별
551
551
  const output = childProcess.execSync(
552
- `powershell -NoProfile -Command "$ErrorActionPreference='SilentlyContinue'; Get-CimInstance Win32_Process | Where-Object { $_.Name -eq 'node.exe' -and $_.CommandLine -match '${safeSession}' } | Select-Object -ExpandProperty ProcessId"`,
553
- { encoding: "utf8", timeout: 8000, stdio: ["pipe", "pipe", "pipe"] },
552
+ `powershell -NoProfile -WindowStyle Hidden -Command "$ErrorActionPreference='SilentlyContinue'; Get-CimInstance Win32_Process | Where-Object { $_.Name -eq 'node.exe' -and $_.CommandLine -match '${safeSession}' } | Select-Object -ExpandProperty ProcessId"`,
553
+ { encoding: "utf8", timeout: 8000, stdio: ["pipe", "pipe", "pipe"], windowsHide: true },
554
554
  );
555
555
  const pids = output.split(/\r?\n/).map((l) => Number.parseInt(l.trim(), 10)).filter((p) => Number.isFinite(p) && p > 0 && p !== hubPid);
556
556
  for (const pid of pids) {
package/hub/team/tui.mjs CHANGED
@@ -72,6 +72,13 @@ function heartbeat(status, shimmerIntensity = 0) {
72
72
  : SPINNER_BASE_COLOR;
73
73
  return `\x1b[38;2;${c.r};${c.g};${c.b}m${SPINNER_FRAMES[idx]}${RESET}`;
74
74
  }
75
+
76
+ function currentShimmer() {
77
+ const elapsed = Date.now() - spinnerStart;
78
+ const t = (elapsed % SPINNER_CYCLE_MS) / SPINNER_CYCLE_MS;
79
+ return 0.5 * (1 + Math.sin(t * Math.PI * 2));
80
+ }
81
+
75
82
  const GRID_GAP = 2;
76
83
  const DEFAULT_DETAIL_LINES = 10;
77
84
  // Tier1 상단 고정 행 수
@@ -215,6 +222,16 @@ function fadeBorderColor(currentStatus, prevStatus, changedAt) {
215
222
  return `\x1b[38;2;${c.r};${c.g};${c.b}m`;
216
223
  }
217
224
 
225
+ function dedupeRole(role, name, cli) {
226
+ if (!role) return "";
227
+ let r = role;
228
+ const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
229
+ r = r.replace(new RegExp(esc(cli), "gi"), "").trim();
230
+ r = r.replace(new RegExp(esc(name), "gi"), "").trim();
231
+ r = r.replace(/\(\s*\)/g, "").replace(/^\s*[•·\-]\s*/, "").trim();
232
+ return r;
233
+ }
234
+
218
235
  // ── 텍스트 래핑 ──────────────────────────────────────────────────────────
219
236
  function wrapLine(text, width) {
220
237
  const limit = Math.max(8, width);
@@ -310,7 +327,7 @@ function buildTier1(names, workers, pipeline, elapsed, width, version) {
310
327
  const phase = pipeline.phase || "exec";
311
328
  const row1 = truncate(
312
329
  `${color("▲", FG.triflux)} v${version} ${dim("│")} ${color(phase, phaseColor(phase))} ${dim("│")} ${elapsed}s ${dim("│")} ` +
313
- `${color(`✓${ok}`, MOCHA.ok)} ${color(`◑${partial}`, MOCHA.partial)} ${color(`✗${failed}`, MOCHA.fail)} ${dim(`▶${running}`)} ${color("Tab/j/k:nav • f:follow • r:raw • 1-9:jump", MOCHA.subtext)}`,
330
+ `${color(`✓${ok}`, MOCHA.ok)} ${color(`◑${partial}`, MOCHA.partial)} ${color(`✗${failed}`, MOCHA.fail)} ${dim(`▶${running}`)} ${color("Tab:focus • j/k/↑↓:nav • f:follow • r:raw • l:tab • 1-9:jump", MOCHA.subtext)}`,
314
331
  width,
315
332
  );
316
333
  return [row1];
@@ -357,9 +374,10 @@ function buildWorkerRail(name, st, opts = {}) {
357
374
 
358
375
  // Tier2 행 1: 이름 + CLI + role
359
376
  const selMark = selected ? (focused ? color("▶", MOCHA.blue) : color(">", FG.triflux)) : " ";
360
- const hb = heartbeat(status);
377
+ const hb = heartbeat(status, status === "running" ? currentShimmer() : 0);
378
+ const displayRole = dedupeRole(role, name, cli);
361
379
  const title = truncate(
362
- `${selMark} ${hb} ${color(name, FG.triflux)} ${color("•", MOCHA.overlay)} ${color(cli, cliColor(cli))}${role ? ` ${color(`(${role})`, MOCHA.overlay)}` : ""}`,
380
+ `${selMark} ${hb} ${color(name, FG.triflux)} ${color("•", MOCHA.overlay)} ${color(cli, cliColor(cli))}${displayRole ? ` ${color(`(${displayRole})`, MOCHA.overlay)}` : ""}`,
363
381
  innerWidth,
364
382
  );
365
383
 
@@ -442,9 +460,10 @@ function buildFocusPane(name, st, opts = {}) {
442
460
  const status = runtimeStatus(st);
443
461
 
444
462
  // Tab bar: 활성 탭은 MOCHA.blue + bold, 비활성은 MOCHA.overlay
445
- const tabLog = `${MOCHA.blue}${bold("[Log]")}`;
446
- const tabDetail = color("[Detail]", MOCHA.overlay);
447
- const tabFiles = color(`[Files ${files.length}]`, MOCHA.overlay);
463
+ const activeTab = opts.activeTab || "log";
464
+ const tabLog = activeTab === "log" ? `${MOCHA.blue}${bold("[Log]")}` : color("[Log]", MOCHA.overlay);
465
+ const tabDetail = activeTab === "detail" ? `${MOCHA.blue}${bold("[Detail]")}` : color("[Detail]", MOCHA.overlay);
466
+ const tabFiles = activeTab === "files" ? `${MOCHA.blue}${bold(`[Files ${files.length}]`)}` : color(`[Files ${files.length}]`, MOCHA.overlay);
448
467
  const tabBar = truncate(`${tabLog} ${tabDetail} ${tabFiles}`, innerWidth);
449
468
 
450
469
  const stickyLines = [
@@ -457,7 +476,26 @@ function buildFocusPane(name, st, opts = {}) {
457
476
 
458
477
  // 본문 스크롤 영역
459
478
  const bodyAvail = Math.max(0, height - stickyLines.length - 3); // top+bot border + scrollInfo
460
- const allBodyLines = wrapTextAll(detailText(st), innerWidth, rawMode);
479
+
480
+ let allBodyLines;
481
+ if (activeTab === "detail") {
482
+ const summaryLines = [];
483
+ for (const key of SUMMARY_KEYS) {
484
+ const value = st.handoff?.[key];
485
+ if (Array.isArray(value) && value.length > 0) summaryLines.push(`${key}: ${value.join(", ")}`);
486
+ else if (value) summaryLines.push(`${key}: ${value}`);
487
+ }
488
+ allBodyLines = summaryLines.length > 0
489
+ ? summaryLines.flatMap((l) => wrapLine(l, innerWidth))
490
+ : [dim("no structured data")];
491
+ } else if (activeTab === "files") {
492
+ const filesList = sanitizeFiles(st.handoff?.files_changed || st.files_changed);
493
+ allBodyLines = filesList.length > 0
494
+ ? filesList.map((f, i) => `${i + 1}. ${f}`)
495
+ : [dim("no files changed")];
496
+ } else {
497
+ allBodyLines = wrapTextAll(detailText(st), innerWidth, rawMode);
498
+ }
461
499
 
462
500
  let startIdx;
463
501
  if (followTail) {
@@ -497,7 +535,7 @@ function buildSummaryBar(names, workers, selectedWorker, pipeline, width, versio
497
535
  return padRight(truncate(label, maxChipWidth), maxChipWidth);
498
536
  });
499
537
  const chipsLine = truncate(chips.join(color(" │ ", MOCHA.overlay)), width - 4);
500
- const keysLine = truncate(color("Tab:focus • j/k:scroll • f:follow • r:raw • 1-9:jump", MOCHA.subtext), width - 4);
538
+ const keysLine = truncate(color("Tab:focus • j/k/↑↓:nav • f:follow • r:raw • l:tab • 1-9:jump", MOCHA.subtext), width - 4);
501
539
  const framed = box([chipsLine, keysLine], width);
502
540
  return [framed.top, ...framed.body, framed.bot];
503
541
  }
@@ -590,6 +628,7 @@ export function createLogDashboard(opts = {}) {
590
628
  let detailScrollOffset = 0;
591
629
  let followTail = false;
592
630
  let rawMode = false;
631
+ let focusTab = "log"; // "log" | "detail" | "files"
593
632
  let inputAttached = false;
594
633
  let rawModeEnabled = false;
595
634
 
@@ -682,6 +721,14 @@ export function createLogDashboard(opts = {}) {
682
721
  if (key === "f") { followTail = !followTail; if (followTail) detailScrollOffset = 0; render(); return; }
683
722
  // r: raw mode 토글
684
723
  if (key === "r") { rawMode = !rawMode; render(); return; }
724
+ // l: 탭 전환 (Log → Detail → Files)
725
+ if (key === "l") {
726
+ const tabs = ["log", "detail", "files"];
727
+ focusTab = tabs[(tabs.indexOf(focusTab) + 1) % tabs.length];
728
+ detailScrollOffset = 0;
729
+ render();
730
+ return;
731
+ }
685
732
  // 1-9: 워커 직접 선택
686
733
  if (/^[1-9]$/.test(key)) {
687
734
  const names = visibleWorkerNames();
@@ -747,6 +794,7 @@ export function createLogDashboard(opts = {}) {
747
794
  followTail,
748
795
  rawMode,
749
796
  focused: focus === "detail",
797
+ activeTab: focusTab,
750
798
  });
751
799
  return [...tier1, ...summaryBar, ...focusPane];
752
800
  }
@@ -787,6 +835,7 @@ export function createLogDashboard(opts = {}) {
787
835
  followTail,
788
836
  rawMode,
789
837
  focused: focus === "detail",
838
+ activeTab: focusTab,
790
839
  });
791
840
  }
792
841
  while (focusLines.length < bodyHeight) focusLines.push(padRight("", focusWidth));
@@ -927,6 +976,15 @@ export function createLogDashboard(opts = {}) {
927
976
  return focus === "detail";
928
977
  },
929
978
 
979
+ getFocusTab() {
980
+ return focusTab;
981
+ },
982
+
983
+ setFocusTab(tab) {
984
+ const valid = ["log", "detail", "files"];
985
+ if (valid.includes(tab)) { focusTab = tab; detailScrollOffset = 0; }
986
+ },
987
+
930
988
  getLayout() {
931
989
  return layoutHint;
932
990
  },
package/hud/utils.mjs CHANGED
@@ -6,9 +6,7 @@ import { dirname } from "node:path";
6
6
  import { createHash } from "node:crypto";
7
7
  import {
8
8
  PERCENT_CELL_WIDTH, TIME_CELL_INNER_WIDTH, SV_CELL_WIDTH,
9
- FIVE_HOUR_MS, SEVEN_DAY_MS,
10
9
  } from "./constants.mjs";
11
- import { dim } from "./colors.mjs";
12
10
 
13
11
  export async function readStdinJson() {
14
12
  if (process.stdin.isTTY) return {};
@@ -58,16 +56,23 @@ export function stripAnsi(text) {
58
56
  return String(text).replace(/\x1b\[[0-9;]*m/g, "");
59
57
  }
60
58
 
61
- export function padAnsiRight(text, width) {
62
- const len = stripAnsi(text).length;
59
+ function getVisibleLength(text) {
60
+ return stripAnsi(text).length;
61
+ }
62
+
63
+ function padAnsi(text, width, align = "right") {
64
+ const len = getVisibleLength(text);
63
65
  if (len >= width) return text;
64
- return text + " ".repeat(width - len);
66
+ const padding = " ".repeat(width - len);
67
+ return align === "left" ? padding + text : text + padding;
68
+ }
69
+
70
+ export function padAnsiRight(text, width) {
71
+ return padAnsi(text, width, "right");
65
72
  }
66
73
 
67
74
  export function padAnsiLeft(text, width) {
68
- const len = stripAnsi(text).length;
69
- if (len >= width) return text;
70
- return " ".repeat(width - len) + text;
75
+ return padAnsi(text, width, "left");
71
76
  }
72
77
 
73
78
  export function fitText(text, width) {
@@ -164,13 +169,29 @@ export function advanceToNextCycle(epochMs, cycleMs) {
164
169
  return epochMs + Math.ceil(elapsed / cycleMs) * cycleMs;
165
170
  }
166
171
 
172
+ function parseResetDate(isoOrUnix) {
173
+ if (!isoOrUnix) return null;
174
+ const date = typeof isoOrUnix === "string"
175
+ ? new Date(isoOrUnix)
176
+ : new Date(isoOrUnix * 1000);
177
+ return Number.isNaN(date.getTime()) ? null : date;
178
+ }
179
+
180
+ function getResetTargetMs(isoOrUnix, cycleMs = 0) {
181
+ const date = parseResetDate(isoOrUnix);
182
+ if (!date) return null;
183
+ return advanceToNextCycle(date.getTime(), cycleMs);
184
+ }
185
+
186
+ function getRemainingResetMs(isoOrUnix, cycleMs = 0) {
187
+ const targetMs = getResetTargetMs(isoOrUnix, cycleMs);
188
+ if (targetMs == null) return null;
189
+ return targetMs - Date.now();
190
+ }
191
+
167
192
  export function formatResetRemaining(isoOrUnix, cycleMs = 0) {
168
- if (!isoOrUnix) return "";
169
- const d = typeof isoOrUnix === "string" ? new Date(isoOrUnix) : new Date(isoOrUnix * 1000);
170
- if (isNaN(d.getTime())) return "";
171
- const targetMs = advanceToNextCycle(d.getTime(), cycleMs);
172
- const diffMs = targetMs - Date.now();
173
- if (diffMs <= 0) return "";
193
+ const diffMs = getRemainingResetMs(isoOrUnix, cycleMs);
194
+ if (diffMs == null || diffMs <= 0) return "";
174
195
  const totalMinutes = Math.floor(diffMs / 60000);
175
196
  const totalHours = Math.floor(totalMinutes / 60);
176
197
  const minutes = totalMinutes % 60;
@@ -178,18 +199,13 @@ export function formatResetRemaining(isoOrUnix, cycleMs = 0) {
178
199
  }
179
200
 
180
201
  export function isResetPast(isoOrUnix) {
181
- if (!isoOrUnix) return false;
182
- const d = typeof isoOrUnix === "string" ? new Date(isoOrUnix) : new Date(isoOrUnix * 1000);
183
- return !isNaN(d.getTime()) && d.getTime() <= Date.now();
202
+ const date = parseResetDate(isoOrUnix);
203
+ return date != null && date.getTime() <= Date.now();
184
204
  }
185
205
 
186
206
  export function formatResetRemainingDayHour(isoOrUnix, cycleMs = 0) {
187
- if (!isoOrUnix) return "";
188
- const d = typeof isoOrUnix === "string" ? new Date(isoOrUnix) : new Date(isoOrUnix * 1000);
189
- if (isNaN(d.getTime())) return "";
190
- const targetMs = advanceToNextCycle(d.getTime(), cycleMs);
191
- const diffMs = targetMs - Date.now();
192
- if (diffMs <= 0) return "";
207
+ const diffMs = getRemainingResetMs(isoOrUnix, cycleMs);
208
+ if (diffMs == null || diffMs <= 0) return "";
193
209
  const totalMinutes = Math.floor(diffMs / 60000);
194
210
  const days = Math.floor(totalMinutes / (60 * 24));
195
211
  const hours = Math.floor((totalMinutes % (60 * 24)) / 60);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triflux",
3
- "version": "8.4.1",
3
+ "version": "8.5.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": {