triflux 9.7.9 → 9.7.10
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/bin/triflux.mjs +6 -0
- package/hooks/hook-orchestrator.mjs +66 -23
- package/hooks/hook-registry.json +3 -3
- package/hub/team/dashboard-open.mjs +4 -1
- package/hub/team/headless.mjs +10 -3
- package/package.json +1 -1
- package/scripts/preflight-cache.mjs +1 -1
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
{
|
|
10
10
|
"name": "triflux",
|
|
11
11
|
"description": "CLI-first multi-model orchestrator for Claude Code. Routes tasks to Codex, Gemini, and Claude CLIs with automatic triage, DAG-based parallel execution, headless psmux sessions, and cost-optimized routing. Includes 41 skills, HUD status bar, hook orchestrator, and shell-based CLI routing.",
|
|
12
|
-
"version": "9.7.
|
|
12
|
+
"version": "9.7.10",
|
|
13
13
|
"author": {
|
|
14
14
|
"name": "tellang"
|
|
15
15
|
},
|
|
@@ -30,5 +30,5 @@
|
|
|
30
30
|
]
|
|
31
31
|
}
|
|
32
32
|
],
|
|
33
|
-
"version": "9.7.
|
|
33
|
+
"version": "9.7.10"
|
|
34
34
|
}
|
package/bin/triflux.mjs
CHANGED
|
@@ -2852,6 +2852,12 @@ async function cmdCodexTeam(args = []) {
|
|
|
2852
2852
|
// ── Hub preflight 체크 (multi/auto 실행 전) ──
|
|
2853
2853
|
|
|
2854
2854
|
async function checkHubRunning() {
|
|
2855
|
+
// preflight 캐시 먼저 확인 — 히트 시 fetch 스킵
|
|
2856
|
+
try {
|
|
2857
|
+
const cacheFile = join(homedir(), ".claude", "cache", "tfx-preflight.json");
|
|
2858
|
+
const cached = JSON.parse(readFileSync(cacheFile, "utf8"));
|
|
2859
|
+
if (Date.now() - cached.timestamp < 300_000 && cached.hub?.ok) return true;
|
|
2860
|
+
} catch {}
|
|
2855
2861
|
const port = Number(process.env.TFX_HUB_PORT || "27888");
|
|
2856
2862
|
try {
|
|
2857
2863
|
const res = await fetch(`http://127.0.0.1:${port}/status`, {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
import { readFileSync, existsSync } from "node:fs";
|
|
23
23
|
import { join, dirname } from "node:path";
|
|
24
24
|
import { fileURLToPath } from "node:url";
|
|
25
|
-
import { execFileSync } from "node:child_process";
|
|
25
|
+
import { execFileSync, execFile } from "node:child_process";
|
|
26
26
|
import { PLUGIN_ROOT } from "./lib/resolve-root.mjs";
|
|
27
27
|
|
|
28
28
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -109,6 +109,28 @@ function executeHook(hook, stdinData) {
|
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
function executeHookAsync(hook, stdinData) {
|
|
113
|
+
return new Promise((resolve) => {
|
|
114
|
+
const cmd = resolveCommand(hook.command);
|
|
115
|
+
const timeout = (hook.timeout || 10) * 1000;
|
|
116
|
+
const parts = parseCommand(cmd);
|
|
117
|
+
if (parts.length === 0) { resolve({ code: 1, stdout: "", stderr: "empty command" }); return; }
|
|
118
|
+
const [executable, ...args] = parts;
|
|
119
|
+
const child = execFile(executable, args, {
|
|
120
|
+
timeout,
|
|
121
|
+
encoding: "utf8",
|
|
122
|
+
windowsHide: true,
|
|
123
|
+
cwd: process.cwd(),
|
|
124
|
+
env: { ...process.env },
|
|
125
|
+
}, (err, stdout, stderr) => {
|
|
126
|
+
if (err) resolve({ code: err.status ?? 1, stdout: stdout || "", stderr: stderr || "" });
|
|
127
|
+
else resolve({ code: 0, stdout: stdout || "", stderr: "" });
|
|
128
|
+
});
|
|
129
|
+
if (stdinData) { child.stdin.write(stdinData); child.stdin.end(); }
|
|
130
|
+
else child.stdin.end();
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
112
134
|
// ── 명령어 파싱 (따옴표 처리) ───────────────────────────────
|
|
113
135
|
function parseCommand(cmd) {
|
|
114
136
|
const parts = [];
|
|
@@ -189,7 +211,7 @@ function mergeOutputs(accumulated, newOutput) {
|
|
|
189
211
|
}
|
|
190
212
|
|
|
191
213
|
// ── 메인 ────────────────────────────────────────────────────
|
|
192
|
-
function main() {
|
|
214
|
+
async function main() {
|
|
193
215
|
const stdinRaw = readStdin();
|
|
194
216
|
const registry = loadRegistry();
|
|
195
217
|
|
|
@@ -224,28 +246,51 @@ function main() {
|
|
|
224
246
|
.filter((h) => h.enabled !== false)
|
|
225
247
|
.sort((a, b) => (a.priority ?? 999) - (b.priority ?? 999));
|
|
226
248
|
|
|
227
|
-
// 매처 필터링
|
|
228
|
-
|
|
229
|
-
let blocked = false;
|
|
249
|
+
// 매처 필터링
|
|
250
|
+
const matched = sorted.filter((h) => matchesMatcher(h.matcher, toolName));
|
|
230
251
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
252
|
+
// 같은 priority 그룹별 병렬 실행
|
|
253
|
+
const groups = [];
|
|
254
|
+
for (const hook of matched) {
|
|
255
|
+
const p = hook.priority ?? 999;
|
|
256
|
+
if (groups.length === 0 || groups[groups.length - 1].priority !== p) {
|
|
257
|
+
groups.push({ priority: p, hooks: [hook] });
|
|
258
|
+
} else {
|
|
259
|
+
groups[groups.length - 1].hooks.push(hook);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
234
262
|
|
|
235
|
-
|
|
263
|
+
let mergedOutput = null;
|
|
264
|
+
let blocked = false;
|
|
236
265
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
if (result.stderr) process.stderr.write(result.stderr);
|
|
240
|
-
blocked = true;
|
|
241
|
-
break;
|
|
242
|
-
}
|
|
266
|
+
for (const group of groups) {
|
|
267
|
+
if (blocked) break;
|
|
243
268
|
|
|
244
|
-
if (
|
|
245
|
-
|
|
269
|
+
if (group.hooks.length === 1) {
|
|
270
|
+
// 단일 훅 — 기존 동기 실행
|
|
271
|
+
const result = executeHook(group.hooks[0], stdinRaw);
|
|
272
|
+
if (result.code === 2) {
|
|
273
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
274
|
+
blocked = true;
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
if (result.code === 0 && result.stdout.trim()) {
|
|
278
|
+
mergedOutput = mergeOutputs(mergedOutput, result.stdout.trim());
|
|
279
|
+
}
|
|
280
|
+
} else {
|
|
281
|
+
// 같은 priority 다중 훅 — 비동기 병렬 실행
|
|
282
|
+
const results = await Promise.all(group.hooks.map((h) => executeHookAsync(h, stdinRaw)));
|
|
283
|
+
for (const result of results) {
|
|
284
|
+
if (result.code === 2) {
|
|
285
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
286
|
+
blocked = true;
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
if (result.code === 0 && result.stdout.trim()) {
|
|
290
|
+
mergedOutput = mergeOutputs(mergedOutput, result.stdout.trim());
|
|
291
|
+
}
|
|
292
|
+
}
|
|
246
293
|
}
|
|
247
|
-
|
|
248
|
-
// exit 0이 아닌 다른 코드(1, 3+ 등)는 무시하고 계속
|
|
249
294
|
}
|
|
250
295
|
|
|
251
296
|
// 결과 출력
|
|
@@ -260,10 +305,8 @@ function main() {
|
|
|
260
305
|
process.exit(0);
|
|
261
306
|
}
|
|
262
307
|
|
|
263
|
-
|
|
264
|
-
main();
|
|
265
|
-
} catch (err) {
|
|
308
|
+
main().catch((err) => {
|
|
266
309
|
// 오케스트레이터 자체 실패 → 비차단
|
|
267
310
|
process.stderr.write(`[hook-orchestrator] error: ${err.message}\n`);
|
|
268
311
|
process.exit(0);
|
|
269
|
-
}
|
|
312
|
+
});
|
package/hooks/hook-registry.json
CHANGED
|
@@ -143,11 +143,11 @@
|
|
|
143
143
|
"source": "triflux",
|
|
144
144
|
"matcher": "*",
|
|
145
145
|
"command": "node \"${PLUGIN_ROOT}/scripts/preflight-cache.mjs\"",
|
|
146
|
-
"priority":
|
|
146
|
+
"priority": 2,
|
|
147
147
|
"enabled": true,
|
|
148
|
-
"timeout":
|
|
148
|
+
"timeout": 8,
|
|
149
149
|
"blocking": false,
|
|
150
|
-
"description": "CLI/Hub 가용성 캐시"
|
|
150
|
+
"description": "CLI/Hub 가용성 캐시 (hub-ensure와 병렬 실행)"
|
|
151
151
|
},
|
|
152
152
|
{
|
|
153
153
|
"id": "ext-session-vault-start",
|
|
@@ -97,17 +97,20 @@ export function openHeadlessDashboardTarget(sessionName, opts = {}) {
|
|
|
97
97
|
const safeSession = sanitizeSessionName(sessionName);
|
|
98
98
|
const workerNumber = worker == null ? null : parseWorkerNumber(worker);
|
|
99
99
|
|
|
100
|
+
// 선택 워커 → pane focus만 (새 창 열지 않음)
|
|
100
101
|
if (!openAll && workerNumber != null) {
|
|
101
102
|
try {
|
|
102
103
|
psmuxExec(["select-pane", "-t", `${safeSession}:0.${workerNumber}`]);
|
|
103
104
|
} catch {}
|
|
105
|
+
return true;
|
|
104
106
|
}
|
|
105
107
|
|
|
108
|
+
// 전체 열기 (Shift+Enter) → 새 WT 창으로 세션 attach
|
|
106
109
|
return spawnWindowsTerminal(
|
|
107
110
|
{ command: "psmux", args: ["attach-session", "-t", safeSession] },
|
|
108
111
|
{
|
|
109
112
|
mode: decideDashboardOpenMode({ openAll }),
|
|
110
|
-
title: title ||
|
|
113
|
+
title: title || `▲ ${safeSession}`,
|
|
111
114
|
cwd,
|
|
112
115
|
},
|
|
113
116
|
);
|
package/hub/team/headless.mjs
CHANGED
|
@@ -606,12 +606,19 @@ export function ensureWtProfile(workerCount = 2) {
|
|
|
606
606
|
* @param {number} [workerCount=2]
|
|
607
607
|
* @returns {boolean} 성공 여부
|
|
608
608
|
*/
|
|
609
|
+
let _wtAvailable = null;
|
|
610
|
+
function isWtAvailable() {
|
|
611
|
+
if (_wtAvailable !== null) return _wtAvailable;
|
|
612
|
+
if (!process.env.WT_SESSION) { _wtAvailable = false; return false; }
|
|
613
|
+
try { execSync("where wt.exe", { stdio: "ignore" }); _wtAvailable = true; } catch { _wtAvailable = false; }
|
|
614
|
+
return _wtAvailable;
|
|
615
|
+
}
|
|
616
|
+
|
|
609
617
|
export function autoAttachTerminal(sessionName, opts = {}, workerCount = 2) {
|
|
610
618
|
// 보안: sessionName 셸 주입 방지 — 영숫자, 하이픈, 언더스코어만 허용
|
|
611
619
|
const safeName = String(sessionName).replace(/[^a-zA-Z0-9_\-]/g, "");
|
|
612
620
|
sessionName = safeName || "tfx-session";
|
|
613
|
-
if (!
|
|
614
|
-
try { execSync("where wt.exe", { stdio: "ignore" }); } catch { return false; }
|
|
621
|
+
if (!isWtAvailable()) return false;
|
|
615
622
|
ensureWtProfile(workerCount);
|
|
616
623
|
try {
|
|
617
624
|
const child = spawn("wt.exe", [
|
|
@@ -653,7 +660,7 @@ export function buildDashboardAttachArgs(sessionName, dashboardLayout = "single"
|
|
|
653
660
|
* @returns {boolean}
|
|
654
661
|
*/
|
|
655
662
|
export function attachDashboardTab(sessionName, workerCount = 2, dashboardLayout = "single", dashboardSize = 0.40, dashboardAnchor = "window") {
|
|
656
|
-
|
|
663
|
+
if (!isWtAvailable()) return false;
|
|
657
664
|
ensureWtProfile(workerCount);
|
|
658
665
|
try {
|
|
659
666
|
const args = buildDashboardAttachArgs(sessionName, dashboardLayout, workerCount, dashboardAnchor);
|
package/package.json
CHANGED
|
@@ -12,7 +12,7 @@ const PKG_ROOT = join(dirname(__filename), "..");
|
|
|
12
12
|
|
|
13
13
|
const CACHE_DIR = join(homedir(), ".claude", "cache");
|
|
14
14
|
const CACHE_FILE = join(CACHE_DIR, "tfx-preflight.json");
|
|
15
|
-
const CACHE_TTL_MS =
|
|
15
|
+
const CACHE_TTL_MS = 300_000; // 5분 (warmup과 통일)
|
|
16
16
|
|
|
17
17
|
function checkRoute() {
|
|
18
18
|
const routePath = join(homedir(), ".claude", "scripts", "tfx-route.sh");
|