triflux 4.1.1 → 4.1.4
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 +12 -10
- package/hub/server.mjs +3 -1
- package/hub/team/native.mjs +44 -34
- package/hud/hud-qos-status.mjs +1 -1
- package/package.json +2 -1
- package/scripts/preinstall.mjs +37 -0
- package/scripts/setup.mjs +43 -0
- package/scripts/tfx-route.sh +24 -5
- package/skills/tfx-auto/SKILL.md +21 -0
package/bin/triflux.mjs
CHANGED
|
@@ -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("--
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
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
|
-
|
|
2406
|
-
|
|
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();
|
package/hub/team/native.mjs
CHANGED
|
@@ -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
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
이 규칙을 위반하면 작업 실패로 간주한다.
|
package/hud/hud-qos-status.mjs
CHANGED
|
@@ -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;
|
|
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.
|
|
3
|
+
"version": "4.1.4",
|
|
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");
|
package/scripts/tfx-route.sh
CHANGED
|
@@ -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="
|
|
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 [[
|
|
253
|
-
echo "[tfx-route] Hub 서버 스크립트
|
|
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
|
)
|
package/skills/tfx-auto/SKILL.md
CHANGED
|
@@ -100,6 +100,27 @@ argument-hint: "<command|task> [args...]"
|
|
|
100
100
|
|
|
101
101
|
**수동 모드 (`N:agent_type`):** Codex 분류 건너뜀 → Opus가 N개 서브태스크 분해. N > 10 거부.
|
|
102
102
|
|
|
103
|
+
## 멀티 태스크 라우팅 (트리아지 후)
|
|
104
|
+
|
|
105
|
+
> **트리아지 결과 서브태스크가 2개 이상이면 tfx-multi Native Teams 모드로 자동 전환한다.**
|
|
106
|
+
|
|
107
|
+
| 서브태스크 수 | 실행 경로 | 이유 |
|
|
108
|
+
|--------------|----------|------|
|
|
109
|
+
| 1개 | tfx-auto 직접 실행 (아래 "실행" 섹션) | 팀 오버헤드 불필요, 경량 fire-and-forget |
|
|
110
|
+
| 2개+ | **tfx-multi Phase 3** (TeamCreate → TaskCreate → Agent 래퍼) | Shift+Down 네비게이션, 상태 추적, fallback |
|
|
111
|
+
|
|
112
|
+
**전환 방법:** 트리아지 완료 후 서브태스크 배열을 그대로 tfx-multi Phase 3에 전달한다.
|
|
113
|
+
tfx-multi의 Phase 2(트리아지)는 건너뛰고 Phase 3a(TeamCreate)부터 시작한다.
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
if subtasks.length >= 2:
|
|
117
|
+
→ tfx-multi Phase 3 실행 (트리아지 결과 재사용)
|
|
118
|
+
→ TeamCreate → TaskCreate × N → Agent 래퍼 spawn (Phase 3a~3c)
|
|
119
|
+
→ Phase 4 결과 수집 → Phase 5 정리
|
|
120
|
+
else:
|
|
121
|
+
→ tfx-auto 직접 실행 (아래)
|
|
122
|
+
```
|
|
123
|
+
|
|
103
124
|
## 실행
|
|
104
125
|
|
|
105
126
|
### CLI 에이전트 (Codex/Gemini)
|