triflux 10.9.9 → 10.9.11
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/hooks/keyword-rules.json +104 -38
- package/hub/account-broker.mjs +7 -0
- package/hub/cli-adapter-base.mjs +3 -0
- package/hub/public/dashboard.html +61 -0
- package/hub/server.mjs +153 -7
- package/package.json +1 -1
- package/scripts/tfx-route-worker.mjs +6 -0
- package/scripts/tfx-route.sh +125 -188
- package/scripts/wt-cli.mjs +57 -0
- package/skills/tfx-deep-analysis/SKILL.md +1 -7
- package/skills/tfx-deep-analysis/SKILL.md.tmpl +0 -1
package/hooks/keyword-rules.json
CHANGED
|
@@ -11,7 +11,12 @@
|
|
|
11
11
|
"skill": null,
|
|
12
12
|
"action": "suppress_all",
|
|
13
13
|
"priority": 0,
|
|
14
|
-
"supersedes": [
|
|
14
|
+
"supersedes": [
|
|
15
|
+
"tfx-multi",
|
|
16
|
+
"tfx-unified",
|
|
17
|
+
"tfx-codex",
|
|
18
|
+
"tfx-gemini"
|
|
19
|
+
],
|
|
15
20
|
"exclusive": true,
|
|
16
21
|
"state": null,
|
|
17
22
|
"mcp_route": null
|
|
@@ -57,7 +62,9 @@
|
|
|
57
62
|
],
|
|
58
63
|
"skill": "tfx-swarm",
|
|
59
64
|
"priority": 1,
|
|
60
|
-
"supersedes": [
|
|
65
|
+
"supersedes": [
|
|
66
|
+
"tfx-codex"
|
|
67
|
+
],
|
|
61
68
|
"exclusive": false,
|
|
62
69
|
"state": null,
|
|
63
70
|
"mcp_route": null
|
|
@@ -65,14 +72,38 @@
|
|
|
65
72
|
{
|
|
66
73
|
"id": "tfx-unified",
|
|
67
74
|
"patterns": [
|
|
68
|
-
{
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
{
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
{
|
|
76
|
+
"source": "\\btfx[\\s-]?auto\\b",
|
|
77
|
+
"flags": "i"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"source": "\\btfx[\\s-]?auto[\\s-]?codex\\b",
|
|
81
|
+
"flags": "i"
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"source": "(?:만들어|고쳐|구현해|짜줘|수정해|바꿔)",
|
|
85
|
+
"flags": "i"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"source": "(?:리뷰해|검토해|봐줘|괜찮아)",
|
|
89
|
+
"flags": "i"
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"source": "(?:테스트|검증|돌려봐|QA)",
|
|
93
|
+
"flags": "i"
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"source": "(?:분석해|계획|설계해)",
|
|
97
|
+
"flags": "i"
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"source": "(?:찾아봐|조사해|검색해)",
|
|
101
|
+
"flags": "i"
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"source": "(?:정리해|슬롭|클린업)",
|
|
105
|
+
"flags": "i"
|
|
106
|
+
},
|
|
76
107
|
{
|
|
77
108
|
"source": "\\b(?:implement|build|fix|review|test|plan|analyze)\\b",
|
|
78
109
|
"flags": "i"
|
|
@@ -80,7 +111,9 @@
|
|
|
80
111
|
],
|
|
81
112
|
"skill": "tfx-auto",
|
|
82
113
|
"priority": 2,
|
|
83
|
-
"supersedes": [
|
|
114
|
+
"supersedes": [
|
|
115
|
+
"tfx-auto-codex"
|
|
116
|
+
],
|
|
84
117
|
"exclusive": false,
|
|
85
118
|
"state": null,
|
|
86
119
|
"mcp_route": null
|
|
@@ -193,17 +226,32 @@
|
|
|
193
226
|
{
|
|
194
227
|
"id": "wt-tab-route",
|
|
195
228
|
"patterns": [
|
|
196
|
-
{
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
{
|
|
229
|
+
{
|
|
230
|
+
"source": "(?:새\\s*탭|탭\\s*(?:새로|추가|생성|열|띄|파|만들))",
|
|
231
|
+
"flags": "i"
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
"source": "(?:패인|화면|pane)\\s*(?:분할|나눠|스플릿|split)",
|
|
235
|
+
"flags": "i"
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
"source": "\\bwt\\b\\s*(?:에\\s*)?(?:탭|tab|열|띄|새)",
|
|
239
|
+
"flags": "i"
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
"source": "(?:터미널|terminal)\\s*(?:탭|새\\s*탭)",
|
|
243
|
+
"flags": "i"
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
"source": "\\b(?:new\\s+tab|split\\s+pane|open\\s+(?:new\\s+)?tab)\\b",
|
|
247
|
+
"flags": "i"
|
|
248
|
+
}
|
|
201
249
|
],
|
|
202
|
-
"skill":
|
|
203
|
-
"action": "context_hint",
|
|
204
|
-
"hint": "WT 탭/패인 생성 요청입니다. wt.exe 직접 호출은 safety-guard가 차단합니다.\n\nhub/team/wt-manager.mjs의 createWtManager() 팩토리로 인스턴스를 만든 뒤 API를 호출하세요:\n- wt.createTab({ title, command, profile, cwd }) — 새 탭\n- wt.splitPane({ direction: 'H'|'V', title, command }) — 패인 분할\n- wt.applySplitLayout([{ title, command, direction }]) — 다중 배치\n- wt.closeTab(title) — 탭 닫기\n\n사용법:\nnode -e \"import('./hub/team/wt-manager.mjs').then(m => { const wt = m.createWtManager(); wt.createTab({ title: 'MyTab', command: 'pwsh' }); })\"",
|
|
250
|
+
"skill": "tfx-wt",
|
|
205
251
|
"priority": 1,
|
|
206
|
-
"supersedes": [
|
|
252
|
+
"supersedes": [
|
|
253
|
+
"tfx-unified"
|
|
254
|
+
],
|
|
207
255
|
"exclusive": false,
|
|
208
256
|
"state": null,
|
|
209
257
|
"mcp_route": null
|
|
@@ -211,14 +259,20 @@
|
|
|
211
259
|
{
|
|
212
260
|
"id": "wt-tab-rename",
|
|
213
261
|
"patterns": [
|
|
214
|
-
{
|
|
215
|
-
|
|
262
|
+
{
|
|
263
|
+
"source": "(?:탭\\s*(?:이름|제목)\\s*(?:바꿔|변경|rename))",
|
|
264
|
+
"flags": "i"
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
"source": "(?:rename\\s+tab|tab\\s+rename)",
|
|
268
|
+
"flags": "i"
|
|
269
|
+
}
|
|
216
270
|
],
|
|
217
|
-
"skill":
|
|
218
|
-
"action": "context_hint",
|
|
219
|
-
"hint": "WT 탭 이름 변경 요청입니다. wt.exe 직접 호출은 safety-guard가 차단합니다.\n\nhub/team/wt-manager.mjs의 createWtManager() 팩토리로 인스턴스를 만든 뒤 API를 호출하세요:\n- wt.renameTab({ oldTitle, newTitle }) — 탭 이름 변경\n\n사용법:\nnode -e \"import('./hub/team/wt-manager.mjs').then(m => { const wt = m.createWtManager(); wt.renameTab({ oldTitle: 'OldName', newTitle: 'NewName' }); })\"",
|
|
271
|
+
"skill": "tfx-wt",
|
|
220
272
|
"priority": 1,
|
|
221
|
-
"supersedes": [
|
|
273
|
+
"supersedes": [
|
|
274
|
+
"tfx-unified"
|
|
275
|
+
],
|
|
222
276
|
"exclusive": false,
|
|
223
277
|
"state": null,
|
|
224
278
|
"mcp_route": null
|
|
@@ -226,14 +280,20 @@
|
|
|
226
280
|
{
|
|
227
281
|
"id": "wt-tab-list",
|
|
228
282
|
"patterns": [
|
|
229
|
-
{
|
|
230
|
-
|
|
283
|
+
{
|
|
284
|
+
"source": "(?:탭\\s*(?:목록|리스트|열린|현재))",
|
|
285
|
+
"flags": "i"
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
"source": "(?:list\\s+tabs|tab\\s+list|열린\\s*탭)",
|
|
289
|
+
"flags": "i"
|
|
290
|
+
}
|
|
231
291
|
],
|
|
232
|
-
"skill":
|
|
233
|
-
"action": "context_hint",
|
|
234
|
-
"hint": "WT 탭 목록 조회 요청입니다. wt.exe 직접 호출은 safety-guard가 차단합니다.\n\nhub/team/wt-manager.mjs의 createWtManager() 팩토리로 인스턴스를 만든 뒤 API를 호출하세요:\n- wt.listTabs() — 현재 열린 탭 목록 (pid 파일 기반)\n\n사용법:\nnode -e \"import('./hub/team/wt-manager.mjs').then(m => { const wt = m.createWtManager(); console.log(wt.listTabs()); })\"",
|
|
292
|
+
"skill": "tfx-wt",
|
|
235
293
|
"priority": 1,
|
|
236
|
-
"supersedes": [
|
|
294
|
+
"supersedes": [
|
|
295
|
+
"tfx-unified"
|
|
296
|
+
],
|
|
237
297
|
"exclusive": false,
|
|
238
298
|
"state": null,
|
|
239
299
|
"mcp_route": null
|
|
@@ -241,14 +301,20 @@
|
|
|
241
301
|
{
|
|
242
302
|
"id": "wt-tab-close",
|
|
243
303
|
"patterns": [
|
|
244
|
-
{
|
|
245
|
-
|
|
304
|
+
{
|
|
305
|
+
"source": "(?:탭\\s*(?:닫아|닫기|종료|정리|close))",
|
|
306
|
+
"flags": "i"
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
"source": "(?:close\\s+tab|tab\\s+close|탭\\s*정리)",
|
|
310
|
+
"flags": "i"
|
|
311
|
+
}
|
|
246
312
|
],
|
|
247
|
-
"skill":
|
|
248
|
-
"action": "context_hint",
|
|
249
|
-
"hint": "WT 탭 닫기 요청입니다. wt.exe 직접 호출은 safety-guard가 차단합니다.\n\nhub/team/wt-manager.mjs의 createWtManager() 팩토리로 인스턴스를 만든 뒤 API를 호출하세요:\n- wt.closeTab(title) — 제목으로 탭 닫기\n- wt.closeStale({ olderThanMs, titlePattern }) — 오래된/패턴 매칭 탭 정리\n\n사용법:\nnode -e \"import('./hub/team/wt-manager.mjs').then(m => { const wt = m.createWtManager(); wt.closeTab('MyTab'); })\"",
|
|
313
|
+
"skill": "tfx-wt",
|
|
250
314
|
"priority": 1,
|
|
251
|
-
"supersedes": [
|
|
315
|
+
"supersedes": [
|
|
316
|
+
"tfx-unified"
|
|
317
|
+
],
|
|
252
318
|
"exclusive": false,
|
|
253
319
|
"state": null,
|
|
254
320
|
"mcp_route": null
|
package/hub/account-broker.mjs
CHANGED
|
@@ -354,6 +354,13 @@ class AccountBroker extends EventEmitter {
|
|
|
354
354
|
if (isHalfOpen) {
|
|
355
355
|
this.emit("circuitClose", { id: accountId });
|
|
356
356
|
}
|
|
357
|
+
} else if (result?.skipCircuit) {
|
|
358
|
+
// 인프라 에러 — circuit/cooldown에 카운트하지 않음
|
|
359
|
+
circuitUpdate = {
|
|
360
|
+
failureTimestamps: acct.failureTimestamps,
|
|
361
|
+
circuitOpenedAt: acct.circuitOpenedAt,
|
|
362
|
+
circuitTrialInFlight: acct.circuitTrialInFlight,
|
|
363
|
+
};
|
|
357
364
|
} else {
|
|
358
365
|
circuitUpdate = this.#recordCircuitFailure(acct, isHalfOpen, now);
|
|
359
366
|
if (circuitUpdate.circuitOpenedAt !== acct.circuitOpenedAt) {
|
package/hub/cli-adapter-base.mjs
CHANGED
|
@@ -282,6 +282,9 @@ export async function executeWithCircuitBroker({
|
|
|
282
282
|
const coolMs = parseRetryAfterMs(text, provider);
|
|
283
283
|
brokerMod.broker.markRateLimited(lease.id, coolMs);
|
|
284
284
|
brokerMod.broker.emit("cooldown", { id: lease.id, provider, coolMs, reason: "quota_exhausted" });
|
|
285
|
+
} else if (lastResult.failureMode === "crash") {
|
|
286
|
+
// 인프라 에러(모듈 누락, 서버 에러 등)는 계정 문제가 아님 → circuit/cooldown 건너뜀
|
|
287
|
+
brokerMod.broker.release(lease.id, { ok: false, skipCircuit: true });
|
|
285
288
|
} else {
|
|
286
289
|
brokerMod.broker.release(lease.id, { ok: false });
|
|
287
290
|
}
|
|
@@ -47,6 +47,17 @@ body{background:#0d1117;color:#c9d1d9;font-family:system-ui,-apple-system,sans-s
|
|
|
47
47
|
.legend-dot{width:8px;height:8px;border-radius:50%;display:inline-block;margin-right:4px;vertical-align:middle}
|
|
48
48
|
|
|
49
49
|
.placeholder{text-align:center;padding:24px;color:#8b949e;font-size:.85rem}
|
|
50
|
+
.btn{background:#21262d;border:1px solid #30363d;color:#c9d1d9;padding:4px 12px;border-radius:6px;font-size:.78rem;cursor:pointer;transition:background .2s}
|
|
51
|
+
.btn:hover{background:#30363d}
|
|
52
|
+
.btn:active{background:#3b414a}
|
|
53
|
+
.btn.ok{border-color:#3fb950;color:#3fb950}
|
|
54
|
+
.guide{display:none;max-width:1200px;margin:0 auto 16px;background:#161b22;border:1px solid #30363d;border-radius:8px;padding:16px;font-size:.82rem;line-height:1.7;color:#8b949e}
|
|
55
|
+
.guide.open{display:block}
|
|
56
|
+
.guide h3{color:#c9d1d9;font-size:.9rem;margin:12px 0 6px}
|
|
57
|
+
.guide h3:first-child{margin-top:0}
|
|
58
|
+
.guide code{background:#0d1117;padding:1px 5px;border-radius:3px;font-size:.78rem;color:#d19a66}
|
|
59
|
+
.guide ul{padding-left:18px;margin:4px 0}
|
|
60
|
+
.actions{display:flex;gap:8px;align-items:center}
|
|
50
61
|
</style>
|
|
51
62
|
</head>
|
|
52
63
|
<body>
|
|
@@ -57,9 +68,35 @@ body{background:#0d1117;color:#c9d1d9;font-family:system-ui,-apple-system,sans-s
|
|
|
57
68
|
<span class="status-dot" id="statusDot"></span>
|
|
58
69
|
<span id="statusText">연결 중...</span>
|
|
59
70
|
<span id="lastUpdate" class="sub"></span>
|
|
71
|
+
<div class="actions">
|
|
72
|
+
<button class="btn" id="guideBtn" title="도움말">?</button>
|
|
73
|
+
<button class="btn" id="reloadBtn" title="계정 브로커 리로드">Reload</button>
|
|
74
|
+
<a href="/broker/dashboard" target="_blank" class="btn" style="text-decoration:none">Broker</a>
|
|
75
|
+
</div>
|
|
60
76
|
</div>
|
|
61
77
|
</div>
|
|
62
78
|
|
|
79
|
+
<div class="guide" id="guidePanel">
|
|
80
|
+
<h3>AIMD 동시 워커</h3>
|
|
81
|
+
<p>AIMD(Additive Increase / Multiplicative Decrease) 알고리즘으로 최적 동시 워커 수를 자동 조절합니다. 게이지가 <code>10/10</code>에 가까우면 시스템이 포화 상태입니다.</p>
|
|
82
|
+
<h3>CLI 쿼터 사용률</h3>
|
|
83
|
+
<ul>
|
|
84
|
+
<li><strong>Claude</strong> — 5시간/주간 사용량. 80% 이상이면 빨간색 경고.</li>
|
|
85
|
+
<li><strong>Codex</strong> — Primary/Secondary 버킷별 사용률. 리셋 시간이 표시됩니다.</li>
|
|
86
|
+
<li><strong>Gemini</strong> — 남은 비율 기반 사용률. RPM/RPD 제한 포함.</li>
|
|
87
|
+
</ul>
|
|
88
|
+
<h3>호출 이력 타임라인</h3>
|
|
89
|
+
<p>최근 CLI 호출의 성공/실패/타임아웃을 시간순으로 표시합니다. <span style="color:#3fb950">녹색</span>=성공, <span style="color:#f85149">빨강</span>=실패, <span style="color:#d29922">노랑</span>=타임아웃.</p>
|
|
90
|
+
<h3>토큰 누적 현황</h3>
|
|
91
|
+
<p>CLI별 누적 토큰 사용량과 호출 횟수입니다. 세션 시작 이후의 합계입니다.</p>
|
|
92
|
+
<h3>조작</h3>
|
|
93
|
+
<ul>
|
|
94
|
+
<li><strong>Reload</strong> — <code>POST /broker/reload</code> 호출. accounts.json을 핫리로드합니다.</li>
|
|
95
|
+
<li><strong>Broker</strong> — 계정 브로커 대시보드(별도 페이지)를 엽니다.</li>
|
|
96
|
+
<li>데이터는 <strong>5초</strong>마다 자동 갱신됩니다.</li>
|
|
97
|
+
</ul>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
63
100
|
<div class="grid">
|
|
64
101
|
<!-- AIMD 게이지 -->
|
|
65
102
|
<div class="card">
|
|
@@ -347,6 +384,30 @@ body{background:#0d1117;color:#c9d1d9;font-family:system-ui,-apple-system,sans-s
|
|
|
347
384
|
rTimer = setTimeout(function() { drawEvents(cachedEvents); }, 150);
|
|
348
385
|
});
|
|
349
386
|
|
|
387
|
+
/* ── 가이드 토글 ── */
|
|
388
|
+
document.getElementById('guideBtn').onclick = function() {
|
|
389
|
+
var p = document.getElementById('guidePanel');
|
|
390
|
+
p.classList.toggle('open');
|
|
391
|
+
this.classList.toggle('ok');
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
/* ── 브로커 리로드 ── */
|
|
395
|
+
document.getElementById('reloadBtn').onclick = function() {
|
|
396
|
+
var btn = this;
|
|
397
|
+
btn.textContent = '...';
|
|
398
|
+
fetch('/broker/reload', { method: 'POST' })
|
|
399
|
+
.then(function(r) { return r.json(); })
|
|
400
|
+
.then(function(d) {
|
|
401
|
+
btn.textContent = d.ok ? 'OK (' + (d.accounts || 0) + ')' : 'Fail';
|
|
402
|
+
btn.classList.toggle('ok', d.ok);
|
|
403
|
+
setTimeout(function() { btn.textContent = 'Reload'; btn.classList.remove('ok'); }, 3000);
|
|
404
|
+
})
|
|
405
|
+
.catch(function() {
|
|
406
|
+
btn.textContent = 'Error';
|
|
407
|
+
setTimeout(function() { btn.textContent = 'Reload'; }, 3000);
|
|
408
|
+
});
|
|
409
|
+
};
|
|
410
|
+
|
|
350
411
|
poll();
|
|
351
412
|
setInterval(poll, POLL);
|
|
352
413
|
})();
|
package/hub/server.mjs
CHANGED
|
@@ -719,6 +719,16 @@ export async function startHub({
|
|
|
719
719
|
return;
|
|
720
720
|
}
|
|
721
721
|
|
|
722
|
+
if (path === "/broker/quota-refresh" && (req.method === "POST" || req.method === "GET")) {
|
|
723
|
+
try {
|
|
724
|
+
const results = await refreshAllAccountQuotas();
|
|
725
|
+
return writeJson(res, 200, { ok: true, results, ts: Date.now() });
|
|
726
|
+
} catch (err) {
|
|
727
|
+
hubLog.error({ err: String(err?.message || err) }, "broker.quota_refresh_error");
|
|
728
|
+
return writeJson(res, 200, { ok: false, error: String(err?.message || err) });
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
722
732
|
if (path === "/broker/reload" && req.method === "POST") {
|
|
723
733
|
const result = reloadBroker();
|
|
724
734
|
if (!result.ok) {
|
|
@@ -1667,17 +1677,70 @@ function cleanupStaleSpawnSessions(log) {
|
|
|
1667
1677
|
return killed;
|
|
1668
1678
|
}
|
|
1669
1679
|
|
|
1680
|
+
// ── Quota check for all accounts ───────────────────────────────
|
|
1681
|
+
|
|
1682
|
+
const QUOTA_CACHE_PATH = join(CACHE_DIR, "broker-quota-cache.json");
|
|
1683
|
+
|
|
1684
|
+
async function checkSingleAccountQuota(acct) {
|
|
1685
|
+
const authPath = join(PID_DIR, acct.authFile);
|
|
1686
|
+
if (!existsSync(authPath)) return { id: acct.id, status: "no_auth" };
|
|
1687
|
+
try {
|
|
1688
|
+
const auth = JSON.parse(readFileSync(authPath, "utf8"));
|
|
1689
|
+
if (acct.provider === "codex") {
|
|
1690
|
+
const token = auth.tokens?.access_token || auth.OPENAI_API_KEY || "";
|
|
1691
|
+
if (!token) return { id: acct.id, status: "no_token" };
|
|
1692
|
+
const r = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
1693
|
+
method: "POST", signal: AbortSignal.timeout(8000),
|
|
1694
|
+
headers: { Authorization: "Bearer " + token, "Content-Type": "application/json" },
|
|
1695
|
+
body: JSON.stringify({ model: "gpt-4.1-nano", messages: [{ role: "user", content: "hi" }], max_tokens: 1 }),
|
|
1696
|
+
});
|
|
1697
|
+
const hdrs = Object.fromEntries([...r.headers.entries()].filter(([k]) => /ratelimit/i.test(k)));
|
|
1698
|
+
if (r.status === 429) return { id: acct.id, status: "quota_hit", http: 429, headers: hdrs };
|
|
1699
|
+
if (r.status === 401) {
|
|
1700
|
+
// 401이어도 ratelimit 헤더가 있으면 쿼터 정보 추출 가능
|
|
1701
|
+
return { id: acct.id, status: "auth_error", http: 401, headers: hdrs };
|
|
1702
|
+
}
|
|
1703
|
+
return { id: acct.id, status: r.ok ? "ok" : "error", http: r.status, headers: hdrs };
|
|
1704
|
+
}
|
|
1705
|
+
// gemini — OAuth token refresh needed for accurate check
|
|
1706
|
+
return { id: acct.id, status: "oauth_check_needed" };
|
|
1707
|
+
} catch (e) {
|
|
1708
|
+
return { id: acct.id, status: "error", message: e.message?.substring(0, 60) };
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
async function refreshAllAccountQuotas() {
|
|
1713
|
+
const snap = brokerInstance?.snapshot() || [];
|
|
1714
|
+
const checks = snap.filter(a => a.authFile).map(a => checkSingleAccountQuota(a));
|
|
1715
|
+
const results = await Promise.all(checks);
|
|
1716
|
+
// 캐시 저장
|
|
1717
|
+
try {
|
|
1718
|
+
writeFileSync(QUOTA_CACHE_PATH, JSON.stringify({ ts: Date.now(), results }));
|
|
1719
|
+
} catch { /* best-effort */ }
|
|
1720
|
+
return results;
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
function loadQuotaCache() {
|
|
1724
|
+
try {
|
|
1725
|
+
if (!existsSync(QUOTA_CACHE_PATH)) return null;
|
|
1726
|
+
return JSON.parse(readFileSync(QUOTA_CACHE_PATH, "utf8"));
|
|
1727
|
+
} catch { return null; }
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1670
1730
|
// ── Broker Dashboard HTML ──────────────────────────────────────
|
|
1671
1731
|
|
|
1672
1732
|
function renderBrokerDashboard() {
|
|
1673
1733
|
return `<!DOCTYPE html><html lang="ko"><head><meta charset="utf-8">
|
|
1674
1734
|
<title>Account Broker Dashboard</title>
|
|
1675
|
-
<meta http-equiv="refresh" content="30">
|
|
1676
1735
|
<style>
|
|
1677
1736
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
1678
1737
|
body{background:#0a0a0b;color:#e8e8ec;font:14px/1.6 'SF Mono',Consolas,monospace;padding:24px}
|
|
1679
|
-
h1{font-size:18px;color:#a8b1ff;margin-bottom:
|
|
1680
|
-
.
|
|
1738
|
+
h1{font-size:18px;color:#a8b1ff;margin-bottom:8px}
|
|
1739
|
+
.toolbar{margin-bottom:16px;display:flex;gap:8px;align-items:center}
|
|
1740
|
+
.toolbar button{background:#1a1a2e;color:#a8b1ff;border:1px solid rgba(255,255,255,0.1);border-radius:4px;padding:4px 12px;cursor:pointer;font:12px inherit}
|
|
1741
|
+
.toolbar button:hover{background:#252547}
|
|
1742
|
+
.toolbar .status-text{font-size:11px;color:#555}
|
|
1743
|
+
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(380px,1fr));gap:12px}
|
|
1681
1744
|
.card{background:#141416;border:1px solid rgba(255,255,255,0.06);border-radius:8px;padding:16px}
|
|
1682
1745
|
.card.available{border-left:3px solid #34d399}
|
|
1683
1746
|
.card.busy{border-left:3px solid #fbbf24}
|
|
@@ -1690,16 +1753,75 @@ h1{font-size:18px;color:#a8b1ff;margin-bottom:16px}
|
|
|
1690
1753
|
.status.busy{background:#451a03;color:#fbbf24}
|
|
1691
1754
|
.status.cooldown{background:#450a0a;color:#f87171}
|
|
1692
1755
|
.status.circuit-open{background:#450a0a;color:#ef4444}
|
|
1693
|
-
.
|
|
1756
|
+
.gauge{margin-top:8px}
|
|
1757
|
+
.gauge-row{display:flex;align-items:center;gap:8px;margin:3px 0}
|
|
1758
|
+
.gauge-label{font-size:10px;color:#888;width:28px;text-align:right}
|
|
1759
|
+
.gauge-bar{flex:1;height:8px;background:#1a1a1e;border-radius:4px;overflow:hidden}
|
|
1760
|
+
.gauge-fill{height:100%;border-radius:4px;transition:width .3s}
|
|
1761
|
+
.gauge-fill.green{background:linear-gradient(90deg,#34d399,#059669)}
|
|
1762
|
+
.gauge-fill.yellow{background:linear-gradient(90deg,#fbbf24,#d97706)}
|
|
1763
|
+
.gauge-fill.red{background:linear-gradient(90deg,#f87171,#dc2626)}
|
|
1764
|
+
.gauge-pct{font-size:10px;color:#999;width:32px}
|
|
1765
|
+
.gauge-reset{font-size:9px;color:#555}
|
|
1766
|
+
.meta{margin-top:6px;font-size:11px;color:#666;line-height:1.8}
|
|
1694
1767
|
.meta b{color:#999}
|
|
1695
1768
|
.timer{color:#f87171;font-weight:600}
|
|
1696
|
-
.refresh{color:#555;font-size:11px;margin-top:16px}
|
|
1769
|
+
.refresh-bar{color:#555;font-size:11px;margin-top:16px}
|
|
1697
1770
|
</style></head><body>
|
|
1698
|
-
<h1
|
|
1771
|
+
<h1>Account Broker Dashboard</h1>
|
|
1772
|
+
<div class="toolbar">
|
|
1773
|
+
<button onclick="checkQuotas()">Check All Quotas</button>
|
|
1774
|
+
<button onclick="reloadBroker()">Reload Broker</button>
|
|
1775
|
+
<span id="toolbar-status" class="status-text"></span>
|
|
1776
|
+
</div>
|
|
1699
1777
|
<div id="grid" class="grid"></div>
|
|
1700
|
-
<p class="refresh">auto-refresh: 10s | <a href="/broker/snapshot" style="color:#666">JSON API</a></p>
|
|
1778
|
+
<p class="refresh-bar">auto-refresh: 10s | <a href="/broker/snapshot" style="color:#666">JSON</a> | <a href="/broker/quota-refresh" style="color:#666">Quota API</a></p>
|
|
1701
1779
|
<script>
|
|
1780
|
+
let quotaData={};
|
|
1702
1781
|
function fmt(ms){if(ms<=0)return'-';const s=Math.floor(ms/1000),m=Math.floor(s/60),h=Math.floor(m/60),d=Math.floor(h/24);if(d>0)return d+'d '+h%24+'h';if(h>0)return h+'h '+m%60+'m';if(m>0)return m+'m '+s%60+'s';return s+'s'}
|
|
1782
|
+
|
|
1783
|
+
function gaugeColor(pct){return pct<60?'green':pct<85?'yellow':'red'}
|
|
1784
|
+
|
|
1785
|
+
function renderGauge(label,pct,resetTs){
|
|
1786
|
+
if(pct==null)return '';
|
|
1787
|
+
const now=Date.now()/1000;
|
|
1788
|
+
const resetIn=resetTs>now?fmt((resetTs-now)*1000):'';
|
|
1789
|
+
return '<div class="gauge-row"><span class="gauge-label">'+label+'</span>'+
|
|
1790
|
+
'<div class="gauge-bar"><div class="gauge-fill '+gaugeColor(pct)+'" style="width:'+pct+'%"></div></div>'+
|
|
1791
|
+
'<span class="gauge-pct">'+pct+'%</span>'+
|
|
1792
|
+
(resetIn?'<span class="gauge-reset">reset '+resetIn+'</span>':'')+
|
|
1793
|
+
'</div>';
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
function renderQuotaGauges(acctId){
|
|
1797
|
+
const q=quotaData[acctId];
|
|
1798
|
+
if(!q)return '<div class="gauge"><div class="gauge-row"><span style="font-size:10px;color:#444">quota: click Check All Quotas</span></div></div>';
|
|
1799
|
+
if(q.status==='quota_hit')return '<div class="gauge"><div class="gauge-row"><span style="font-size:10px;color:#f87171">QUOTA EXHAUSTED</span></div></div>';
|
|
1800
|
+
if(q.status==='auth_error'||q.status==='no_token'||q.status==='oauth_check_needed'){
|
|
1801
|
+
// 401이어도 ratelimit 헤더가 있으면 표시
|
|
1802
|
+
const h=q.headers||{};
|
|
1803
|
+
const limReq=h['x-ratelimit-limit-requests'],remReq=h['x-ratelimit-remaining-requests'];
|
|
1804
|
+
const limTok=h['x-ratelimit-limit-tokens'],remTok=h['x-ratelimit-remaining-tokens'];
|
|
1805
|
+
if(limReq){
|
|
1806
|
+
const pctReq=Math.round((1-remReq/limReq)*100);
|
|
1807
|
+
const pctTok=limTok?Math.round((1-remTok/limTok)*100):null;
|
|
1808
|
+
return '<div class="gauge">'+renderGauge('req',pctReq,0)+(pctTok!=null?renderGauge('tok',pctTok,0):'')+'</div>';
|
|
1809
|
+
}
|
|
1810
|
+
return '<div class="gauge"><div class="gauge-row"><span style="font-size:10px;color:#fbbf24">'+(q.status==='oauth_check_needed'?'OAuth refresh needed':'auth: token refresh needed')+'</span></div></div>';
|
|
1811
|
+
}
|
|
1812
|
+
if(q.status==='ok'||q.status==='error'){
|
|
1813
|
+
const h=q.headers||{};
|
|
1814
|
+
const limReq=h['x-ratelimit-limit-requests'],remReq=h['x-ratelimit-remaining-requests'];
|
|
1815
|
+
const limTok=h['x-ratelimit-limit-tokens'],remTok=h['x-ratelimit-remaining-tokens'];
|
|
1816
|
+
const resetReq=h['x-ratelimit-reset-requests'],resetTok=h['x-ratelimit-reset-tokens'];
|
|
1817
|
+
if(!limReq)return '';
|
|
1818
|
+
const pctReq=Math.round((1-remReq/limReq)*100);
|
|
1819
|
+
const pctTok=limTok?Math.round((1-remTok/limTok)*100):null;
|
|
1820
|
+
return '<div class="gauge">'+renderGauge('req',pctReq,0)+(pctTok!=null?renderGauge('tok',pctTok,0):'')+'</div>';
|
|
1821
|
+
}
|
|
1822
|
+
return '';
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1703
1825
|
async function refresh(){
|
|
1704
1826
|
try{
|
|
1705
1827
|
const r=await fetch('/broker/snapshot');
|
|
@@ -1719,6 +1841,7 @@ async function refresh(){
|
|
|
1719
1841
|
'<div class="id">'+a.id+'</div>'+
|
|
1720
1842
|
'<span class="status '+st+'">'+sl+'</span>'+
|
|
1721
1843
|
(cd>0?'<span class="timer" style="margin-left:8px">'+fmt(cd)+' remaining</span>':'')+
|
|
1844
|
+
renderQuotaGauges(a.id)+
|
|
1722
1845
|
'<div class="meta">'+
|
|
1723
1846
|
'<b>Sessions:</b> '+a.totalSessions+
|
|
1724
1847
|
(a.lastUsedAt?' | <b>Last:</b> '+new Date(a.lastUsedAt).toLocaleTimeString():'')+
|
|
@@ -1728,6 +1851,29 @@ async function refresh(){
|
|
|
1728
1851
|
}).join('');
|
|
1729
1852
|
}catch(e){console.error('refresh failed',e)}
|
|
1730
1853
|
}
|
|
1854
|
+
|
|
1855
|
+
async function checkQuotas(){
|
|
1856
|
+
const el=document.getElementById('toolbar-status');
|
|
1857
|
+
el.textContent='checking quotas...';
|
|
1858
|
+
try{
|
|
1859
|
+
const r=await fetch('/broker/quota-refresh',{method:'POST'});
|
|
1860
|
+
const d=await r.json();
|
|
1861
|
+
if(d.ok){
|
|
1862
|
+
d.results.forEach(q=>{quotaData[q.id]=q});
|
|
1863
|
+
el.textContent='updated '+d.results.length+' accounts ('+new Date().toLocaleTimeString()+')';
|
|
1864
|
+
refresh();
|
|
1865
|
+
}else{el.textContent='error';}
|
|
1866
|
+
}catch(e){el.textContent='failed: '+e.message;}
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
async function reloadBroker(){
|
|
1870
|
+
await fetch('/broker/reload',{method:'POST'});
|
|
1871
|
+
document.getElementById('toolbar-status').textContent='broker reloaded';
|
|
1872
|
+
refresh();
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
// 캐시에서 초기 로드
|
|
1876
|
+
fetch('/broker/quota-refresh').catch(()=>{});
|
|
1731
1877
|
refresh();setInterval(refresh,10000);
|
|
1732
1878
|
</script></body></html>`;
|
|
1733
1879
|
}
|
package/package.json
CHANGED
|
@@ -51,6 +51,7 @@ if (!createWorker) {
|
|
|
51
51
|
function parseArgs(argv) {
|
|
52
52
|
const args = {
|
|
53
53
|
allowedMcpServerNames: [],
|
|
54
|
+
extraArgs: [],
|
|
54
55
|
mcpConfig: [],
|
|
55
56
|
};
|
|
56
57
|
|
|
@@ -94,6 +95,10 @@ function parseArgs(argv) {
|
|
|
94
95
|
args.allowedMcpServerNames.push(next);
|
|
95
96
|
index += 1;
|
|
96
97
|
break;
|
|
98
|
+
case "--extra-arg":
|
|
99
|
+
args.extraArgs.push(next);
|
|
100
|
+
index += 1;
|
|
101
|
+
break;
|
|
97
102
|
case "--mcp-config":
|
|
98
103
|
args.mcpConfig.push(next);
|
|
99
104
|
index += 1;
|
|
@@ -202,6 +207,7 @@ const worker = createWorker(args.type, {
|
|
|
202
207
|
permissionMode: args.permissionMode,
|
|
203
208
|
allowDangerouslySkipPermissions: args.allowDangerouslySkipPermissions,
|
|
204
209
|
allowedMcpServerNames: args.allowedMcpServerNames,
|
|
210
|
+
extraArgs: args.extraArgs,
|
|
205
211
|
mcpConfig:
|
|
206
212
|
args.type === "claude" && args.mcpConfig.length === 0
|
|
207
213
|
? resolveDefaultMcpConfig(args.cwd || process.cwd())
|
package/scripts/tfx-route.sh
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# tfx-route.sh v2.
|
|
2
|
+
# tfx-route.sh v2.7 — CLI 라우팅 래퍼 (triflux)
|
|
3
3
|
#
|
|
4
4
|
# v1.x: cli-route.sh (jq+python3+node 혼재, 동기 후처리 ~1s)
|
|
5
5
|
# v2.0: tfx-route.sh 리네임
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
# - Gemini health check 지수 백오프 (30×1s → 5×exp)
|
|
10
10
|
# - 컨텍스트 파일 5번째 인자 지원
|
|
11
11
|
#
|
|
12
|
-
VERSION="2.
|
|
12
|
+
VERSION="2.7"
|
|
13
13
|
#
|
|
14
14
|
# 사용법:
|
|
15
15
|
# tfx-route.sh <agent_type> <prompt> [mcp_profile] [timeout_sec] [context_file]
|
|
@@ -220,10 +220,13 @@ CODEX_BIN="${CODEX_BIN:-$(command -v codex 2>/dev/null || echo codex)}"
|
|
|
220
220
|
GEMINI_BIN="${GEMINI_BIN:-$(command -v gemini 2>/dev/null || echo gemini)}"
|
|
221
221
|
CLAUDE_BIN="${CLAUDE_BIN:-$(command -v claude 2>/dev/null || echo claude)}"
|
|
222
222
|
GEMINI_BIN_ARGS_JSON="${GEMINI_BIN_ARGS_JSON:-[]}"
|
|
223
|
+
# ── Gemini 확장 플래그 (issue #64) ──
|
|
224
|
+
TFX_GEMINI_EXTENSIONS="${TFX_GEMINI_EXTENSIONS:-}"
|
|
225
|
+
TFX_GEMINI_FLAGS="${TFX_GEMINI_FLAGS:-}"
|
|
223
226
|
CLAUDE_BIN_ARGS_JSON="${CLAUDE_BIN_ARGS_JSON:-[]}"
|
|
224
227
|
|
|
225
228
|
# ── Gemini 프로필 경로 (Codex config.toml 대칭) ──
|
|
226
|
-
GEMINI_PROFILES_PATH="${GEMINI_PROFILES_PATH:-$
|
|
229
|
+
GEMINI_PROFILES_PATH="${GEMINI_PROFILES_PATH:-${HOME}/.gemini/triflux-profiles.json}"
|
|
227
230
|
|
|
228
231
|
# ── 상수 ──
|
|
229
232
|
MAX_STDOUT_BYTES=51200 # 50KB — Claude 컨텍스트 절약
|
|
@@ -250,7 +253,6 @@ unset _tfx_breadcrumb
|
|
|
250
253
|
|
|
251
254
|
# fallback 시 원래 에이전트 정보 보존
|
|
252
255
|
ORIGINAL_AGENT=""
|
|
253
|
-
ORIGINAL_CLI_ARGS=""
|
|
254
256
|
|
|
255
257
|
# JSON 문자열 이스케이프:
|
|
256
258
|
# - "\", """ 필수 이스케이프
|
|
@@ -340,33 +342,32 @@ normalize_script_path() {
|
|
|
340
342
|
printf '%s\n' "$path"
|
|
341
343
|
}
|
|
342
344
|
|
|
343
|
-
# ──
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
345
|
+
# ── 스크립트 경로 해석 공통 인프라 ──
|
|
346
|
+
_tfx_script_dir=""
|
|
347
|
+
_get_script_dir() {
|
|
348
|
+
if [[ -z "$_tfx_script_dir" ]]; then
|
|
349
|
+
local ref; ref="$(normalize_script_path "${BASH_SOURCE[0]}")"
|
|
350
|
+
_tfx_script_dir="$(cd "$(dirname "$ref")" && pwd -P)"
|
|
348
351
|
fi
|
|
352
|
+
printf '%s\n' "$_tfx_script_dir"
|
|
353
|
+
}
|
|
349
354
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
local
|
|
353
|
-
[[ -n "$
|
|
354
|
-
|
|
355
|
-
"$script_dir/../hub/bridge.mjs"
|
|
356
|
-
"$script_dir/hub/bridge.mjs"
|
|
357
|
-
)
|
|
358
|
-
|
|
359
|
-
local candidate
|
|
360
|
-
for candidate in "${candidates[@]}"; do
|
|
361
|
-
if [[ -f "$candidate" ]]; then
|
|
362
|
-
printf '%s\n' "$candidate"
|
|
363
|
-
return 0
|
|
364
|
-
fi
|
|
365
|
-
done
|
|
366
|
-
|
|
355
|
+
# _resolve_script ENV_VAR_VALUE CANDIDATE... → 첫 번째 존재하는 파일 경로 반환
|
|
356
|
+
_resolve_script() {
|
|
357
|
+
local env_val="${1:-}"; shift
|
|
358
|
+
[[ -n "$env_val" && -f "$env_val" ]] && { printf '%s\n' "$env_val"; return 0; }
|
|
359
|
+
local c; for c in "$@"; do [[ -f "$c" ]] && { printf '%s\n' "$c"; return 0; }; done
|
|
367
360
|
return 1
|
|
368
361
|
}
|
|
369
362
|
|
|
363
|
+
# ── 팀 Hub Bridge 통신 ──
|
|
364
|
+
resolve_bridge_script() {
|
|
365
|
+
local sd; sd="$(_get_script_dir)"
|
|
366
|
+
_resolve_script "${TFX_BRIDGE_SCRIPT:-}" \
|
|
367
|
+
${TFX_PKG_ROOT:+"$TFX_PKG_ROOT/hub/bridge.mjs"} \
|
|
368
|
+
"$sd/../hub/bridge.mjs" "$sd/hub/bridge.mjs"
|
|
369
|
+
}
|
|
370
|
+
|
|
370
371
|
bridge_cli() {
|
|
371
372
|
if ! command -v "$NODE_BIN" &>/dev/null; then
|
|
372
373
|
return 127
|
|
@@ -401,14 +402,6 @@ bridge_json_stringify() {
|
|
|
401
402
|
shift || true
|
|
402
403
|
|
|
403
404
|
case "$mode" in
|
|
404
|
-
metadata-patch)
|
|
405
|
-
"$NODE_BIN" -e '
|
|
406
|
-
process.stdout.write(JSON.stringify({
|
|
407
|
-
result: process.argv[1] || "",
|
|
408
|
-
summary: process.argv[2] || "",
|
|
409
|
-
}));
|
|
410
|
-
' -- "${1:-}" "${2:-}"
|
|
411
|
-
;;
|
|
412
405
|
task-result)
|
|
413
406
|
"$NODE_BIN" -e '
|
|
414
407
|
process.stdout.write(JSON.stringify({
|
|
@@ -584,10 +577,13 @@ team_complete_task() {
|
|
|
584
577
|
# Claude 재로그인 시 Agent 래퍼가 죽어도 이 파일로 결과 수집 가능
|
|
585
578
|
local result_dir="${TFX_RESULT_DIR:-${HOME}/.claude/tfx-results/${TFX_TEAM_NAME}}"
|
|
586
579
|
if mkdir -p "$result_dir" 2>/dev/null; then
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
580
|
+
# 전체를 Node.js로 안전하게 stringify — 변수 직접 삽입 인젝션 방지
|
|
581
|
+
"$NODE_BIN" -e '
|
|
582
|
+
const [,taskId,agent,team,result,summary,ts] = process.argv;
|
|
583
|
+
process.stdout.write(JSON.stringify({taskId,agent,team,result,summary,timestamp:ts}));
|
|
584
|
+
' -- "$TFX_TEAM_TASK_ID" "$TFX_TEAM_AGENT_NAME" "$TFX_TEAM_NAME" "$result" "$summary_trimmed" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
585
|
+
> "${result_dir}/${TFX_TEAM_TASK_ID}.json" 2>/dev/null \
|
|
586
|
+
&& echo "[tfx-route] 결과 백업: ${result_dir}/${TFX_TEAM_TASK_ID}.json" >&2
|
|
591
587
|
fi
|
|
592
588
|
}
|
|
593
589
|
|
|
@@ -633,8 +629,9 @@ auto_reroute() {
|
|
|
633
629
|
local quota_marker="$TFX_TMP/tfx-quota-${failed_cli}-$(date +%Y%m%d)"
|
|
634
630
|
echo "$(date +%s)" >> "$quota_marker"
|
|
635
631
|
ORIGINAL_AGENT="$AGENT_TYPE"
|
|
636
|
-
ORIGINAL_CLI_ARGS="$CLI_ARGS"
|
|
637
632
|
export TFX_REROUTED_FROM="$CLI_TYPE"
|
|
633
|
+
# EXIT trap 정리 — exec는 현재 프로세스를 교체하므로 trap이 실행되지 않음
|
|
634
|
+
cleanup_workers
|
|
638
635
|
TFX_CLI_MODE="$target_cli" exec bash "${BASH_SOURCE[0]}" \
|
|
639
636
|
"$AGENT_TYPE" "$PROMPT" "$MCP_PROFILE" "$USER_TIMEOUT" "$CONTEXT_FILE"
|
|
640
637
|
}
|
|
@@ -816,7 +813,7 @@ route_agent() {
|
|
|
816
813
|
# ── 에이전트별 상세 설정 ──
|
|
817
814
|
case "$agent" in
|
|
818
815
|
# ─── 구현 레인 ───
|
|
819
|
-
executor)
|
|
816
|
+
executor|codex)
|
|
820
817
|
CLI_ARGS="exec --profile codex53_high ${codex_base}"
|
|
821
818
|
CLI_EFFORT="codex53_high"; DEFAULT_TIMEOUT=1080; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
|
|
822
819
|
build-fixer)
|
|
@@ -825,48 +822,33 @@ route_agent() {
|
|
|
825
822
|
debugger)
|
|
826
823
|
CLI_ARGS="exec --profile codex53_high ${codex_base}"
|
|
827
824
|
CLI_EFFORT="codex53_high"; DEFAULT_TIMEOUT=900; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
|
|
828
|
-
deep-executor)
|
|
829
|
-
CLI_ARGS="exec --profile gpt54_xhigh ${codex_base}"
|
|
830
|
-
CLI_EFFORT="gpt54_xhigh"; DEFAULT_TIMEOUT=3600; RUN_MODE="bg"; OPUS_OVERSIGHT="true" ;;
|
|
831
825
|
|
|
832
826
|
# ─── 설계/분석 레인 (5.4: 1M 컨텍스트, 에이전틱) ───
|
|
833
|
-
architect)
|
|
834
|
-
CLI_ARGS="exec --profile gpt54_xhigh ${codex_base}"
|
|
835
|
-
CLI_EFFORT="gpt54_xhigh"; DEFAULT_TIMEOUT=3600; RUN_MODE="bg"; OPUS_OVERSIGHT="true" ;;
|
|
836
|
-
planner)
|
|
837
|
-
CLI_ARGS="exec --profile gpt54_xhigh ${codex_base}"
|
|
838
|
-
CLI_EFFORT="gpt54_xhigh"; DEFAULT_TIMEOUT=3600; RUN_MODE="fg"; OPUS_OVERSIGHT="true" ;;
|
|
839
|
-
critic)
|
|
827
|
+
deep-executor|architect|critic)
|
|
840
828
|
CLI_ARGS="exec --profile gpt54_xhigh ${codex_base}"
|
|
841
829
|
CLI_EFFORT="gpt54_xhigh"; DEFAULT_TIMEOUT=3600; RUN_MODE="bg"; OPUS_OVERSIGHT="true" ;;
|
|
842
|
-
analyst)
|
|
830
|
+
planner|analyst)
|
|
843
831
|
CLI_ARGS="exec --profile gpt54_xhigh ${codex_base}"
|
|
844
832
|
CLI_EFFORT="gpt54_xhigh"; DEFAULT_TIMEOUT=3600; RUN_MODE="fg"; OPUS_OVERSIGHT="true" ;;
|
|
845
833
|
|
|
846
834
|
# ─── 리뷰 레인 (5.3-codex: SWE-Bench 72%) ───
|
|
847
|
-
code-reviewer)
|
|
835
|
+
code-reviewer|quality-reviewer)
|
|
848
836
|
CLI_ARGS="exec --profile codex53_high ${codex_base} review"
|
|
849
837
|
CLI_EFFORT="codex53_high"; DEFAULT_TIMEOUT=1800; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
|
|
850
838
|
security-reviewer)
|
|
851
839
|
CLI_ARGS="exec --profile codex53_high ${codex_base} review"
|
|
852
840
|
CLI_EFFORT="codex53_high"; DEFAULT_TIMEOUT=1800; RUN_MODE="bg"; OPUS_OVERSIGHT="true" ;;
|
|
853
|
-
quality-reviewer)
|
|
854
|
-
CLI_ARGS="exec --profile codex53_high ${codex_base} review"
|
|
855
|
-
CLI_EFFORT="codex53_high"; DEFAULT_TIMEOUT=1800; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
|
|
856
841
|
|
|
857
842
|
# ─── 리서치 레인 ───
|
|
858
|
-
scientist)
|
|
843
|
+
scientist|document-specialist)
|
|
859
844
|
CLI_ARGS="exec --profile codex53_high ${codex_base}"
|
|
860
845
|
CLI_EFFORT="codex53_high"; DEFAULT_TIMEOUT=1440; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
|
|
861
846
|
scientist-deep)
|
|
862
847
|
CLI_ARGS="exec --profile gpt54_high ${codex_base}"
|
|
863
848
|
CLI_EFFORT="gpt54_high"; DEFAULT_TIMEOUT=3600; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
|
|
864
|
-
document-specialist)
|
|
865
|
-
CLI_ARGS="exec --profile codex53_high ${codex_base}"
|
|
866
|
-
CLI_EFFORT="codex53_high"; DEFAULT_TIMEOUT=1440; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
|
|
867
849
|
|
|
868
850
|
# ─── UI/문서 레인 ───
|
|
869
|
-
designer)
|
|
851
|
+
designer|gemini)
|
|
870
852
|
CLI_ARGS="-m $(resolve_gemini_profile pro31) -y --prompt"
|
|
871
853
|
CLI_EFFORT="pro31"; DEFAULT_TIMEOUT=900; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
|
|
872
854
|
writer)
|
|
@@ -874,10 +856,10 @@ route_agent() {
|
|
|
874
856
|
CLI_EFFORT="flash3"; DEFAULT_TIMEOUT=900; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
|
|
875
857
|
|
|
876
858
|
# ─── 탐색 (Claude-native: Glob/Grep/Read 직접 접근) ───
|
|
877
|
-
explore)
|
|
859
|
+
explore|claude)
|
|
878
860
|
CLI_EFFORT="n/a"; DEFAULT_TIMEOUT=600; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
|
|
879
861
|
|
|
880
|
-
# ─── 검증/테스트
|
|
862
|
+
# ─── 검증/테스트 ───
|
|
881
863
|
verifier)
|
|
882
864
|
CLI_ARGS="exec --profile codex53_high ${codex_base} review"
|
|
883
865
|
CLI_EFFORT="codex53_high"; DEFAULT_TIMEOUT=1200; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
|
|
@@ -892,15 +874,6 @@ route_agent() {
|
|
|
892
874
|
spark)
|
|
893
875
|
CLI_ARGS="exec --profile spark53_low ${codex_base}"
|
|
894
876
|
CLI_EFFORT="spark53_low"; DEFAULT_TIMEOUT=180; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
|
|
895
|
-
# ─── CLI 이름 alias (사용자 편의) ───
|
|
896
|
-
codex)
|
|
897
|
-
CLI_ARGS="exec --profile codex53_high ${codex_base}"
|
|
898
|
-
CLI_EFFORT="codex53_high"; DEFAULT_TIMEOUT=1080; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
|
|
899
|
-
gemini)
|
|
900
|
-
CLI_ARGS="-m $(resolve_gemini_profile pro31) -y --prompt"
|
|
901
|
-
CLI_EFFORT="pro31"; DEFAULT_TIMEOUT=900; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
|
|
902
|
-
claude)
|
|
903
|
-
CLI_EFFORT="n/a"; DEFAULT_TIMEOUT=600; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
|
|
904
877
|
# ─── agent-map.json에만 정의된 신규 에이전트 (CLI_TYPE별 기본값) ───
|
|
905
878
|
*)
|
|
906
879
|
case "$CLI_TYPE" in
|
|
@@ -924,21 +897,29 @@ TFX_CODEX_TRANSPORT="${TFX_CODEX_TRANSPORT:-auto}"
|
|
|
924
897
|
# Preflight 캐시 일괄 로드 — CLI/Hub 가용성 + Codex 요금제를 환경변수로 내보냄
|
|
925
898
|
# 하위 프로세스(스킬 포함)가 TFX_CODEX_OK, TFX_GEMINI_OK, TFX_HUB_OK로 즉시 참조 가능
|
|
926
899
|
if [[ -z "${TFX_PREFLIGHT_LOADED:-}" ]]; then
|
|
927
|
-
eval
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
900
|
+
# eval 제거 — pipe-delimited read로 인젝션 위험 차단
|
|
901
|
+
IFS='|' read -r _pf_codex _pf_gemini _pf_hub _pf_plan _pf_agents < <(
|
|
902
|
+
"$NODE_BIN" -e '
|
|
903
|
+
try {
|
|
904
|
+
const c = JSON.parse(require("fs").readFileSync(require("path").join(require("os").homedir(),".claude","cache","tfx-preflight.json"),"utf8"));
|
|
905
|
+
const parts = [
|
|
906
|
+
c?.codex?.ok ? "1" : "0",
|
|
907
|
+
c?.gemini?.ok ? "1" : "0",
|
|
908
|
+
c?.hub?.ok ? "1" : "0",
|
|
909
|
+
(c?.codex_plan?.plan && c.codex_plan.plan !== "unknown" && c.codex_plan.plan !== "api") ? c.codex_plan.plan : "",
|
|
910
|
+
Array.isArray(c?.available_agents) ? c.available_agents.join(",") : ""
|
|
911
|
+
];
|
|
912
|
+
process.stdout.write(parts.join("|"));
|
|
913
|
+
} catch { process.stdout.write("0|0|0||"); }
|
|
914
|
+
' 2>/dev/null
|
|
915
|
+
) || true
|
|
916
|
+
export TFX_CODEX_OK="${_pf_codex:-0}"
|
|
917
|
+
export TFX_GEMINI_OK="${_pf_gemini:-0}"
|
|
918
|
+
export TFX_HUB_OK="${_pf_hub:-0}"
|
|
919
|
+
[[ -n "${_pf_plan:-}" ]] && export TFX_CODEX_PLAN="$_pf_plan"
|
|
920
|
+
[[ -n "${_pf_agents:-}" ]] && export TFX_AVAILABLE_AGENTS="$_pf_agents"
|
|
921
|
+
export TFX_PREFLIGHT_LOADED=1
|
|
922
|
+
unset _pf_codex _pf_gemini _pf_hub _pf_plan _pf_agents
|
|
942
923
|
TFX_CODEX_PLAN="${TFX_CODEX_PLAN:-pro}"
|
|
943
924
|
fi
|
|
944
925
|
TFX_WORKER_INDEX="${TFX_WORKER_INDEX:-}"
|
|
@@ -1027,8 +1008,6 @@ apply_cli_mode() {
|
|
|
1027
1008
|
CLI_ARGS="-m $(resolve_gemini_profile pro31) -y --prompt"; CLI_EFFORT="pro31" ;;
|
|
1028
1009
|
build-fixer|spark)
|
|
1029
1010
|
CLI_ARGS="-m $(resolve_gemini_profile flash3) -y --prompt"; CLI_EFFORT="flash3"; DEFAULT_TIMEOUT=180 ;;
|
|
1030
|
-
writer)
|
|
1031
|
-
CLI_ARGS="-m $(resolve_gemini_profile flash3) -y --prompt"; CLI_EFFORT="flash3" ;;
|
|
1032
1011
|
*)
|
|
1033
1012
|
CLI_ARGS="-m $(resolve_gemini_profile flash3) -y --prompt"; CLI_EFFORT="flash3" ;;
|
|
1034
1013
|
esac
|
|
@@ -1044,16 +1023,14 @@ apply_cli_mode() {
|
|
|
1044
1023
|
if command -v "$GEMINI_BIN" &>/dev/null; then
|
|
1045
1024
|
TFX_CLI_MODE="gemini"; apply_cli_mode; return
|
|
1046
1025
|
else
|
|
1047
|
-
ORIGINAL_AGENT="${AGENT_TYPE}";
|
|
1048
|
-
CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
|
|
1026
|
+
ORIGINAL_AGENT="${AGENT_TYPE}" CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
|
|
1049
1027
|
echo "[tfx-route] codex/gemini 모두 미설치: $AGENT_TYPE → claude-native fallback" >&2
|
|
1050
1028
|
fi
|
|
1051
1029
|
elif [[ "$CLI_TYPE" == "gemini" ]] && ! command -v "$GEMINI_BIN" &>/dev/null; then
|
|
1052
1030
|
if command -v "$CODEX_BIN" &>/dev/null; then
|
|
1053
1031
|
TFX_CLI_MODE="codex"; apply_cli_mode; return
|
|
1054
1032
|
else
|
|
1055
|
-
ORIGINAL_AGENT="${AGENT_TYPE}";
|
|
1056
|
-
CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
|
|
1033
|
+
ORIGINAL_AGENT="${AGENT_TYPE}" CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
|
|
1057
1034
|
echo "[tfx-route] codex/gemini 모두 미설치: $AGENT_TYPE → claude-native fallback" >&2
|
|
1058
1035
|
fi
|
|
1059
1036
|
fi ;;
|
|
@@ -1067,7 +1044,7 @@ apply_plan_guard() {
|
|
|
1067
1044
|
|
|
1068
1045
|
if [[ "$CLI_EFFORT" == spark53_* ]]; then
|
|
1069
1046
|
local codex_base
|
|
1070
|
-
|
|
1047
|
+
codex_base="$(build_codex_base)"
|
|
1071
1048
|
CLI_ARGS="exec --profile codex53_high ${codex_base}"
|
|
1072
1049
|
CLI_EFFORT="codex53_high"
|
|
1073
1050
|
echo "[tfx-route] TFX_CODEX_PLAN=$TFX_CODEX_PLAN: spark → codex53_high로 다운그레이드 (Pro 전용)" >&2
|
|
@@ -1168,34 +1145,12 @@ get_cached_servers() {
|
|
|
1168
1145
|
}
|
|
1169
1146
|
|
|
1170
1147
|
resolve_mcp_filter_script() {
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
local -a candidates=()
|
|
1178
|
-
|
|
1179
|
-
script_ref="$(normalize_script_path "${BASH_SOURCE[0]}")"
|
|
1180
|
-
if [[ -n "$script_ref" ]]; then
|
|
1181
|
-
script_dir="$(cd "$(dirname "$script_ref")" 2>/dev/null && pwd -P || true)"
|
|
1182
|
-
[[ -n "$script_dir" ]] && candidates+=("$script_dir/lib/mcp-filter.mjs")
|
|
1183
|
-
fi
|
|
1184
|
-
|
|
1185
|
-
candidates+=(
|
|
1186
|
-
"$PWD/scripts/lib/mcp-filter.mjs"
|
|
1187
|
-
"$PWD/lib/mcp-filter.mjs"
|
|
1188
|
-
)
|
|
1189
|
-
|
|
1190
|
-
for candidate in "${candidates[@]}"; do
|
|
1191
|
-
if [[ -f "$candidate" ]]; then
|
|
1192
|
-
MCP_FILTER_SCRIPT="$candidate"
|
|
1193
|
-
printf '%s\n' "$MCP_FILTER_SCRIPT"
|
|
1194
|
-
return 0
|
|
1195
|
-
fi
|
|
1196
|
-
done
|
|
1197
|
-
|
|
1198
|
-
return 1
|
|
1148
|
+
[[ -n "$MCP_FILTER_SCRIPT" && -f "$MCP_FILTER_SCRIPT" ]] && { printf '%s\n' "$MCP_FILTER_SCRIPT"; return 0; }
|
|
1149
|
+
local sd; sd="$(_get_script_dir)"
|
|
1150
|
+
MCP_FILTER_SCRIPT=$(_resolve_script "" \
|
|
1151
|
+
${sd:+"$sd/lib/mcp-filter.mjs"} \
|
|
1152
|
+
"$PWD/scripts/lib/mcp-filter.mjs" "$PWD/lib/mcp-filter.mjs") || return 1
|
|
1153
|
+
printf '%s\n' "$MCP_FILTER_SCRIPT"
|
|
1199
1154
|
}
|
|
1200
1155
|
|
|
1201
1156
|
resolve_mcp_policy() {
|
|
@@ -1216,9 +1171,8 @@ resolve_mcp_policy() {
|
|
|
1216
1171
|
available_servers=""
|
|
1217
1172
|
fi
|
|
1218
1173
|
# Codex 0.115+: 미등록 서버에 config override(enabled=true/false 모두)를 보내면
|
|
1219
|
-
# "invalid transport" 에러 발생. 캐시 비어있으면 빈
|
|
1220
|
-
# mcp-filter가 override를 생성하지
|
|
1221
|
-
[[ -z "$available_servers" ]] && available_servers=""
|
|
1174
|
+
# "invalid transport" 에러 발생. 캐시 비어있으면 빈 문자열이 유지되어
|
|
1175
|
+
# mcp-filter가 override를 생성하지 않는다.
|
|
1222
1176
|
|
|
1223
1177
|
local -a cmd=(
|
|
1224
1178
|
"$NODE_BIN" "$filter_script" shell
|
|
@@ -1358,18 +1312,19 @@ heartbeat_monitor() {
|
|
|
1358
1312
|
echo "[tfx-heartbeat] pid=$pid terminated" >&2
|
|
1359
1313
|
}
|
|
1360
1314
|
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1315
|
+
# _wait_with_heartbeat PID — track + heartbeat + wait + cleanup
|
|
1316
|
+
_wait_with_heartbeat() {
|
|
1317
|
+
local wpid="$1" hb_pid ec=0
|
|
1318
|
+
track_worker_pid "$wpid"
|
|
1319
|
+
heartbeat_monitor "$wpid" &
|
|
1320
|
+
hb_pid=$!
|
|
1321
|
+
wait "$wpid" || ec=$?
|
|
1322
|
+
kill "$hb_pid" 2>/dev/null; wait "$hb_pid" 2>/dev/null
|
|
1323
|
+
return "$ec"
|
|
1324
|
+
}
|
|
1366
1325
|
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
script_dir="$(cd "$(dirname "$script_ref")" && pwd -P)"
|
|
1370
|
-
local candidate="$script_dir/tfx-route-worker.mjs"
|
|
1371
|
-
[[ -f "$candidate" ]] || return 1
|
|
1372
|
-
printf '%s\n' "$candidate"
|
|
1326
|
+
resolve_worker_runner_script() {
|
|
1327
|
+
_resolve_script "${TFX_ROUTE_WORKER_RUNNER:-}" "$(_get_script_dir)/tfx-route-worker.mjs"
|
|
1373
1328
|
}
|
|
1374
1329
|
|
|
1375
1330
|
run_stream_worker() {
|
|
@@ -1378,7 +1333,7 @@ run_stream_worker() {
|
|
|
1378
1333
|
local use_tee_flag="$3"
|
|
1379
1334
|
shift 3
|
|
1380
1335
|
local exit_code_local=0
|
|
1381
|
-
local worker_pid
|
|
1336
|
+
local worker_pid
|
|
1382
1337
|
|
|
1383
1338
|
local runner_script
|
|
1384
1339
|
if ! runner_script=$(resolve_worker_runner_script); then
|
|
@@ -1406,48 +1361,22 @@ run_stream_worker() {
|
|
|
1406
1361
|
printf '%s' "$prompt" | "$TIMEOUT_BIN" "$TIMEOUT_SEC" "${worker_cmd[@]}" >"$STDOUT_LOG" 2>"$STDERR_LOG" &
|
|
1407
1362
|
fi
|
|
1408
1363
|
worker_pid=$!
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
heartbeat_monitor "$worker_pid" &
|
|
1412
|
-
hb_pid=$!
|
|
1413
|
-
|
|
1414
|
-
wait "$worker_pid" || exit_code_local=$?
|
|
1415
|
-
kill "$hb_pid" 2>/dev/null; wait "$hb_pid" 2>/dev/null
|
|
1364
|
+
_wait_with_heartbeat "$worker_pid" || exit_code_local=$?
|
|
1416
1365
|
return "$exit_code_local"
|
|
1417
1366
|
}
|
|
1418
1367
|
|
|
1419
1368
|
resolve_codex_mcp_script() {
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
local script_ref script_dir
|
|
1426
|
-
script_ref="$(normalize_script_path "${BASH_SOURCE[0]}")"
|
|
1427
|
-
script_dir="$(cd "$(dirname "$script_ref")" && pwd -P)"
|
|
1428
|
-
local candidates=()
|
|
1429
|
-
[[ -n "$TFX_PKG_ROOT" ]] && candidates+=("$TFX_PKG_ROOT/hub/workers/codex-mcp.mjs")
|
|
1430
|
-
candidates+=(
|
|
1431
|
-
"$script_dir/hub/workers/codex-mcp.mjs"
|
|
1432
|
-
"$script_dir/../hub/workers/codex-mcp.mjs"
|
|
1433
|
-
)
|
|
1434
|
-
|
|
1435
|
-
local candidate
|
|
1436
|
-
for candidate in "${candidates[@]}"; do
|
|
1437
|
-
if [[ -f "$candidate" ]]; then
|
|
1438
|
-
printf '%s\n' "$candidate"
|
|
1439
|
-
return 0
|
|
1440
|
-
fi
|
|
1441
|
-
done
|
|
1442
|
-
|
|
1443
|
-
return 1
|
|
1369
|
+
local sd; sd="$(_get_script_dir)"
|
|
1370
|
+
_resolve_script "${TFX_CODEX_MCP_SCRIPT:-}" \
|
|
1371
|
+
${TFX_PKG_ROOT:+"$TFX_PKG_ROOT/hub/workers/codex-mcp.mjs"} \
|
|
1372
|
+
"$sd/hub/workers/codex-mcp.mjs" "$sd/../hub/workers/codex-mcp.mjs"
|
|
1444
1373
|
}
|
|
1445
1374
|
|
|
1446
1375
|
run_codex_exec() {
|
|
1447
1376
|
local prompt="$1"
|
|
1448
1377
|
local use_tee_flag="$2"
|
|
1449
1378
|
local exit_code_local=0
|
|
1450
|
-
local worker_pid
|
|
1379
|
+
local worker_pid
|
|
1451
1380
|
local -a codex_args=()
|
|
1452
1381
|
read -r -a codex_args <<< "$CLI_ARGS"
|
|
1453
1382
|
if [[ ${#CODEX_CONFIG_FLAGS[@]} -gt 0 ]]; then
|
|
@@ -1460,13 +1389,7 @@ run_codex_exec() {
|
|
|
1460
1389
|
"$TIMEOUT_BIN" "$TIMEOUT_SEC" "$CLI_CMD" "${codex_args[@]}" "$prompt" < /dev/null >"$STDOUT_LOG" 2>"$STDERR_LOG" &
|
|
1461
1390
|
fi
|
|
1462
1391
|
worker_pid=$!
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
heartbeat_monitor "$worker_pid" &
|
|
1466
|
-
hb_pid=$!
|
|
1467
|
-
|
|
1468
|
-
wait "$worker_pid" || exit_code_local=$?
|
|
1469
|
-
kill "$hb_pid" 2>/dev/null; wait "$hb_pid" 2>/dev/null
|
|
1392
|
+
_wait_with_heartbeat "$worker_pid" || exit_code_local=$?
|
|
1470
1393
|
|
|
1471
1394
|
if [[ ! -s "$STDOUT_LOG" && -s "$STDERR_LOG" ]]; then
|
|
1472
1395
|
# stderr에서 마지막 "codex" 마커 이후의 텍스트를 stdout으로 복구
|
|
@@ -1498,17 +1421,16 @@ run_codex_exec() {
|
|
|
1498
1421
|
run_codex_mcp() {
|
|
1499
1422
|
local prompt="$1"
|
|
1500
1423
|
local use_tee_flag="$2"
|
|
1501
|
-
local mcp_script
|
|
1424
|
+
local mcp_script
|
|
1502
1425
|
local exit_code_local=0
|
|
1503
|
-
local worker_pid
|
|
1426
|
+
local worker_pid
|
|
1504
1427
|
|
|
1505
1428
|
if ! mcp_script=$(resolve_codex_mcp_script); then
|
|
1506
1429
|
echo "[tfx-route] 경고: Codex MCP 래퍼를 찾지 못했습니다." >&2
|
|
1507
1430
|
return "$CODEX_MCP_TRANSPORT_EXIT_CODE"
|
|
1508
1431
|
fi
|
|
1509
1432
|
|
|
1510
|
-
|
|
1511
|
-
if ! command -v "$node_bin" &>/dev/null; then
|
|
1433
|
+
if ! command -v "$NODE_BIN" &>/dev/null; then
|
|
1512
1434
|
echo "[tfx-route] 경고: node를 찾지 못해 Codex MCP 경로를 사용할 수 없습니다." >&2
|
|
1513
1435
|
return "$CODEX_MCP_TRANSPORT_EXIT_CODE"
|
|
1514
1436
|
fi
|
|
@@ -1550,18 +1472,12 @@ run_codex_mcp() {
|
|
|
1550
1472
|
esac
|
|
1551
1473
|
|
|
1552
1474
|
if [[ "$use_tee_flag" == "true" ]]; then
|
|
1553
|
-
"$TIMEOUT_BIN" "$TIMEOUT_SEC" "$
|
|
1475
|
+
"$TIMEOUT_BIN" "$TIMEOUT_SEC" "$NODE_BIN" "${mcp_args[@]}" 2>"$STDERR_LOG" | tee "$STDOUT_LOG" &
|
|
1554
1476
|
else
|
|
1555
|
-
"$TIMEOUT_BIN" "$TIMEOUT_SEC" "$
|
|
1477
|
+
"$TIMEOUT_BIN" "$TIMEOUT_SEC" "$NODE_BIN" "${mcp_args[@]}" >"$STDOUT_LOG" 2>"$STDERR_LOG" &
|
|
1556
1478
|
fi
|
|
1557
1479
|
worker_pid=$!
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
heartbeat_monitor "$worker_pid" &
|
|
1561
|
-
hb_pid=$!
|
|
1562
|
-
|
|
1563
|
-
wait "$worker_pid" || exit_code_local=$?
|
|
1564
|
-
kill "$hb_pid" 2>/dev/null; wait "$hb_pid" 2>/dev/null
|
|
1480
|
+
_wait_with_heartbeat "$worker_pid" || exit_code_local=$?
|
|
1565
1481
|
|
|
1566
1482
|
# 모듈 로드 실패(의존성 누락) → MCP transport exit code로 변환하여 fallback 트리거
|
|
1567
1483
|
if [[ "$exit_code_local" -ne 0 && "$exit_code_local" -ne 124 ]] && grep -q 'ERR_MODULE_NOT_FOUND' "$STDERR_LOG" 2>/dev/null; then
|
|
@@ -1760,6 +1676,27 @@ FALLBACK_EOF
|
|
|
1760
1676
|
done
|
|
1761
1677
|
fi
|
|
1762
1678
|
|
|
1679
|
+
# ── Gemini extensions (-e) 주입 (issue #64) ──
|
|
1680
|
+
if [[ -n "$TFX_GEMINI_EXTENSIONS" ]]; then
|
|
1681
|
+
local ext
|
|
1682
|
+
IFS="," read -ra _gemini_exts <<< "$TFX_GEMINI_EXTENSIONS"
|
|
1683
|
+
for ext in "${_gemini_exts[@]}"; do
|
|
1684
|
+
ext=$(echo "$ext" | xargs) # trim whitespace
|
|
1685
|
+
[[ -n "$ext" ]] && gemini_worker_args+=("--extra-arg" "-e" "--extra-arg" "$ext")
|
|
1686
|
+
done
|
|
1687
|
+
echo "[tfx-route] Gemini extensions: ${TFX_GEMINI_EXTENSIONS}" >&2
|
|
1688
|
+
fi
|
|
1689
|
+
|
|
1690
|
+
# ── Gemini 추가 플래그 주입 (issue #64) ──
|
|
1691
|
+
if [[ -n "$TFX_GEMINI_FLAGS" ]]; then
|
|
1692
|
+
local flag
|
|
1693
|
+
read -ra _gemini_flags <<< "$TFX_GEMINI_FLAGS"
|
|
1694
|
+
for flag in "${_gemini_flags[@]}"; do
|
|
1695
|
+
[[ -n "$flag" ]] && gemini_worker_args+=("--extra-arg" "$flag")
|
|
1696
|
+
done
|
|
1697
|
+
echo "[tfx-route] Gemini extra flags: ${TFX_GEMINI_FLAGS}" >&2
|
|
1698
|
+
fi
|
|
1699
|
+
|
|
1763
1700
|
run_stream_worker "gemini" "$FULL_PROMPT" "$use_tee" "${gemini_worker_args[@]}" || exit_code=$?
|
|
1764
1701
|
if [[ "$exit_code" -ne 0 && "$exit_code" -ne 124 ]]; then
|
|
1765
1702
|
echo "[tfx-route] Gemini stream wrapper 실패(exit=${exit_code}). claude-native fallback." >&2
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// wt-cli.mjs — wt-manager CLI wrapper for Claude Code
|
|
3
|
+
// safety-guard가 wt.exe 직접 호출을 차단하므로 이 스크립트를 경유한다.
|
|
4
|
+
import { createWtManager } from "../hub/team/wt-manager.mjs";
|
|
5
|
+
|
|
6
|
+
const [action, ...rest] = process.argv.slice(2);
|
|
7
|
+
if (!action) {
|
|
8
|
+
console.error(
|
|
9
|
+
"Usage: node scripts/wt-cli.mjs <action> [json-opts]\n" +
|
|
10
|
+
"Actions: create-tab, split-pane, layout, list, close, close-stale, rename",
|
|
11
|
+
);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const opts = rest.length ? JSON.parse(rest.join(" ")) : {};
|
|
16
|
+
const wt = createWtManager();
|
|
17
|
+
|
|
18
|
+
switch (action) {
|
|
19
|
+
case "create-tab": {
|
|
20
|
+
const r = await wt.createTab(opts);
|
|
21
|
+
console.log(JSON.stringify(r));
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
case "split-pane": {
|
|
25
|
+
await wt.splitPane(opts);
|
|
26
|
+
console.log(JSON.stringify({ success: true }));
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
case "layout": {
|
|
30
|
+
const panes = Array.isArray(opts) ? opts : opts.panes;
|
|
31
|
+
const r = await wt.applySplitLayout(panes);
|
|
32
|
+
console.log(JSON.stringify(r || { success: true }));
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
case "list": {
|
|
36
|
+
console.log(JSON.stringify(wt.listTabs()));
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
case "close": {
|
|
40
|
+
await wt.closeTab(opts.title);
|
|
41
|
+
console.log(JSON.stringify({ success: true }));
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
case "close-stale": {
|
|
45
|
+
const closed = await wt.closeStale(opts);
|
|
46
|
+
console.log(JSON.stringify({ success: true, closed }));
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
case "rename": {
|
|
50
|
+
const r = wt.renameTab(opts);
|
|
51
|
+
console.log(JSON.stringify(r));
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
default:
|
|
55
|
+
console.error(`Unknown action: ${action}`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
@@ -46,13 +46,7 @@ psmux --version 2>/dev/null && \
|
|
|
46
46
|
IF claude -p (one-shot 모드):
|
|
47
47
|
→ Tier 3 즉시 fallback
|
|
48
48
|
|
|
49
|
-
IF psmux
|
|
50
|
-
→ Tier 3
|
|
51
|
-
|
|
52
|
-
IF Hub 미응답:
|
|
53
|
-
→ hub-ensure 자동 재시작 시도: Bash("node ~/.claude/scripts/hub-ensure.mjs")
|
|
54
|
-
→ 재시작 성공(exit 0) → Tier 판정 재시도
|
|
55
|
-
→ 재시작 실패 → Tier 3
|
|
49
|
+
IF psmux 없음 OR Hub 미응답:
|
|
56
50
|
→ Tier 3
|
|
57
51
|
|
|
58
52
|
IF Codex 없음 AND Gemini 없음:
|