triflux 7.1.4 → 7.2.2

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.
Files changed (73) hide show
  1. package/.claude-plugin/marketplace.json +31 -31
  2. package/.claude-plugin/plugin.json +22 -23
  3. package/bin/triflux.mjs +18 -5
  4. package/hooks/keyword-rules.json +393 -361
  5. package/hub/bridge.mjs +799 -786
  6. package/hub/delegator/contracts.mjs +37 -38
  7. package/hub/delegator/schema/delegator-tools.schema.json +250 -250
  8. package/hub/delegator/service.mjs +307 -302
  9. package/hub/intent.mjs +108 -11
  10. package/hub/lib/process-utils.mjs +20 -0
  11. package/hub/pipe.mjs +589 -589
  12. package/hub/pipeline/gates/confidence.mjs +1 -1
  13. package/hub/pipeline/gates/selfcheck.mjs +2 -4
  14. package/hub/pipeline/state.mjs +191 -187
  15. package/hub/pipeline/transitions.mjs +124 -120
  16. package/hub/public/dashboard.html +355 -349
  17. package/hub/quality/deslop.mjs +5 -3
  18. package/hub/reflexion.mjs +5 -1
  19. package/hub/research.mjs +6 -1
  20. package/hub/router.mjs +791 -782
  21. package/hub/server.mjs +893 -822
  22. package/hub/store.mjs +807 -778
  23. package/hub/team/agent-map.json +10 -0
  24. package/hub/team/ansi.mjs +3 -4
  25. package/hub/team/cli/commands/control.mjs +43 -43
  26. package/hub/team/cli/commands/interrupt.mjs +36 -36
  27. package/hub/team/cli/commands/kill.mjs +3 -3
  28. package/hub/team/cli/commands/send.mjs +37 -37
  29. package/hub/team/cli/commands/start/index.mjs +18 -8
  30. package/hub/team/cli/commands/start/parse-args.mjs +3 -1
  31. package/hub/team/cli/commands/start/start-headless.mjs +4 -1
  32. package/hub/team/cli/commands/status.mjs +87 -87
  33. package/hub/team/cli/commands/stop.mjs +1 -1
  34. package/hub/team/cli/commands/task.mjs +1 -1
  35. package/hub/team/cli/index.mjs +41 -39
  36. package/hub/team/cli/manifest.mjs +29 -28
  37. package/hub/team/cli/services/hub-client.mjs +37 -0
  38. package/hub/team/cli/services/state-store.mjs +26 -12
  39. package/hub/team/dashboard.mjs +11 -4
  40. package/hub/team/handoff.mjs +12 -0
  41. package/hub/team/headless.mjs +202 -200
  42. package/hub/team/native-supervisor.mjs +386 -346
  43. package/hub/team/nativeProxy.mjs +680 -692
  44. package/hub/team/staleState.mjs +361 -369
  45. package/hub/team/tui-viewer.mjs +27 -3
  46. package/hub/team/tui.mjs +1 -0
  47. package/hub/token-mode.mjs +114 -24
  48. package/hub/workers/delegator-mcp.mjs +1059 -1057
  49. package/hud/colors.mjs +88 -0
  50. package/hud/constants.mjs +78 -0
  51. package/hud/hud-qos-status.mjs +206 -1872
  52. package/hud/providers/claude.mjs +309 -0
  53. package/hud/providers/codex.mjs +151 -0
  54. package/hud/providers/gemini.mjs +320 -0
  55. package/hud/renderers.mjs +424 -0
  56. package/hud/terminal.mjs +140 -0
  57. package/hud/utils.mjs +271 -0
  58. package/package.json +1 -2
  59. package/scripts/__tests__/keyword-detector.test.mjs +234 -234
  60. package/scripts/headless-guard-fast.sh +21 -0
  61. package/scripts/headless-guard.mjs +26 -6
  62. package/scripts/lib/keyword-rules.mjs +166 -168
  63. package/scripts/setup.mjs +725 -690
  64. package/scripts/tfx-route-post.mjs +424 -424
  65. package/scripts/tfx-route.sh +1671 -1650
  66. package/scripts/tmp-cleanup.mjs +74 -0
  67. package/skills/tfx-auto/SKILL.md +279 -278
  68. package/skills/tfx-auto-codex/SKILL.md +98 -77
  69. package/skills/tfx-codex/SKILL.md +65 -65
  70. package/skills/tfx-gemini/SKILL.md +83 -82
  71. package/skills/tfx-hub/SKILL.md +205 -136
  72. package/skills/tfx-multi/SKILL.md +11 -5
  73. package/.mcp.json +0 -8
package/hud/utils.mjs ADDED
@@ -0,0 +1,271 @@
1
+ // ============================================================================
2
+ // 유틸리티 함수
3
+ // ============================================================================
4
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
5
+ import { dirname } from "node:path";
6
+ import { createHash } from "node:crypto";
7
+ import {
8
+ PERCENT_CELL_WIDTH, TIME_CELL_INNER_WIDTH, SV_CELL_WIDTH,
9
+ FIVE_HOUR_MS, SEVEN_DAY_MS,
10
+ } from "./constants.mjs";
11
+ import { dim } from "./colors.mjs";
12
+
13
+ export async function readStdinJson() {
14
+ if (process.stdin.isTTY) return {};
15
+ return new Promise((resolve) => {
16
+ const timeout = setTimeout(() => {
17
+ process.stdin.destroy();
18
+ resolve({});
19
+ }, 200);
20
+ const chunks = [];
21
+ process.stdin.on("data", (chunk) => chunks.push(chunk));
22
+ process.stdin.on("end", () => {
23
+ clearTimeout(timeout);
24
+ const raw = chunks.join("").trim();
25
+ if (!raw) { resolve({}); return; }
26
+ try { resolve(JSON.parse(raw)); } catch { resolve({}); }
27
+ });
28
+ process.stdin.on("error", () => {
29
+ clearTimeout(timeout);
30
+ resolve({});
31
+ });
32
+ process.stdin.resume();
33
+ });
34
+ }
35
+
36
+ export function readJson(filePath, fallback) {
37
+ if (!existsSync(filePath)) return fallback;
38
+ try { return JSON.parse(readFileSync(filePath, "utf-8")); } catch { return fallback; }
39
+ }
40
+
41
+ export function writeJsonSafe(filePath, data) {
42
+ try {
43
+ mkdirSync(dirname(filePath), { recursive: true });
44
+ writeFileSync(filePath, JSON.stringify(data), { mode: 0o600 });
45
+ } catch { /* 쓰기 실패 무시 */ }
46
+ }
47
+
48
+ // .omc/ → .claude/cache/ 마이그레이션: 새 경로 우선, 없으면 레거시 읽고 복사
49
+ export function readJsonMigrate(newPath, legacyPath, fallback) {
50
+ const data = readJson(newPath, null);
51
+ if (data != null) return data;
52
+ const legacy = readJson(legacyPath, null);
53
+ if (legacy != null) { writeJsonSafe(newPath, legacy); return legacy; }
54
+ return fallback;
55
+ }
56
+
57
+ export function stripAnsi(text) {
58
+ return String(text).replace(/\x1b\[[0-9;]*m/g, "");
59
+ }
60
+
61
+ export function padAnsiRight(text, width) {
62
+ const len = stripAnsi(text).length;
63
+ if (len >= width) return text;
64
+ return text + " ".repeat(width - len);
65
+ }
66
+
67
+ export function padAnsiLeft(text, width) {
68
+ const len = stripAnsi(text).length;
69
+ if (len >= width) return text;
70
+ return " ".repeat(width - len) + text;
71
+ }
72
+
73
+ export function fitText(text, width) {
74
+ const t = String(text || "");
75
+ if (t.length <= width) return t;
76
+ if (width <= 1) return "…";
77
+ return `${t.slice(0, width - 1)}…`;
78
+ }
79
+
80
+ export function makeHash(text) {
81
+ return createHash("sha256").update(String(text || ""), "utf8").digest("hex").slice(0, 16);
82
+ }
83
+
84
+ export function clampPercent(value) {
85
+ const numeric = Number(value);
86
+ if (!Number.isFinite(numeric)) return 0;
87
+ return Math.max(0, Math.min(100, Math.round(numeric)));
88
+ }
89
+
90
+ export function formatPercentCell(value) {
91
+ return `${clampPercent(value)}%`.padStart(PERCENT_CELL_WIDTH, " ");
92
+ }
93
+
94
+ export function formatPlaceholderPercentCell() {
95
+ return "--%".padStart(PERCENT_CELL_WIDTH, " ");
96
+ }
97
+
98
+ export function normalizeTimeToken(value) {
99
+ const text = String(value || "n/a");
100
+ const hourMinute = text.match(/^(\d+)h(\d+)m$/);
101
+ if (hourMinute) {
102
+ return `${Number(hourMinute[1])}h${String(Number(hourMinute[2])).padStart(2, "0")}m`;
103
+ }
104
+ const dayHour = text.match(/^(\d+)d(\d+)h$/);
105
+ if (dayHour) {
106
+ return `${String(Number(dayHour[1])).padStart(2, "0")}d${String(Number(dayHour[2])).padStart(2, "0")}h`;
107
+ }
108
+ return text;
109
+ }
110
+
111
+ export function formatTimeCell(value) {
112
+ const text = normalizeTimeToken(value);
113
+ // 시간값(숫자 포함)은 0패딩, 비시간값(n/a 등)은 공백패딩
114
+ const padChar = /\d/.test(text) ? "0" : " ";
115
+ return `(${text.padStart(TIME_CELL_INNER_WIDTH, padChar)})`;
116
+ }
117
+
118
+ // 주간(d/h) 전용 — 최대 7d00h(5자)이므로 공백 불필요
119
+ export function formatTimeCellDH(value) {
120
+ const text = normalizeTimeToken(value);
121
+ return `(${text})`;
122
+ }
123
+
124
+ export function getCliArgValue(flag) {
125
+ const idx = process.argv.indexOf(flag);
126
+ if (idx < 0) return null;
127
+ return process.argv[idx + 1] || null;
128
+ }
129
+
130
+ export function formatDuration(ms) {
131
+ if (!Number.isFinite(ms) || ms <= 0) return "n/a";
132
+ const totalMinutes = Math.floor(ms / 60000);
133
+ const days = Math.floor(totalMinutes / (60 * 24));
134
+ const hours = Math.floor((totalMinutes % (60 * 24)) / 60);
135
+ const minutes = totalMinutes % 60;
136
+ if (days > 0) return hours > 0 ? `${days}d${hours}h` : `${days}d`;
137
+ if (hours > 0) return minutes > 0 ? `${hours}h${minutes}m` : `${hours}h`;
138
+ return `${minutes}m`;
139
+ }
140
+
141
+ export function formatTokenCount(n) {
142
+ if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
143
+ if (n >= 1_000) return `${Math.round(n / 1_000)}K`;
144
+ return String(n);
145
+ }
146
+
147
+ export function getContextPercent(stdin) {
148
+ const nativePercent = stdin?.context_window?.used_percentage;
149
+ if (typeof nativePercent === "number" && Number.isFinite(nativePercent)) return clampPercent(nativePercent);
150
+ const usage = stdin?.context_window?.current_usage || {};
151
+ const totalTokens = Number(usage.input_tokens || 0)
152
+ + Number(usage.cache_creation_input_tokens || 0)
153
+ + Number(usage.cache_read_input_tokens || 0);
154
+ const capacity = Number(stdin?.context_window?.context_window_size || 0);
155
+ if (!capacity || capacity <= 0) return 0;
156
+ return clampPercent((totalTokens / capacity) * 100);
157
+ }
158
+
159
+ // 과거 리셋 시간 → 다음 주기로 순환하여 미래 시점 반환
160
+ export function advanceToNextCycle(epochMs, cycleMs) {
161
+ const now = Date.now();
162
+ if (epochMs >= now || !cycleMs) return epochMs;
163
+ const elapsed = now - epochMs;
164
+ return epochMs + Math.ceil(elapsed / cycleMs) * cycleMs;
165
+ }
166
+
167
+ 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 "";
174
+ const totalMinutes = Math.floor(diffMs / 60000);
175
+ const totalHours = Math.floor(totalMinutes / 60);
176
+ const minutes = totalMinutes % 60;
177
+ return `${totalHours}h${String(minutes).padStart(2, "0")}m`;
178
+ }
179
+
180
+ 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();
184
+ }
185
+
186
+ 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 "";
193
+ const totalMinutes = Math.floor(diffMs / 60000);
194
+ const days = Math.floor(totalMinutes / (60 * 24));
195
+ const hours = Math.floor((totalMinutes % (60 * 24)) / 60);
196
+ return `${String(days).padStart(2, "0")}d${String(hours).padStart(2, "0")}h`;
197
+ }
198
+
199
+ export function calcCooldownLeftSeconds(isoDatetime) {
200
+ if (!isoDatetime) return 0;
201
+ const cooldownMs = new Date(isoDatetime).getTime() - Date.now();
202
+ if (!Number.isFinite(cooldownMs) || cooldownMs <= 0) return 0;
203
+ return Math.ceil(cooldownMs / 1000);
204
+ }
205
+
206
+ export function getProviderAccountId(provider, accountsConfig, accountsState) {
207
+ const providerState = accountsState?.providers?.[provider] || {};
208
+ const selectedId = providerState.last_selected_id;
209
+ if (selectedId) return selectedId;
210
+ const providerConfig = accountsConfig?.providers?.[provider] || [];
211
+ return providerConfig[0]?.id || `${provider}-main`;
212
+ }
213
+
214
+ // JWT base64 디코딩 공통 헬퍼
215
+ export function decodeJwtEmail(idToken) {
216
+ if (!idToken) return null;
217
+ const parts = idToken.split(".");
218
+ if (parts.length < 2) return null;
219
+ let payload = parts[1].replace(/-/g, "+").replace(/_/g, "/");
220
+ while (payload.length % 4) payload += "=";
221
+ try {
222
+ const decoded = JSON.parse(Buffer.from(payload, "base64").toString("utf-8"));
223
+ return decoded.email || null;
224
+ } catch { return null; }
225
+ }
226
+
227
+ // HTTPS POST (타임아웃 포함) — https 모듈은 호출자가 주입
228
+ export function createHttpsPost(https, timeoutMs) {
229
+ return function httpsPost(url, body, accessToken) {
230
+ return new Promise((resolve) => {
231
+ const urlObj = new URL(url);
232
+ const data = JSON.stringify(body);
233
+ const req = https.request({
234
+ hostname: urlObj.hostname,
235
+ path: urlObj.pathname + urlObj.search,
236
+ method: "POST",
237
+ headers: {
238
+ "Content-Type": "application/json",
239
+ "Authorization": `Bearer ${accessToken}`,
240
+ "Content-Length": Buffer.byteLength(data),
241
+ },
242
+ timeout: timeoutMs,
243
+ }, (res) => {
244
+ const chunks = [];
245
+ res.on("data", (c) => chunks.push(c));
246
+ res.on("end", () => {
247
+ try { resolve(JSON.parse(Buffer.concat(chunks).toString())); }
248
+ catch { resolve(null); }
249
+ });
250
+ });
251
+ req.on("error", () => resolve(null));
252
+ req.on("timeout", () => { req.destroy(); resolve(null); });
253
+ req.write(data);
254
+ req.end();
255
+ });
256
+ };
257
+ }
258
+
259
+ // sv 퍼센트 포맷 (1000+ → k 표기, 5자 고정폭)
260
+ export function formatSvPct(value) {
261
+ if (value == null) return "--%".padStart(SV_CELL_WIDTH);
262
+ if (value >= 10000) return `${Math.round(value / 1000)}k%`.padStart(SV_CELL_WIDTH);
263
+ if (value >= 1000) return `${(value / 1000).toFixed(1)}k%`.padStart(SV_CELL_WIDTH);
264
+ return `${value}%`.padStart(SV_CELL_WIDTH);
265
+ }
266
+
267
+ export function formatSavings(dollars) {
268
+ if (dollars >= 100) return `$${Math.round(dollars)}`;
269
+ if (dollars >= 10) return `$${dollars.toFixed(1)}`;
270
+ return `$${dollars.toFixed(2)}`;
271
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triflux",
3
- "version": "7.1.4",
3
+ "version": "7.2.2",
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": {
@@ -20,7 +20,6 @@
20
20
  "hooks",
21
21
  "hud",
22
22
  ".claude-plugin",
23
- ".mcp.json",
24
23
  "README.md",
25
24
  "README.ko.md",
26
25
  "LICENSE"