triflux 4.1.1 → 4.1.3

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/bin/triflux.mjs CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  // triflux CLI — setup, doctor, version
3
3
  import { copyFileSync, existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync, readdirSync, unlinkSync, rmSync, statSync, openSync, closeSync } from "fs";
4
4
  import { join, dirname } from "path";
@@ -2392,18 +2392,20 @@ async function main() {
2392
2392
  case "tray": {
2393
2393
  const trayUrl = new URL("../hub/tray.mjs", import.meta.url);
2394
2394
  const trayPath = fileURLToPath(trayUrl);
2395
- if (cmdArgs.includes("--detach")) {
2396
- const child = spawn(process.execPath, [trayPath], {
2397
- detached: true,
2398
- stdio: "ignore",
2399
- windowsHide: true,
2400
- });
2401
- child.unref();
2402
- console.log(`\n ${GREEN_BRIGHT}✓${RESET} tray 시작됨 (PID ${child.pid})\n`);
2395
+ if (cmdArgs.includes("--attach")) {
2396
+ // --attach: 포그라운드 모드 (디버깅용)
2397
+ const { startTray } = await import(trayUrl.href);
2398
+ await startTray();
2403
2399
  return;
2404
2400
  }
2405
- const { startTray } = await import(trayUrl.href);
2406
- await startTray();
2401
+ // 기본: detach 모드 (프리징 방지)
2402
+ const child = spawn(process.execPath, [trayPath], {
2403
+ detached: true,
2404
+ stdio: "ignore",
2405
+ windowsHide: true,
2406
+ });
2407
+ child.unref();
2408
+ console.log(`\n ${GREEN_BRIGHT}✓${RESET} tray 시작됨 (PID ${child.pid})\n`);
2407
2409
  return;
2408
2410
  }
2409
2411
  case "multi": {
package/hub/server.mjs CHANGED
@@ -230,8 +230,9 @@ function servePublicFile(res, path) {
230
230
 
231
231
  mkdirSync(PUBLIC_DIR, { recursive: true });
232
232
  if (!existsSync(filePath)) {
233
+ console.warn(`[tfx-hub] 정적 파일 없음: ${filePath}`);
233
234
  res.writeHead(404);
234
- res.end('Not Found');
235
+ res.end('Not Found (static file missing)');
235
236
  return true;
236
237
  }
237
238
 
@@ -746,6 +747,7 @@ export async function startHub({ port = 27888, dbPath, host = '127.0.0.1', sessi
746
747
  }));
747
748
 
748
749
  console.log(`[tfx-hub] MCP 서버 시작: ${info.url} / pipe ${pipe.path} / assign-callback ${assignCallbacks.path} (PID ${process.pid})`);
750
+ console.log(`[tfx-hub] PUBLIC_DIR: ${PUBLIC_DIR} (exists: ${existsSync(PUBLIC_DIR)}, dashboard: ${existsSync(resolve(PUBLIC_DIR, 'dashboard.html'))})`);
749
751
 
750
752
  const stopFn = async () => {
751
753
  router.stopSweeper();
@@ -10,11 +10,12 @@ import * as fs from "node:fs/promises";
10
10
  import os from "node:os";
11
11
  import path from "node:path";
12
12
 
13
- const ROUTE_SCRIPT = "~/.claude/scripts/tfx-route.sh";
14
- export const SLIM_WRAPPER_SUBAGENT_TYPE = "slim-wrapper";
15
- const ROUTE_LOG_RE = /\[tfx-route\]/i;
16
- const ROUTE_COMMAND_RE = /(?:^|[\s"'`])(?:bash\s+)?(?:[^"'`\s]*\/)?tfx-route\.sh\b/i;
17
- const ROUTE_PROMPT_RE = /tfx-route\.sh/i;
13
+ const ROUTE_SCRIPT = "~/.claude/scripts/tfx-route.sh";
14
+ export const SLIM_WRAPPER_SUBAGENT_TYPE = "slim-wrapper";
15
+ const ROUTE_LOG_RE = /\[tfx-route\]/i;
16
+ const ROUTE_COMMAND_RE = /(?:^|[\s"'`])(?:bash\s+)?(?:[^"'`\s]*\/)?tfx-route\.sh\b/i;
17
+ const ROUTE_PROMPT_RE = /tfx-route\.sh/i;
18
+ const DIRECT_TOOL_BYPASS_RE = /\b(?:Read|Edit|Write)\s*\(/;
18
19
 
19
20
  function inferWorkerIndex(agentName = "") {
20
21
  const match = /(\d+)(?!.*\d)/.exec(agentName);
@@ -59,37 +60,46 @@ export function buildSlimWrapperAgent(cli, opts = {}) {
59
60
  * @param {string} [input.promptText]
60
61
  * @param {string} [input.stdoutText]
61
62
  * @param {string} [input.stderrText]
62
- * @returns {{
63
- * expectedRouteInvocation: boolean,
64
- * promptMentionsRoute: boolean,
65
- * sawRouteCommand: boolean,
66
- * sawRouteLog: boolean,
67
- * usedRoute: boolean,
68
- * abnormal: boolean,
69
- * reason: string|null,
70
- * }}
71
- */
72
- export function verifySlimWrapperRouteExecution(input = {}) {
63
+ * @returns {{
64
+ * expectedRouteInvocation: boolean,
65
+ * promptMentionsRoute: boolean,
66
+ * sawRouteCommand: boolean,
67
+ * sawRouteLog: boolean,
68
+ * sawDirectToolBypass: boolean,
69
+ * usedRoute: boolean,
70
+ * abnormal: boolean,
71
+ * reason: string|null,
72
+ * }}
73
+ */
74
+ export function verifySlimWrapperRouteExecution(input = {}) {
73
75
  const promptText = String(input.promptText || "");
74
76
  const stdoutText = String(input.stdoutText || "");
75
77
  const stderrText = String(input.stderrText || "");
76
- const combinedLogs = `${stdoutText}\n${stderrText}`;
77
- const promptMentionsRoute = ROUTE_PROMPT_RE.test(promptText);
78
- const sawRouteCommand = ROUTE_COMMAND_RE.test(combinedLogs);
79
- const sawRouteLog = ROUTE_LOG_RE.test(combinedLogs);
80
- const usedRoute = sawRouteCommand || sawRouteLog;
81
- const expectedRouteInvocation = promptMentionsRoute;
82
-
83
- return {
84
- expectedRouteInvocation,
85
- promptMentionsRoute,
86
- sawRouteCommand,
87
- sawRouteLog,
88
- usedRoute,
89
- abnormal: expectedRouteInvocation && !usedRoute,
90
- reason: expectedRouteInvocation && !usedRoute ? "missing_tfx_route_evidence" : null,
91
- };
92
- }
78
+ const combinedLogs = `${stdoutText}\n${stderrText}`;
79
+ const promptMentionsRoute = ROUTE_PROMPT_RE.test(promptText);
80
+ const sawRouteCommand = ROUTE_COMMAND_RE.test(combinedLogs);
81
+ const sawRouteLog = ROUTE_LOG_RE.test(combinedLogs);
82
+ const sawDirectToolBypass = DIRECT_TOOL_BYPASS_RE.test(stdoutText);
83
+ const usedRoute = sawRouteCommand || sawRouteLog;
84
+ const expectedRouteInvocation = promptMentionsRoute;
85
+ const abnormal = expectedRouteInvocation && (sawDirectToolBypass || !usedRoute);
86
+ const reason = !abnormal
87
+ ? null
88
+ : sawDirectToolBypass
89
+ ? "direct_tool_bypass_detected"
90
+ : "missing_tfx_route_evidence";
91
+
92
+ return {
93
+ expectedRouteInvocation,
94
+ promptMentionsRoute,
95
+ sawRouteCommand,
96
+ sawRouteLog,
97
+ sawDirectToolBypass,
98
+ usedRoute,
99
+ abnormal,
100
+ reason,
101
+ };
102
+ }
93
103
 
94
104
  /**
95
105
  * role/mcp_profile별 tfx-route.sh 기본 timeout (초)
@@ -153,7 +163,7 @@ export function buildSlimWrapperPrompt(cli, opts = {}) {
153
163
  2. Bash 종료 후 TaskUpdate + SendMessage로 Claude Code 태스크 동기화
154
164
  3. 종료${pipelineHint}
155
165
 
156
- [HARD CONSTRAINT] 허용 도구: Bash, TaskUpdate, SendMessage만 사용한다.
166
+ [HARD CONSTRAINT] 허용 도구: Bash, TaskUpdate, TaskGet, TaskList, SendMessage만 사용한다.
157
167
  Read, Edit, Write, Grep, Glob, Agent, WebSearch, WebFetch 등 다른 모든 도구 사용을 금지한다.
158
168
  코드를 직접 읽거나 수정하면 안 된다. 반드시 아래 Bash 명령(tfx-route.sh)을 통해 Codex/Gemini에 위임하라.
159
169
  이 규칙을 위반하면 작업 실패로 간주한다.
@@ -19,7 +19,7 @@ const RED = "\x1b[31m";
19
19
  const GREEN = "\x1b[32m";
20
20
  const YELLOW = "\x1b[33m";
21
21
  const CYAN = "\x1b[36m";
22
- const CLAUDE_ORANGE = "\x1b[38;5;173m"; // #D87656 (Claude 테라코타)
22
+ const CLAUDE_ORANGE = "\x1b[38;2;232;112;64m"; // #E87040 (Claude 공식 오렌지)
23
23
  const CODEX_WHITE = "\x1b[97m"; // bright white (SGR 37은 Windows Terminal에서 연회색 매핑)
24
24
  const GEMINI_BLUE = "\x1b[38;5;39m";
25
25
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triflux",
3
- "version": "4.1.1",
3
+ "version": "4.1.3",
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": {
@@ -26,6 +26,7 @@
26
26
  ],
27
27
  "scripts": {
28
28
  "setup": "node scripts/setup.mjs",
29
+ "preinstall": "node scripts/preinstall.mjs",
29
30
  "postinstall": "node scripts/setup.mjs",
30
31
  "test": "node --test --test-force-exit --test-concurrency=1 \"tests/**/*.test.mjs\" \"scripts/__tests__/**/*.test.mjs\"",
31
32
  "test:unit": "node --test --test-force-exit --test-concurrency=1 tests/unit/**/*.test.mjs",
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+ // npm install 전 Hub를 안전하게 중지하여 EBUSY 방지
3
+ // better-sqlite3.node 파일이 Hub 프로세스에 의해 잠기면 npm이 덮어쓸 수 없음
4
+
5
+ import { existsSync, readFileSync, unlinkSync } from "fs";
6
+ import { join } from "path";
7
+ import { homedir } from "os";
8
+
9
+ const HUB_PID_FILE = join(homedir(), ".claude", "cache", "tfx-hub", "hub.pid");
10
+
11
+ function stopHub() {
12
+ if (!existsSync(HUB_PID_FILE)) return;
13
+
14
+ try {
15
+ const info = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
16
+ const pid = Number(info?.pid);
17
+ if (!Number.isFinite(pid) || pid <= 0) return;
18
+
19
+ // 프로세스 존재 확인
20
+ process.kill(pid, 0);
21
+
22
+ // SIGTERM 전송
23
+ process.kill(pid, "SIGTERM");
24
+ console.log(`[triflux preinstall] Hub 중지됨 (PID ${pid}) — EBUSY 방지`);
25
+
26
+ // PID 파일 정리
27
+ try { unlinkSync(HUB_PID_FILE); } catch {}
28
+ } catch (err) {
29
+ if (err.code === "ESRCH") {
30
+ // 프로세스 이미 종료됨 — PID 파일만 정리
31
+ try { unlinkSync(HUB_PID_FILE); } catch {}
32
+ }
33
+ // EPERM 등 기타 에러는 무시 (설치를 막으면 안 됨)
34
+ }
35
+ }
36
+
37
+ stopHub();
package/scripts/setup.mjs CHANGED
@@ -228,6 +228,49 @@ if (!existsSync(mcpSdkPath) && existsSync(srcNodeModules)) {
228
228
  }
229
229
  }
230
230
 
231
+ // ── 패키지 루트 breadcrumb 기록 ──
232
+ // tfx-route.sh가 hub/server.mjs, hub/bridge.mjs를 찾을 수 있도록
233
+ // 패키지 루트 경로를 ~/.claude/scripts/.tfx-pkg-root에 기록한다.
234
+ {
235
+ const breadcrumbPath = join(CLAUDE_DIR, "scripts", ".tfx-pkg-root");
236
+ const pkgRootForward = PLUGIN_ROOT.replace(/\\/g, "/");
237
+ const currentBreadcrumb = existsSync(breadcrumbPath)
238
+ ? readFileSync(breadcrumbPath, "utf8").trim()
239
+ : "";
240
+ if (currentBreadcrumb !== pkgRootForward) {
241
+ const breadcrumbDir = dirname(breadcrumbPath);
242
+ if (!existsSync(breadcrumbDir)) mkdirSync(breadcrumbDir, { recursive: true });
243
+ writeFileSync(breadcrumbPath, pkgRootForward + "\n", "utf8");
244
+ synced++;
245
+ }
246
+ }
247
+
248
+ // ── 에이전트 동기화 (.claude/agents/ → ~/.claude/agents/) ──
249
+ // slim-wrapper 등 커스텀 에이전트를 글로벌에 배포하여
250
+ // 다른 프로젝트에서도 subagent_type으로 참조 가능하게 한다.
251
+
252
+ const agentsSrc = join(PLUGIN_ROOT, ".claude", "agents");
253
+ const agentsDst = join(CLAUDE_DIR, "agents");
254
+
255
+ if (existsSync(agentsSrc)) {
256
+ if (!existsSync(agentsDst)) mkdirSync(agentsDst, { recursive: true });
257
+
258
+ for (const name of readdirSync(agentsSrc)) {
259
+ if (!name.endsWith(".md")) continue;
260
+
261
+ const src = join(agentsSrc, name);
262
+ const dst = join(agentsDst, name);
263
+
264
+ if (!existsSync(dst)) {
265
+ copyFileSync(src, dst);
266
+ synced++;
267
+ } else if (shouldSyncTextFile(src, dst)) {
268
+ copyFileSync(src, dst);
269
+ synced++;
270
+ }
271
+ }
272
+ }
273
+
231
274
  // ── 스킬 동기화 ──
232
275
 
233
276
  const skillsSrc = join(PLUGIN_ROOT, "skills");
@@ -51,6 +51,14 @@ TFX_TEAM_LEAD_NAME="${TFX_TEAM_LEAD_NAME:-team-lead}"
51
51
  TFX_HUB_PIPE="${TFX_HUB_PIPE:-}"
52
52
  TFX_HUB_URL="${TFX_HUB_URL:-http://127.0.0.1:27888}" # bridge.mjs HTTP fallback hint
53
53
 
54
+ # ── 패키지 루트 해석 (setup.mjs가 기록한 breadcrumb) ──
55
+ TFX_PKG_ROOT=""
56
+ _tfx_breadcrumb="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/.tfx-pkg-root"
57
+ if [[ -f "$_tfx_breadcrumb" ]]; then
58
+ TFX_PKG_ROOT="$(head -1 "$_tfx_breadcrumb" 2>/dev/null | tr -d '\r\n')"
59
+ fi
60
+ unset _tfx_breadcrumb
61
+
54
62
  # fallback 시 원래 에이전트 정보 보존
55
63
  ORIGINAL_AGENT=""
56
64
  ORIGINAL_CLI_ARGS=""
@@ -152,7 +160,9 @@ resolve_bridge_script() {
152
160
 
153
161
  local script_dir
154
162
  script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
155
- local candidates=(
163
+ local candidates=()
164
+ [[ -n "$TFX_PKG_ROOT" ]] && candidates+=("$TFX_PKG_ROOT/hub/bridge.mjs")
165
+ candidates+=(
156
166
  "$script_dir/../hub/bridge.mjs"
157
167
  "$script_dir/hub/bridge.mjs"
158
168
  )
@@ -247,10 +257,17 @@ team_send_message() {
247
257
  try_restart_hub() {
248
258
  local hub_server script_dir hub_port
249
259
  script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
250
- hub_server="$script_dir/../hub/server.mjs"
260
+ hub_server=""
261
+ local _hub_candidates=()
262
+ [[ -n "$TFX_PKG_ROOT" ]] && _hub_candidates+=("$TFX_PKG_ROOT/hub/server.mjs")
263
+ _hub_candidates+=("$script_dir/../hub/server.mjs")
264
+ for _hc in "${_hub_candidates[@]}"; do
265
+ if [[ -f "$_hc" ]]; then hub_server="$_hc"; break; fi
266
+ done
267
+ unset _hub_candidates _hc
251
268
 
252
- if [[ ! -f "$hub_server" ]]; then
253
- echo "[tfx-route] Hub 서버 스크립트 미발견: $hub_server" >&2
269
+ if [[ -z "$hub_server" ]]; then
270
+ echo "[tfx-route] Hub 서버 스크립트 미발견 (pkg_root=${TFX_PKG_ROOT:-unset}, script_dir=$script_dir)" >&2
254
271
  return 1
255
272
  fi
256
273
 
@@ -946,7 +963,9 @@ resolve_codex_mcp_script() {
946
963
  local script_ref script_dir
947
964
  script_ref="$(normalize_script_path "${BASH_SOURCE[0]}")"
948
965
  script_dir="$(cd "$(dirname "$script_ref")" && pwd -P)"
949
- local candidates=(
966
+ local candidates=()
967
+ [[ -n "$TFX_PKG_ROOT" ]] && candidates+=("$TFX_PKG_ROOT/hub/workers/codex-mcp.mjs")
968
+ candidates+=(
950
969
  "$script_dir/hub/workers/codex-mcp.mjs"
951
970
  "$script_dir/../hub/workers/codex-mcp.mjs"
952
971
  )