triflux 9.7.9 → 9.7.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.
@@ -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.9",
12
+ "version": "9.7.11",
13
13
  "author": {
14
14
  "name": "tellang"
15
15
  },
@@ -30,5 +30,5 @@
30
30
  ]
31
31
  }
32
32
  ],
33
- "version": "9.7.9"
33
+ "version": "9.7.11"
34
34
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triflux",
3
- "version": "9.7.9",
3
+ "version": "9.7.11",
4
4
  "description": "CLI-first multi-model orchestrator for Claude Code — route tasks to Codex, Gemini, and Claude",
5
5
  "author": {
6
6
  "name": "tellang"
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
- let mergedOutput = null;
229
- let blocked = false;
249
+ // 매처 필터링
250
+ const matched = sorted.filter((h) => matchesMatcher(h.matcher, toolName));
230
251
 
231
- for (const hook of sorted) {
232
- // 매처 체크
233
- if (!matchesMatcher(hook.matcher, toolName)) continue;
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
- const result = executeHook(hook, stdinRaw);
263
+ let mergedOutput = null;
264
+ let blocked = false;
236
265
 
237
- if (result.code === 2) {
238
- // BLOCK — stderr를 에러로 전달하고 즉시 중단
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 (result.code === 0 && result.stdout.trim()) {
245
- mergedOutput = mergeOutputs(mergedOutput, result.stdout.trim());
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
- try {
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
+ });
@@ -143,11 +143,11 @@
143
143
  "source": "triflux",
144
144
  "matcher": "*",
145
145
  "command": "node \"${PLUGIN_ROOT}/scripts/preflight-cache.mjs\"",
146
- "priority": 3,
146
+ "priority": 2,
147
147
  "enabled": true,
148
- "timeout": 5,
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",
@@ -22,7 +22,7 @@ export class CodexBackend {
22
22
  const cwdFlag = opts.cwd ? ` --cwd '${opts.cwd}'` : "";
23
23
  // Codex 0.117.0+: config.toml에 sandbox 설정이 있으면 CLI 플래그 중복 불가
24
24
  // --dangerously-bypass-approvals-and-sandbox 대신 exec 서브커맨드만 사용 (config가 sandbox 관리)
25
- return `codex exec ${prompt} --output-last-message '${resultFile}' --color never${modelFlag}${cwdFlag}`;
25
+ return `codex exec ${prompt} --output-last-message '${resultFile}' --color never --skip-git-repo-check${modelFlag}${cwdFlag}`;
26
26
  }
27
27
 
28
28
  env() { return {}; }
@@ -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 || (openAll ? `▲ ${safeSession}` : `▲ ${safeSession}:${workerNumber ?? "all"}`),
113
+ title: title || `▲ ${safeSession}`,
111
114
  cwd,
112
115
  },
113
116
  );
@@ -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 (!process.env.WT_SESSION) return false;
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
- try { execSync("where wt.exe", { stdio: "ignore" }); } catch { return false; }
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triflux",
3
- "version": "9.7.9",
3
+ "version": "9.7.11",
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": {
@@ -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 = 30_000; // 30초
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");