triflux 3.3.0-dev.8 → 4.0.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.
Files changed (91) hide show
  1. package/README.ko.md +108 -199
  2. package/README.md +108 -199
  3. package/bin/triflux.mjs +2415 -1762
  4. package/hooks/keyword-rules.json +361 -354
  5. package/hooks/pipeline-stop.mjs +5 -2
  6. package/hub/assign-callbacks.mjs +136 -136
  7. package/hub/bridge.mjs +734 -684
  8. package/hub/delegator/contracts.mjs +38 -38
  9. package/hub/delegator/index.mjs +14 -14
  10. package/hub/delegator/schema/delegator-tools.schema.json +250 -250
  11. package/hub/delegator/service.mjs +302 -118
  12. package/hub/delegator/tool-definitions.mjs +35 -35
  13. package/hub/hitl.mjs +67 -67
  14. package/hub/paths.mjs +28 -0
  15. package/hub/pipe.mjs +589 -561
  16. package/hub/pipeline/state.mjs +23 -0
  17. package/hub/public/dashboard.html +349 -0
  18. package/hub/public/tray-icon.ico +0 -0
  19. package/hub/public/tray-icon.png +0 -0
  20. package/hub/router.mjs +782 -782
  21. package/hub/schema.sql +40 -40
  22. package/hub/server.mjs +810 -637
  23. package/hub/store.mjs +706 -706
  24. package/hub/team/cli/commands/attach.mjs +37 -0
  25. package/hub/team/cli/commands/control.mjs +43 -0
  26. package/hub/team/cli/commands/debug.mjs +74 -0
  27. package/hub/team/cli/commands/focus.mjs +53 -0
  28. package/hub/team/cli/commands/interrupt.mjs +36 -0
  29. package/hub/team/cli/commands/kill.mjs +37 -0
  30. package/hub/team/cli/commands/list.mjs +24 -0
  31. package/hub/team/cli/commands/send.mjs +37 -0
  32. package/hub/team/cli/commands/start/index.mjs +87 -0
  33. package/hub/team/cli/commands/start/parse-args.mjs +32 -0
  34. package/hub/team/cli/commands/start/start-in-process.mjs +40 -0
  35. package/hub/team/cli/commands/start/start-mux.mjs +73 -0
  36. package/hub/team/cli/commands/start/start-wt.mjs +69 -0
  37. package/hub/team/cli/commands/status.mjs +87 -0
  38. package/hub/team/cli/commands/stop.mjs +31 -0
  39. package/hub/team/cli/commands/task.mjs +30 -0
  40. package/hub/team/cli/commands/tasks.mjs +13 -0
  41. package/hub/team/{cli.mjs → cli/help.mjs} +38 -99
  42. package/hub/team/cli/index.mjs +39 -0
  43. package/hub/team/cli/manifest.mjs +28 -0
  44. package/hub/team/cli/render.mjs +30 -0
  45. package/hub/team/cli/services/attach-fallback.mjs +54 -0
  46. package/hub/team/cli/services/hub-client.mjs +171 -0
  47. package/hub/team/cli/services/member-selector.mjs +30 -0
  48. package/hub/team/cli/services/native-control.mjs +115 -0
  49. package/hub/team/cli/services/runtime-mode.mjs +60 -0
  50. package/hub/team/cli/services/state-store.mjs +34 -0
  51. package/hub/team/cli/services/task-model.mjs +30 -0
  52. package/hub/team/native-supervisor.mjs +69 -63
  53. package/hub/team/native.mjs +367 -367
  54. package/hub/team/nativeProxy.mjs +217 -173
  55. package/hub/team/pane.mjs +149 -149
  56. package/hub/team/psmux.mjs +946 -946
  57. package/hub/team/session.mjs +608 -608
  58. package/hub/team/staleState.mjs +369 -299
  59. package/hub/tools.mjs +107 -107
  60. package/hub/tray.mjs +332 -0
  61. package/hub/workers/claude-worker.mjs +446 -446
  62. package/hub/workers/codex-mcp.mjs +414 -414
  63. package/hub/workers/delegator-mcp.mjs +1045 -1045
  64. package/hub/workers/factory.mjs +21 -21
  65. package/hub/workers/gemini-worker.mjs +349 -349
  66. package/hub/workers/interface.mjs +41 -41
  67. package/package.json +61 -60
  68. package/scripts/__tests__/keyword-detector.test.mjs +234 -234
  69. package/scripts/hub-ensure.mjs +102 -101
  70. package/scripts/keyword-detector.mjs +272 -272
  71. package/scripts/keyword-rules-expander.mjs +521 -521
  72. package/scripts/lib/keyword-rules.mjs +168 -168
  73. package/scripts/lib/mcp-filter.mjs +642 -642
  74. package/scripts/lib/mcp-server-catalog.mjs +118 -118
  75. package/scripts/mcp-check.mjs +126 -126
  76. package/scripts/preflight-cache.mjs +19 -0
  77. package/scripts/run.cjs +62 -62
  78. package/scripts/setup.mjs +68 -31
  79. package/scripts/test-tfx-route-no-claude-native.mjs +57 -57
  80. package/scripts/tfx-route-worker.mjs +161 -161
  81. package/scripts/tfx-route.sh +1360 -1326
  82. package/skills/tfx-auto/SKILL.md +196 -196
  83. package/skills/tfx-auto-codex/SKILL.md +77 -77
  84. package/skills/tfx-multi/SKILL.md +378 -378
  85. package/hub/team/cli-team-common.mjs +0 -348
  86. package/hub/team/cli-team-control.mjs +0 -393
  87. package/hub/team/cli-team-start.mjs +0 -516
  88. package/hub/team/cli-team-status.mjs +0 -283
  89. package/skills/auto-verify/SKILL.md +0 -145
  90. package/skills/manage-skills/SKILL.md +0 -192
  91. package/skills/verify-implementation/SKILL.md +0 -138
@@ -3,6 +3,29 @@
3
3
  // store.mjs의 기존 SQLite 연결(db)을 활용한다.
4
4
  // pipeline_state 테이블은 schema.sql에 정의.
5
5
 
6
+ import { join } from 'node:path';
7
+
8
+ import { TFX_STATE_DIR, ensureTfxDirs } from '../paths.mjs';
9
+
10
+ /**
11
+ * 파이프라인 상태 DB 경로를 계산한다.
12
+ * @param {string} baseDir
13
+ * @returns {string}
14
+ */
15
+ export function getPipelineStateDbPath(baseDir) {
16
+ return join(baseDir, TFX_STATE_DIR, 'state.db');
17
+ }
18
+
19
+ /**
20
+ * 파이프라인 상태 DB 경로와 .tfx 디렉토리를 준비한다.
21
+ * @param {string} baseDir
22
+ * @returns {string}
23
+ */
24
+ export function ensurePipelineStateDbPath(baseDir) {
25
+ ensureTfxDirs(baseDir);
26
+ return getPipelineStateDbPath(baseDir);
27
+ }
28
+
6
29
  /**
7
30
  * pipeline_state 테이블 초기화 (store.db에 없으면 생성)
8
31
  * @param {object} db - better-sqlite3 인스턴스
@@ -0,0 +1,349 @@
1
+ <!DOCTYPE html>
2
+ <html lang="ko">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>triflux QoS 대시보드</title>
7
+ <style>
8
+ *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
9
+ body{background:#0d1117;color:#c9d1d9;font-family:system-ui,-apple-system,sans-serif;min-height:100vh;padding:16px}
10
+ .header{display:flex;align-items:center;justify-content:space-between;padding:12px 0;margin-bottom:16px;border-bottom:1px solid #30363d;max-width:1200px;margin-left:auto;margin-right:auto}
11
+ .header h1{font-size:1.4rem;font-weight:600}
12
+ .status-bar{display:flex;align-items:center;gap:12px;font-size:.85rem;color:#8b949e}
13
+ .status-dot{width:10px;height:10px;border-radius:50%;display:inline-block;transition:background .3s}
14
+ .status-dot.ok{background:#3fb950}
15
+ .status-dot.err{background:#f85149}
16
+ .grid{display:grid;grid-template-columns:1fr 1fr;gap:16px;max-width:1200px;margin:0 auto}
17
+ @media(max-width:768px){.grid{grid-template-columns:1fr}}
18
+ .card{background:#161b22;border:1px solid #30363d;border-radius:8px;padding:16px}
19
+ .card-title{font-size:.95rem;font-weight:600;margin-bottom:12px;color:#c9d1d9}
20
+ .sub{color:#8b949e;font-size:.8rem}
21
+
22
+ .gauge-wrap{display:flex;flex-direction:column;align-items:center;gap:8px}
23
+ .gauge-value{font-size:2.2rem;font-weight:700;margin-top:-36px}
24
+ .gauge-label{font-size:.85rem;color:#8b949e}
25
+
26
+ .quota-row{margin-bottom:14px}
27
+ .quota-row:last-child{margin-bottom:0}
28
+ .quota-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;font-size:.85rem}
29
+ .quota-header .cli-name{font-weight:600}
30
+ .quota-header .meta{color:#8b949e;font-size:.78rem}
31
+ .bar-row{display:flex;gap:6px;align-items:center;margin-bottom:4px}
32
+ .bar-row:last-child{margin-bottom:0}
33
+ .bar-row .bar-label{font-size:.72rem;color:#8b949e;width:56px;text-align:right;flex-shrink:0}
34
+ .bar-track{height:18px;background:#21262d;border-radius:4px;overflow:hidden;position:relative;flex:1}
35
+ .bar-fill{height:100%;border-radius:4px;transition:width .4s ease;display:flex;align-items:center;justify-content:flex-end;padding-right:6px;font-size:.72rem;font-weight:600;color:#0d1117;min-width:0}
36
+ .bar-pct-outer{font-size:.72rem;color:#8b949e;margin-left:6px;width:32px;flex-shrink:0}
37
+
38
+ .token-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}
39
+ .token-item{background:#0d1117;border:1px solid #30363d;border-radius:6px;padding:12px;text-align:center}
40
+ .token-item .label{font-size:.78rem;color:#8b949e;margin-bottom:4px}
41
+ .token-item .value{font-size:1.5rem;font-weight:700}
42
+ .token-item .calls{font-size:.78rem;color:#8b949e;margin-top:2px}
43
+
44
+ .chart-wrap{position:relative;width:100%;height:160px}
45
+ .chart-wrap canvas{width:100%!important;height:100%!important}
46
+ .chart-legend{display:flex;gap:14px;margin-top:8px;font-size:.78rem;color:#8b949e}
47
+ .legend-dot{width:8px;height:8px;border-radius:50%;display:inline-block;margin-right:4px;vertical-align:middle}
48
+
49
+ .placeholder{text-align:center;padding:24px;color:#8b949e;font-size:.85rem}
50
+ </style>
51
+ </head>
52
+ <body>
53
+
54
+ <div class="header">
55
+ <h1>&#9889; triflux QoS 대시보드</h1>
56
+ <div class="status-bar">
57
+ <span class="status-dot" id="statusDot"></span>
58
+ <span id="statusText">연결 중...</span>
59
+ <span id="lastUpdate" class="sub"></span>
60
+ </div>
61
+ </div>
62
+
63
+ <div class="grid">
64
+ <!-- AIMD 게이지 -->
65
+ <div class="card">
66
+ <div class="card-title">AIMD 동시 워커</div>
67
+ <div class="gauge-wrap">
68
+ <svg width="220" height="130" viewBox="0 0 220 130">
69
+ <path d="M20 120 A90 90 0 0 1 200 120" fill="none" stroke="#21262d" stroke-width="18" stroke-linecap="round"/>
70
+ <path id="gaugeArc" d="M20 120 A90 90 0 0 1 200 120" fill="none" stroke="#3fb950" stroke-width="18" stroke-linecap="round" stroke-dasharray="0 283"/>
71
+ <!-- 눈금 -->
72
+ <g id="gaugeTicks" fill="#8b949e" font-size="10" text-anchor="middle"></g>
73
+ </svg>
74
+ <div class="gauge-value" id="gaugeVal">-</div>
75
+ <div class="gauge-label" id="gaugeLabel">- / 10</div>
76
+ </div>
77
+ </div>
78
+
79
+ <!-- CLI 쿼터 바 -->
80
+ <div class="card">
81
+ <div class="card-title">CLI 쿼터 사용률</div>
82
+ <div id="quotaBars">
83
+ <div class="placeholder">데이터 대기 중...</div>
84
+ </div>
85
+ </div>
86
+
87
+ <!-- 호출 이력 차트 -->
88
+ <div class="card" style="grid-column:1/-1">
89
+ <div class="card-title">호출 이력 타임라인</div>
90
+ <div class="chart-wrap">
91
+ <canvas id="eventsCanvas"></canvas>
92
+ </div>
93
+ <div class="chart-legend">
94
+ <span><span class="legend-dot" style="background:#3fb950"></span>성공</span>
95
+ <span><span class="legend-dot" style="background:#f85149"></span>실패</span>
96
+ <span><span class="legend-dot" style="background:#d29922"></span>타임아웃</span>
97
+ </div>
98
+ </div>
99
+
100
+ <!-- 토큰 누적 -->
101
+ <div class="card" style="grid-column:1/-1">
102
+ <div class="card-title">토큰 누적 현황</div>
103
+ <div class="token-grid" id="tokenCards">
104
+ <div class="placeholder" style="grid-column:1/-1">데이터 대기 중...</div>
105
+ </div>
106
+ </div>
107
+ </div>
108
+
109
+ <script>
110
+ (function() {
111
+ var POLL = 5000;
112
+ var MAX_W = 10;
113
+ var ARC_TOTAL = 283;
114
+
115
+ var $dot = document.getElementById('statusDot');
116
+ var $stxt = document.getElementById('statusText');
117
+ var $time = document.getElementById('lastUpdate');
118
+ var $arc = document.getElementById('gaugeArc');
119
+ var $gval = document.getElementById('gaugeVal');
120
+ var $glab = document.getElementById('gaugeLabel');
121
+ var $quota = document.getElementById('quotaBars');
122
+ var $tokens = document.getElementById('tokenCards');
123
+ var canvas = document.getElementById('eventsCanvas');
124
+ var cx = canvas.getContext('2d');
125
+ var cachedEvents = [];
126
+
127
+ function status(ok) {
128
+ $dot.className = 'status-dot ' + (ok ? 'ok' : 'err');
129
+ $stxt.textContent = ok ? '연결됨' : '연결 실패';
130
+ }
131
+
132
+ function ts(d) {
133
+ return d.toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
134
+ }
135
+
136
+ function fmtTok(n) {
137
+ if (n == null) return '-';
138
+ if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
139
+ if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K';
140
+ return String(n);
141
+ }
142
+
143
+ function barCol(p) {
144
+ if (p >= 80) return '#f85149';
145
+ if (p >= 50) return '#d29922';
146
+ return '#3fb950';
147
+ }
148
+
149
+ function remain(rt) {
150
+ if (!rt) return '';
151
+ var ms = new Date(rt) - Date.now();
152
+ if (ms <= 0) return '만료됨';
153
+ var h = Math.floor(ms / 3600000);
154
+ var m = Math.floor((ms % 3600000) / 60000);
155
+ return h > 0 ? h + '시간 ' + m + '분' : m + '분';
156
+ }
157
+
158
+ /* ── 게이지 ── */
159
+ function gauge(val) {
160
+ val = Math.max(0, Math.min(MAX_W, val || 0));
161
+ var r = val / MAX_W;
162
+ $arc.setAttribute('stroke-dasharray', (r * ARC_TOTAL) + ' ' + ARC_TOTAL);
163
+ $arc.setAttribute('stroke', r > 0.8 ? '#f85149' : r > 0.5 ? '#d29922' : '#3fb950');
164
+ $gval.textContent = val;
165
+ $glab.textContent = val + ' / ' + MAX_W;
166
+ }
167
+
168
+ /* ── 쿼터 ── */
169
+ function makeBar(label, pct) {
170
+ pct = Math.min(100, Math.max(0, Math.round(pct)));
171
+ var c = barCol(pct);
172
+ var inner = pct > 10 ? pct + '%' : '';
173
+ var outer = pct <= 10 ? pct + '%' : '';
174
+ return '<div class="bar-row">' +
175
+ '<span class="bar-label">' + label + '</span>' +
176
+ '<div class="bar-track"><div class="bar-fill" style="width:' + pct + '%;background:' + c + '">' + inner + '</div></div>' +
177
+ '<span class="bar-pct-outer">' + outer + '</span>' +
178
+ '</div>';
179
+ }
180
+
181
+ function quotaBlock(name, color, bars, resetTime) {
182
+ var rem = remain(resetTime);
183
+ var h = '<div class="quota-row"><div class="quota-header">' +
184
+ '<span class="cli-name" style="color:' + color + '">' + name + '</span>';
185
+ if (rem) h += '<span class="meta">리셋: ' + rem + '</span>';
186
+ h += '</div>';
187
+ for (var i = 0; i < bars.length; i++) h += makeBar(bars[i][0], bars[i][1]);
188
+ h += '</div>';
189
+ return h;
190
+ }
191
+
192
+ function renderQuota(d) {
193
+ var s = '';
194
+
195
+ // Claude
196
+ var cl = d.claude && d.claude.data;
197
+ if (cl) {
198
+ s += quotaBlock('Claude (c)', '#d19a66', [
199
+ ['5시간', cl.fiveHourPercent || 0],
200
+ ['주간', cl.weeklyPercent || 0]
201
+ ], cl.resetTime);
202
+ }
203
+
204
+ // Codex
205
+ var cx2 = d.codex && d.codex.buckets && d.codex.buckets.codex;
206
+ if (cx2) {
207
+ s += quotaBlock('Codex (x)', '#ffffff', [
208
+ ['Primary', cx2.primary ? cx2.primary.used_percent || 0 : 0],
209
+ ['Secondary', cx2.secondary ? cx2.secondary.used_percent || 0 : 0]
210
+ ], cx2.primary && cx2.primary.resetTime);
211
+ }
212
+
213
+ // Gemini
214
+ var gm = d.gemini && d.gemini.buckets;
215
+ if (gm && gm.length > 0) {
216
+ var frac = gm[0].remainingFraction;
217
+ s += quotaBlock('Gemini (g)', '#61afef', [
218
+ ['사용률', frac != null ? Math.round((1 - frac) * 100) : 0]
219
+ ], gm[0].resetTime);
220
+ }
221
+
222
+ $quota.innerHTML = s || '<div class="placeholder">쿼터 데이터 없음</div>';
223
+ }
224
+
225
+ /* ── 토큰 ── */
226
+ function renderTokens(acc) {
227
+ if (!acc || Object.keys(acc).length === 0) {
228
+ $tokens.innerHTML = '<div class="placeholder" style="grid-column:1/-1">누적 데이터 없음</div>';
229
+ return;
230
+ }
231
+ var h = '';
232
+ var keys = Object.keys(acc);
233
+ for (var i = 0; i < keys.length; i++) {
234
+ var k = keys[i], v = acc[k];
235
+ if (!v) continue;
236
+ h += '<div class="token-item">' +
237
+ '<div class="label">' + k.toUpperCase() + '</div>' +
238
+ '<div class="value">' + fmtTok(v.tokens) + '</div>' +
239
+ '<div class="calls">' + (v.calls || 0) + '회 호출</div>' +
240
+ '</div>';
241
+ }
242
+ $tokens.innerHTML = h || '<div class="placeholder" style="grid-column:1/-1">누적 데이터 없음</div>';
243
+ }
244
+
245
+ /* ── 이벤트 차트 ── */
246
+ var colorMap = { success: '#3fb950', failed: '#f85149', fail: '#f85149', error: '#f85149', timeout: '#d29922' };
247
+
248
+ function drawEvents(evts) {
249
+ var dpr = window.devicePixelRatio || 1;
250
+ var rect = canvas.parentElement.getBoundingClientRect();
251
+ var w = rect.width, h = rect.height;
252
+ canvas.width = w * dpr;
253
+ canvas.height = h * dpr;
254
+ cx.setTransform(dpr, 0, 0, dpr, 0, 0);
255
+ cx.clearRect(0, 0, w, h);
256
+
257
+ if (!evts || evts.length === 0) {
258
+ cx.fillStyle = '#8b949e';
259
+ cx.font = '13px system-ui';
260
+ cx.textAlign = 'center';
261
+ cx.fillText('이벤트 데이터 없음', w / 2, h / 2);
262
+ return;
263
+ }
264
+
265
+ var pad = { l: 50, r: 16, t: 12, b: 28 };
266
+ var cw = w - pad.l - pad.r;
267
+ var ch = h - pad.t - pad.b;
268
+
269
+ var times = [];
270
+ for (var i = 0; i < evts.length; i++) {
271
+ times.push(new Date(evts[i].timestamp || evts[i].time || evts[i].t).getTime());
272
+ }
273
+ var tMin = Math.min.apply(null, times);
274
+ var tMax = Math.max.apply(null, times);
275
+ if (tMax === tMin) tMax = tMin + 60000;
276
+
277
+ // X축 라인
278
+ cx.strokeStyle = '#30363d';
279
+ cx.lineWidth = 1;
280
+ cx.beginPath();
281
+ cx.moveTo(pad.l, pad.t + ch);
282
+ cx.lineTo(pad.l + cw, pad.t + ch);
283
+ cx.stroke();
284
+
285
+ // X축 라벨
286
+ cx.fillStyle = '#8b949e';
287
+ cx.font = '11px system-ui';
288
+ cx.textAlign = 'center';
289
+ var nL = Math.min(6, evts.length);
290
+ for (var j = 0; j < nL; j++) {
291
+ var tp = tMin + (tMax - tMin) * j / (nL - 1 || 1);
292
+ var xp = pad.l + cw * (tp - tMin) / (tMax - tMin);
293
+ var dd = new Date(tp);
294
+ cx.fillText(dd.getHours() + ':' + String(dd.getMinutes()).padStart(2, '0'), xp, h - 6);
295
+ }
296
+
297
+ // 도트
298
+ var dr = Math.max(3, Math.min(6, cw / evts.length * 0.4));
299
+ for (var k = 0; k < evts.length; k++) {
300
+ var ev = evts[k];
301
+ var t = times[k];
302
+ var ex = pad.l + cw * (t - tMin) / (tMax - tMin);
303
+ // Y: 해시 분산 (겹침 방지)
304
+ var ey = pad.t + ch * 0.15 + (ch * 0.65) * ((k * 7 + 3) % 13) / 13;
305
+ var st = (ev.status || ev.result || 'success').toLowerCase();
306
+ cx.beginPath();
307
+ cx.arc(ex, ey, dr, 0, Math.PI * 2);
308
+ cx.fillStyle = colorMap[st] || '#8b949e';
309
+ cx.fill();
310
+ }
311
+ }
312
+
313
+ /* ── 폴링 ── */
314
+ function poll() {
315
+ fetch('/api/qos-stats')
316
+ .then(function(r) {
317
+ if (!r.ok) throw new Error('HTTP ' + r.status);
318
+ return r.json();
319
+ })
320
+ .then(function(data) {
321
+ status(true);
322
+ $time.textContent = ts(new Date());
323
+
324
+ gauge(data.aimd ? data.aimd.batchSize : 0);
325
+ renderQuota(data);
326
+
327
+ cachedEvents = (data.aimd && data.aimd.events) || [];
328
+ drawEvents(cachedEvents);
329
+
330
+ renderTokens(data.accumulator);
331
+ })
332
+ .catch(function(e) {
333
+ status(false);
334
+ $time.textContent = ts(new Date()) + ' (실패)';
335
+ });
336
+ }
337
+
338
+ var rTimer;
339
+ window.addEventListener('resize', function() {
340
+ clearTimeout(rTimer);
341
+ rTimer = setTimeout(function() { drawEvents(cachedEvents); }, 150);
342
+ });
343
+
344
+ poll();
345
+ setInterval(poll, POLL);
346
+ })();
347
+ </script>
348
+ </body>
349
+ </html>
Binary file
Binary file