triflux 10.9.30 → 10.9.31
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 +170 -20
- package/hooks/keyword-rules.json +7 -24
- package/hooks/session-start-fast.mjs +54 -12
- package/hub/cli-adapter-base.mjs +25 -5
- package/hub/lib/env-detect.mjs +36 -9
- package/hub/pipe.mjs +4 -1
- package/hub/platform.mjs +3 -1
- package/hub/server.mjs +120 -38
- package/hub/team/conductor.mjs +6 -5
- package/hub/team/dashboard-open.mjs +21 -8
- package/hub/team/git-preflight.mjs +9 -9
- package/hub/team/handoff.mjs +2 -1
- package/hub/team/headless.mjs +26 -9
- package/hub/team/notify.mjs +2 -2
- package/hub/team/process-cleanup.mjs +1 -1
- package/hub/team/psmux.mjs +84 -22
- package/hub/team/runtime-strategy.mjs +2 -2
- package/hub/team/swarm-cli.mjs +5 -7
- package/hub/team/swarm-hypervisor.mjs +12 -5
- package/hub/team/swarm-planner.mjs +3 -1
- package/hub/team/synapse-cli.mjs +19 -5
- package/hub/team/synapse-registry.mjs +29 -8
- package/hub/team/tui-core.mjs +8 -12
- package/hub/team/tui-lite.mjs +2 -2
- package/hub/team/tui-synapse.mjs +20 -10
- package/hub/team/tui.mjs +35 -20
- package/hub/team/worktree-lifecycle.mjs +3 -1
- package/hub/team/wt-manager.mjs +10 -2
- package/hub/workers/codex-app-server-worker.mjs +169 -170
- package/hub/workers/delegator-mcp.mjs +23 -14
- package/hub/workers/gemini-worker.mjs +6 -2
- package/hub/workers/lib/jsonrpc-stdio.mjs +19 -19
- package/package.json +1 -1
- package/scripts/__tests__/tfx-doctor-diagnose.test.mjs +7 -3
- package/scripts/claude-login-detect.mjs +24 -5
- package/scripts/config-audit.mjs +92 -22
- package/scripts/doctor-diagnose.mjs +64 -22
- package/scripts/headless-guard.mjs +6 -2
- package/scripts/hub-watchdog.mjs +1 -1
- package/scripts/keyword-detector.mjs +3 -1
- package/scripts/lib/cross-review-utils.mjs +6 -1
- package/scripts/lib/keyword-rules.mjs +1 -3
- package/scripts/lib/mcp-filter.mjs +2 -1
- package/scripts/release/lib.mjs +10 -4
- package/scripts/remote-spawn.mjs +13 -5
- package/scripts/session-stale-cleanup.mjs +19 -10
- package/scripts/setup.mjs +17 -13
- package/skills/tfx-auto/SKILL.md +35 -64
- package/skills/tfx-deep-analysis/SKILL.md +6 -1
- package/skills/tfx-doctor/SKILL.md +0 -21
- package/skills/tfx-hub/SKILL.md +85 -273
- package/skills/tfx-setup/SKILL.md +41 -0
- package/skills/tfx-setup/SKILL.md.tmpl +41 -0
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
{
|
|
10
10
|
"name": "triflux",
|
|
11
11
|
"description": "Tri-CLI orchestrator for Claude Code. Routes tasks across Claude + Codex + Gemini with consensus intelligence, natural language routing, 42 skills, and cross-model review.",
|
|
12
|
-
"version": "10.9.
|
|
12
|
+
"version": "10.9.31",
|
|
13
13
|
"author": {
|
|
14
14
|
"name": "tellang"
|
|
15
15
|
},
|
|
@@ -30,5 +30,5 @@
|
|
|
30
30
|
]
|
|
31
31
|
}
|
|
32
32
|
],
|
|
33
|
-
"version": "10.9.
|
|
33
|
+
"version": "10.9.31"
|
|
34
34
|
}
|
package/bin/triflux.mjs
CHANGED
|
@@ -154,7 +154,8 @@ const CLI_COMMAND_SCHEMAS = Object.freeze({
|
|
|
154
154
|
{
|
|
155
155
|
name: "--diagnose",
|
|
156
156
|
type: "boolean",
|
|
157
|
-
description:
|
|
157
|
+
description:
|
|
158
|
+
"진단 번들(zip) 생성: spawn-trace + hook timing + system info",
|
|
158
159
|
},
|
|
159
160
|
{
|
|
160
161
|
name: "--json",
|
|
@@ -1229,10 +1230,13 @@ function cmdSetup(options = {}) {
|
|
|
1229
1230
|
// ── tmux 기본 셸 확인 (macOS/Linux) ──
|
|
1230
1231
|
if (process.platform !== "win32" && which("tmux")) {
|
|
1231
1232
|
try {
|
|
1232
|
-
const shellOut = execSync(
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1233
|
+
const shellOut = execSync(
|
|
1234
|
+
"tmux show-options -g default-shell 2>/dev/null",
|
|
1235
|
+
{
|
|
1236
|
+
encoding: "utf8",
|
|
1237
|
+
timeout: 3000,
|
|
1238
|
+
},
|
|
1239
|
+
).trim();
|
|
1236
1240
|
if (shellOut) {
|
|
1237
1241
|
ok(`tmux 기본 셸: ${shellOut.split(/\s+/).pop() || "확인 완료"}`);
|
|
1238
1242
|
}
|
|
@@ -1936,7 +1940,10 @@ async function cmdDoctor(options = {}) {
|
|
|
1936
1940
|
}
|
|
1937
1941
|
{
|
|
1938
1942
|
const claudeGuide = ensureGlobalClaudeRoutingSection(CLAUDE_DIR);
|
|
1939
|
-
if (
|
|
1943
|
+
if (
|
|
1944
|
+
claudeGuide.skipped &&
|
|
1945
|
+
claudeGuide.reason !== "global_sync_disabled"
|
|
1946
|
+
)
|
|
1940
1947
|
warn(`CLAUDE.md 라우팅 섹션 확인 실패: ${claudeGuide.reason}`);
|
|
1941
1948
|
else if (
|
|
1942
1949
|
claudeGuide.action === "created" ||
|
|
@@ -2504,6 +2511,86 @@ async function cmdDoctor(options = {}) {
|
|
|
2504
2511
|
addDoctorCheck(report, { name: "stale-skills", status: "ok" });
|
|
2505
2512
|
}
|
|
2506
2513
|
|
|
2514
|
+
// 8.5 Dev 의존성 (npm link 환경에서 node_modules 누락 감지 — Issue #101)
|
|
2515
|
+
section("Dev Dependencies");
|
|
2516
|
+
try {
|
|
2517
|
+
const pkgJsonPath = join(PKG_ROOT, "package.json");
|
|
2518
|
+
const nodeModulesPath = join(PKG_ROOT, "node_modules");
|
|
2519
|
+
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
|
|
2520
|
+
const runtimeDeps = Object.keys(pkgJson.dependencies || {});
|
|
2521
|
+
const isLinkedDev = (() => {
|
|
2522
|
+
try {
|
|
2523
|
+
const stat = statSync(nodeModulesPath);
|
|
2524
|
+
return stat.isDirectory();
|
|
2525
|
+
} catch {
|
|
2526
|
+
return false;
|
|
2527
|
+
}
|
|
2528
|
+
})();
|
|
2529
|
+
const missingDeps = [];
|
|
2530
|
+
if (!isLinkedDev) {
|
|
2531
|
+
// node_modules 전체 누락 — 전역 install 또는 미압축 릴리즈일 가능성
|
|
2532
|
+
if (runtimeDeps.length > 0) {
|
|
2533
|
+
missingDeps.push(...runtimeDeps);
|
|
2534
|
+
}
|
|
2535
|
+
} else {
|
|
2536
|
+
for (const dep of runtimeDeps) {
|
|
2537
|
+
const depPath = join(nodeModulesPath, ...dep.split("/"));
|
|
2538
|
+
if (!existsSync(depPath)) missingDeps.push(dep);
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
if (missingDeps.length === 0) {
|
|
2542
|
+
ok(
|
|
2543
|
+
`의존성 ${runtimeDeps.length}개 설치됨${
|
|
2544
|
+
isLinkedDev ? ` ${DIM}(dev)${RESET}` : ""
|
|
2545
|
+
}`,
|
|
2546
|
+
);
|
|
2547
|
+
addDoctorCheck(report, {
|
|
2548
|
+
name: "dev-deps",
|
|
2549
|
+
status: "ok",
|
|
2550
|
+
total: runtimeDeps.length,
|
|
2551
|
+
linkedDev: isLinkedDev,
|
|
2552
|
+
});
|
|
2553
|
+
} else {
|
|
2554
|
+
const head = missingDeps.slice(0, 5);
|
|
2555
|
+
const tail = missingDeps.length > 5 ? `+${missingDeps.length - 5}` : "";
|
|
2556
|
+
warn(
|
|
2557
|
+
`누락 의존성 ${missingDeps.length}개: ${head.join(", ")}${tail ? ` ${tail}` : ""}`,
|
|
2558
|
+
);
|
|
2559
|
+
info("수정: tfx doctor --fix 또는 `npm install` (PKG_ROOT 내)");
|
|
2560
|
+
addDoctorCheck(report, {
|
|
2561
|
+
name: "dev-deps",
|
|
2562
|
+
status: "missing",
|
|
2563
|
+
missing: missingDeps,
|
|
2564
|
+
linkedDev: isLinkedDev,
|
|
2565
|
+
pkgRoot: PKG_ROOT,
|
|
2566
|
+
fix: "tfx doctor --fix",
|
|
2567
|
+
});
|
|
2568
|
+
issues++;
|
|
2569
|
+
if (fix) {
|
|
2570
|
+
// --fix 모드: npm install 실행 (Windows 호환 shell: true)
|
|
2571
|
+
info(`npm install 실행 중 (${PKG_ROOT})...`);
|
|
2572
|
+
try {
|
|
2573
|
+
const { execFileSync } = await import("node:child_process");
|
|
2574
|
+
execFileSync("npm", ["install", "--no-audit", "--no-fund"], {
|
|
2575
|
+
cwd: PKG_ROOT,
|
|
2576
|
+
stdio: "inherit",
|
|
2577
|
+
shell: process.platform === "win32",
|
|
2578
|
+
});
|
|
2579
|
+
ok("npm install 완료 — 의존성 복구됨");
|
|
2580
|
+
} catch (err) {
|
|
2581
|
+
warn(`npm install 실패: ${err?.message || err}`);
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
} catch (err) {
|
|
2586
|
+
warn(`dev 의존성 체크 실패: ${err?.message || err}`);
|
|
2587
|
+
addDoctorCheck(report, {
|
|
2588
|
+
name: "dev-deps",
|
|
2589
|
+
status: "error",
|
|
2590
|
+
error: String(err?.message || err),
|
|
2591
|
+
});
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2507
2594
|
// 9. 플러그인 등록
|
|
2508
2595
|
section("Plugin");
|
|
2509
2596
|
const pluginsFile = join(CLAUDE_DIR, "plugins", "installed_plugins.json");
|
|
@@ -4849,13 +4936,35 @@ async function cmdHub(args = [], options = {}) {
|
|
|
4849
4936
|
});
|
|
4850
4937
|
}
|
|
4851
4938
|
|
|
4939
|
+
// Issue #102: spawn stderr 를 임시 파일로 캡처해 실패 시 root cause 노출.
|
|
4940
|
+
// detached spawn 은 pipe 유지가 까다로우니 fd 리다이렉트로 접근.
|
|
4941
|
+
const { openSync: _openSync, closeSync: _closeSync } = await import(
|
|
4942
|
+
"node:fs"
|
|
4943
|
+
);
|
|
4944
|
+
const { tmpdir: _tmpdir } = await import("node:os");
|
|
4945
|
+
const startupErrPath = join(
|
|
4946
|
+
_tmpdir(),
|
|
4947
|
+
`tfx-hub-start-${Date.now()}-${process.pid}.err`,
|
|
4948
|
+
);
|
|
4949
|
+
let errFd;
|
|
4950
|
+
try {
|
|
4951
|
+
errFd = _openSync(startupErrPath, "w");
|
|
4952
|
+
} catch {
|
|
4953
|
+
errFd = undefined;
|
|
4954
|
+
}
|
|
4955
|
+
|
|
4852
4956
|
const child = spawn(process.execPath, [serverPath], {
|
|
4853
4957
|
env: { ...process.env, TFX_HUB_PORT: port },
|
|
4854
|
-
stdio: "ignore",
|
|
4958
|
+
stdio: ["ignore", "ignore", errFd ?? "ignore"],
|
|
4855
4959
|
detached: true,
|
|
4856
4960
|
windowsHide: true,
|
|
4857
4961
|
});
|
|
4858
4962
|
child.unref();
|
|
4963
|
+
if (errFd !== undefined) {
|
|
4964
|
+
try {
|
|
4965
|
+
_closeSync(errFd);
|
|
4966
|
+
} catch {}
|
|
4967
|
+
}
|
|
4859
4968
|
|
|
4860
4969
|
// PID 파일 확인 (최대 3초 대기, 100ms 폴링)
|
|
4861
4970
|
let started = false;
|
|
@@ -4879,13 +4988,46 @@ async function cmdHub(args = [], options = {}) {
|
|
|
4879
4988
|
console.log("");
|
|
4880
4989
|
autoRegisterMcp(hubInfo.url, { codexEnabled: true });
|
|
4881
4990
|
console.log("");
|
|
4991
|
+
// 성공했으면 임시 stderr 파일 정리
|
|
4992
|
+
try {
|
|
4993
|
+
unlinkSync(startupErrPath);
|
|
4994
|
+
} catch {}
|
|
4882
4995
|
} else {
|
|
4883
|
-
//
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4996
|
+
// Issue #102: 캡처된 stderr 에서 root cause 추출
|
|
4997
|
+
let rootCause = "";
|
|
4998
|
+
try {
|
|
4999
|
+
rootCause = readFileSync(startupErrPath, "utf8").trim();
|
|
5000
|
+
} catch {}
|
|
5001
|
+
|
|
5002
|
+
console.log(`\n ${YELLOW}⚠${RESET} 백그라운드 시작 실패`);
|
|
5003
|
+
|
|
5004
|
+
if (rootCause) {
|
|
5005
|
+
// 가장 유용한 에러 라인 강조 (ERR_*, Error:, throw)
|
|
5006
|
+
const highlight = rootCause
|
|
5007
|
+
.split(/\r?\n/)
|
|
5008
|
+
.find((line) => /ERR_[A-Z_]+|^Error:|cannot find/i.test(line));
|
|
5009
|
+
if (highlight) {
|
|
5010
|
+
console.log(` ${RED}▸ ${highlight.trim()}${RESET}`);
|
|
5011
|
+
}
|
|
5012
|
+
console.log(`\n ${DIM}전체 로그: ${startupErrPath}${RESET}`);
|
|
5013
|
+
// 원인별 실전 힌트
|
|
5014
|
+
if (/Cannot find package/i.test(rootCause)) {
|
|
5015
|
+
console.log(
|
|
5016
|
+
` ${DIM}힌트: \`cd ${PKG_ROOT} && npm install\` 로 의존성 복구 (특히 \`npm link\` 환경).${RESET}`,
|
|
5017
|
+
);
|
|
5018
|
+
} else if (/EADDRINUSE/i.test(rootCause)) {
|
|
5019
|
+
console.log(
|
|
5020
|
+
` ${DIM}힌트: 포트 ${port} 이 이미 사용 중. \`tfx hub stop\` 후 재시도.${RESET}`,
|
|
5021
|
+
);
|
|
5022
|
+
}
|
|
5023
|
+
} else {
|
|
5024
|
+
console.log(
|
|
5025
|
+
` ${DIM}stderr 캡처 실패 — 아래 명령으로 포그라운드 실행해 원인 확인:${RESET}`,
|
|
5026
|
+
);
|
|
5027
|
+
}
|
|
5028
|
+
|
|
4887
5029
|
console.log(
|
|
4888
|
-
|
|
5030
|
+
`\n ${DIM}포그라운드 실행: TFX_HUB_PORT=${port} node ${serverPath}${RESET}\n`,
|
|
4889
5031
|
);
|
|
4890
5032
|
}
|
|
4891
5033
|
break;
|
|
@@ -5253,9 +5395,15 @@ async function main() {
|
|
|
5253
5395
|
const auditScript = join(PKG_ROOT, "scripts", "config-audit.mjs");
|
|
5254
5396
|
const auditArgs = JSON_OUTPUT ? ["--json"] : [];
|
|
5255
5397
|
try {
|
|
5256
|
-
const out = execFileSync(
|
|
5257
|
-
|
|
5258
|
-
|
|
5398
|
+
const out = execFileSync(
|
|
5399
|
+
process.execPath,
|
|
5400
|
+
[auditScript, ...auditArgs],
|
|
5401
|
+
{
|
|
5402
|
+
timeout: 15000,
|
|
5403
|
+
encoding: "utf8",
|
|
5404
|
+
windowsHide: true,
|
|
5405
|
+
},
|
|
5406
|
+
);
|
|
5259
5407
|
process.stdout.write(out);
|
|
5260
5408
|
} catch (e) {
|
|
5261
5409
|
process.stdout.write(e.stdout || "");
|
|
@@ -5268,8 +5416,12 @@ async function main() {
|
|
|
5268
5416
|
const result = await diagnose({ json: JSON_OUTPUT });
|
|
5269
5417
|
if (!JSON_OUTPUT) {
|
|
5270
5418
|
if (result.ok) {
|
|
5271
|
-
console.log(
|
|
5272
|
-
|
|
5419
|
+
console.log(
|
|
5420
|
+
`\n ${GREEN_BRIGHT}✓${RESET} 진단 번들 생성: ${result.zipPath}`,
|
|
5421
|
+
);
|
|
5422
|
+
console.log(
|
|
5423
|
+
` spawn 이벤트: ${result.traceCount}건, 훅 타이밍: ${result.hookTimingCount}건\n`,
|
|
5424
|
+
);
|
|
5273
5425
|
} else {
|
|
5274
5426
|
console.log(`\n ${RED}✗${RESET} 진단 실패: ${result.error}\n`);
|
|
5275
5427
|
}
|
|
@@ -5397,9 +5549,7 @@ async function main() {
|
|
|
5397
5549
|
return;
|
|
5398
5550
|
}
|
|
5399
5551
|
case "synapse": {
|
|
5400
|
-
const { cmdSynapseStatus } = await import(
|
|
5401
|
-
"../hub/team/synapse-cli.mjs"
|
|
5402
|
-
);
|
|
5552
|
+
const { cmdSynapseStatus } = await import("../hub/team/synapse-cli.mjs");
|
|
5403
5553
|
const sub = cmdArgs[0] || "status";
|
|
5404
5554
|
if (sub !== "status") {
|
|
5405
5555
|
throw createCliError(`synapse 서브커맨드 미지원: ${sub}`, {
|
package/hooks/keyword-rules.json
CHANGED
|
@@ -11,12 +11,7 @@
|
|
|
11
11
|
"skill": null,
|
|
12
12
|
"action": "suppress_all",
|
|
13
13
|
"priority": 0,
|
|
14
|
-
"supersedes": [
|
|
15
|
-
"tfx-multi",
|
|
16
|
-
"tfx-unified",
|
|
17
|
-
"tfx-codex",
|
|
18
|
-
"tfx-gemini"
|
|
19
|
-
],
|
|
14
|
+
"supersedes": ["tfx-multi", "tfx-unified", "tfx-codex", "tfx-gemini"],
|
|
20
15
|
"exclusive": true,
|
|
21
16
|
"state": null,
|
|
22
17
|
"mcp_route": null
|
|
@@ -62,9 +57,7 @@
|
|
|
62
57
|
],
|
|
63
58
|
"skill": "tfx-swarm",
|
|
64
59
|
"priority": 1,
|
|
65
|
-
"supersedes": [
|
|
66
|
-
"tfx-codex"
|
|
67
|
-
],
|
|
60
|
+
"supersedes": ["tfx-codex"],
|
|
68
61
|
"exclusive": false,
|
|
69
62
|
"state": null,
|
|
70
63
|
"mcp_route": null
|
|
@@ -111,9 +104,7 @@
|
|
|
111
104
|
],
|
|
112
105
|
"skill": "tfx-auto",
|
|
113
106
|
"priority": 2,
|
|
114
|
-
"supersedes": [
|
|
115
|
-
"tfx-auto-codex"
|
|
116
|
-
],
|
|
107
|
+
"supersedes": ["tfx-auto-codex"],
|
|
117
108
|
"exclusive": false,
|
|
118
109
|
"state": null,
|
|
119
110
|
"mcp_route": null
|
|
@@ -249,9 +240,7 @@
|
|
|
249
240
|
],
|
|
250
241
|
"skill": "tfx-wt",
|
|
251
242
|
"priority": 1,
|
|
252
|
-
"supersedes": [
|
|
253
|
-
"tfx-unified"
|
|
254
|
-
],
|
|
243
|
+
"supersedes": ["tfx-unified"],
|
|
255
244
|
"exclusive": false,
|
|
256
245
|
"state": null,
|
|
257
246
|
"mcp_route": null
|
|
@@ -270,9 +259,7 @@
|
|
|
270
259
|
],
|
|
271
260
|
"skill": "tfx-wt",
|
|
272
261
|
"priority": 1,
|
|
273
|
-
"supersedes": [
|
|
274
|
-
"tfx-unified"
|
|
275
|
-
],
|
|
262
|
+
"supersedes": ["tfx-unified"],
|
|
276
263
|
"exclusive": false,
|
|
277
264
|
"state": null,
|
|
278
265
|
"mcp_route": null
|
|
@@ -291,9 +278,7 @@
|
|
|
291
278
|
],
|
|
292
279
|
"skill": "tfx-wt",
|
|
293
280
|
"priority": 1,
|
|
294
|
-
"supersedes": [
|
|
295
|
-
"tfx-unified"
|
|
296
|
-
],
|
|
281
|
+
"supersedes": ["tfx-unified"],
|
|
297
282
|
"exclusive": false,
|
|
298
283
|
"state": null,
|
|
299
284
|
"mcp_route": null
|
|
@@ -312,9 +297,7 @@
|
|
|
312
297
|
],
|
|
313
298
|
"skill": "tfx-wt",
|
|
314
299
|
"priority": 1,
|
|
315
|
-
"supersedes": [
|
|
316
|
-
"tfx-unified"
|
|
317
|
-
],
|
|
300
|
+
"supersedes": ["tfx-unified"],
|
|
318
301
|
"exclusive": false,
|
|
319
302
|
"state": null,
|
|
320
303
|
"mcp_route": null
|
|
@@ -38,9 +38,15 @@ async function runBlocking(stdinData) {
|
|
|
38
38
|
timings.push({ hook: "setup.critical", dur_ms: Math.round(dur) });
|
|
39
39
|
if (result?.stdout) output.stdout += result.stdout + "\n";
|
|
40
40
|
if (result?.stderr) output.stderr += result.stderr + "\n";
|
|
41
|
-
log.info(
|
|
41
|
+
log.info(
|
|
42
|
+
{ hook: "setup.critical", dur_ms: Math.round(dur) },
|
|
43
|
+
"hook.completed",
|
|
44
|
+
);
|
|
42
45
|
} catch (err) {
|
|
43
|
-
log.error(
|
|
46
|
+
log.error(
|
|
47
|
+
{ hook: "setup.critical", err: String(err.message || err) },
|
|
48
|
+
"hook.failed",
|
|
49
|
+
);
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
// 2. mcp-safety-guard.run — EPERM 방지
|
|
@@ -50,9 +56,15 @@ async function runBlocking(stdinData) {
|
|
|
50
56
|
guard.run();
|
|
51
57
|
const dur = performance.now() - t0;
|
|
52
58
|
timings.push({ hook: "mcp-safety-guard", dur_ms: Math.round(dur) });
|
|
53
|
-
log.info(
|
|
59
|
+
log.info(
|
|
60
|
+
{ hook: "mcp-safety-guard", dur_ms: Math.round(dur) },
|
|
61
|
+
"hook.completed",
|
|
62
|
+
);
|
|
54
63
|
} catch (err) {
|
|
55
|
-
log.error(
|
|
64
|
+
log.error(
|
|
65
|
+
{ hook: "mcp-safety-guard", err: String(err.message || err) },
|
|
66
|
+
"hook.failed",
|
|
67
|
+
);
|
|
56
68
|
}
|
|
57
69
|
|
|
58
70
|
// 3. hub-ensure — Hub 필수 인프라, BLOCKING으로 실행
|
|
@@ -65,12 +77,21 @@ async function runBlocking(stdinData) {
|
|
|
65
77
|
if (result?.stdout) output.stdout += result.stdout + "\n";
|
|
66
78
|
if (result?.stderr) output.stderr += result.stderr + "\n";
|
|
67
79
|
if (result?.code !== 0) {
|
|
68
|
-
log.warn(
|
|
80
|
+
log.warn(
|
|
81
|
+
{ hook: "hub-ensure", dur_ms: Math.round(dur), code: result?.code },
|
|
82
|
+
"hook.warn",
|
|
83
|
+
);
|
|
69
84
|
} else {
|
|
70
|
-
log.info(
|
|
85
|
+
log.info(
|
|
86
|
+
{ hook: "hub-ensure", dur_ms: Math.round(dur) },
|
|
87
|
+
"hook.completed",
|
|
88
|
+
);
|
|
71
89
|
}
|
|
72
90
|
} catch (err) {
|
|
73
|
-
log.error(
|
|
91
|
+
log.error(
|
|
92
|
+
{ hook: "hub-ensure", err: String(err.message || err) },
|
|
93
|
+
"hook.failed",
|
|
94
|
+
);
|
|
74
95
|
}
|
|
75
96
|
|
|
76
97
|
return { ...output, timings };
|
|
@@ -96,7 +117,9 @@ function runDeferred(stdinData) {
|
|
|
96
117
|
const mod = await importMod(join(SCRIPTS, "claude-login-detect.mjs"));
|
|
97
118
|
const result = mod.run?.();
|
|
98
119
|
if (result?.changed) {
|
|
99
|
-
return {
|
|
120
|
+
return {
|
|
121
|
+
stdout: `[claude-login] HUD 캐시 ${result.cleared}개 초기화됨\n`,
|
|
122
|
+
};
|
|
100
123
|
}
|
|
101
124
|
},
|
|
102
125
|
},
|
|
@@ -118,14 +141,25 @@ function runDeferred(stdinData) {
|
|
|
118
141
|
|
|
119
142
|
for (const task of tasks) {
|
|
120
143
|
const t0 = performance.now();
|
|
121
|
-
task
|
|
144
|
+
task
|
|
145
|
+
.fn()
|
|
122
146
|
.then((result) => {
|
|
123
147
|
const dur = performance.now() - t0;
|
|
124
|
-
log.info(
|
|
148
|
+
log.info(
|
|
149
|
+
{ hook: task.name, dur_ms: Math.round(dur), code: result?.code },
|
|
150
|
+
"deferred.completed",
|
|
151
|
+
);
|
|
125
152
|
})
|
|
126
153
|
.catch((err) => {
|
|
127
154
|
const dur = performance.now() - t0;
|
|
128
|
-
log.error(
|
|
155
|
+
log.error(
|
|
156
|
+
{
|
|
157
|
+
hook: task.name,
|
|
158
|
+
dur_ms: Math.round(dur),
|
|
159
|
+
err: String(err.message || err),
|
|
160
|
+
},
|
|
161
|
+
"deferred.failed",
|
|
162
|
+
);
|
|
129
163
|
});
|
|
130
164
|
}
|
|
131
165
|
}
|
|
@@ -162,7 +196,15 @@ export async function execute(stdinData, externalHooks = []) {
|
|
|
162
196
|
runBackground(stdinData);
|
|
163
197
|
|
|
164
198
|
const totalDur = performance.now() - totalStart;
|
|
165
|
-
log.info(
|
|
199
|
+
log.info(
|
|
200
|
+
{
|
|
201
|
+
total_ms: Math.round(totalDur),
|
|
202
|
+
blocking_count: 3,
|
|
203
|
+
deferred_count: 2,
|
|
204
|
+
bg_count: 1,
|
|
205
|
+
},
|
|
206
|
+
"session-start.done",
|
|
207
|
+
);
|
|
166
208
|
|
|
167
209
|
return {
|
|
168
210
|
stdout: blocking.stdout,
|
package/hub/cli-adapter-base.mjs
CHANGED
|
@@ -17,18 +17,28 @@ const FALLBACK_COOLDOWN_MS = { codex: 5 * 3600_000, gemini: 86400_000 };
|
|
|
17
17
|
*/
|
|
18
18
|
export function parseRetryAfterMs(text, provider) {
|
|
19
19
|
// 1. ISO timestamp: "try again at 2026-04-11T10:00:00"
|
|
20
|
-
const isoMatch = text.match(
|
|
20
|
+
const isoMatch = text.match(
|
|
21
|
+
/(?:try again|retry|available|resets?)\s+(?:at|after)\s+(\d{4}-\d{2}-\d{2}T[\d:.]+Z?)/i,
|
|
22
|
+
);
|
|
21
23
|
if (isoMatch) {
|
|
22
24
|
const target = new Date(isoMatch[1]).getTime();
|
|
23
25
|
if (target > Date.now()) return target - Date.now();
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
// 2. Duration: "N seconds/minutes/hours/days"
|
|
27
|
-
const durMatch = text.match(
|
|
29
|
+
const durMatch = text.match(
|
|
30
|
+
/(?:retry|wait|resets?|again)\s+(?:in\s+|after\s+)?(\d+)\s*(second|minute|hour|day|week)/i,
|
|
31
|
+
);
|
|
28
32
|
if (durMatch) {
|
|
29
33
|
const n = Number(durMatch[1]);
|
|
30
34
|
const unit = durMatch[2].toLowerCase();
|
|
31
|
-
const multipliers = {
|
|
35
|
+
const multipliers = {
|
|
36
|
+
second: 1000,
|
|
37
|
+
minute: 60_000,
|
|
38
|
+
hour: 3600_000,
|
|
39
|
+
day: 86400_000,
|
|
40
|
+
week: 604800_000,
|
|
41
|
+
};
|
|
32
42
|
return n * (multipliers[unit] || 3600_000);
|
|
33
43
|
}
|
|
34
44
|
|
|
@@ -37,7 +47,12 @@ export function parseRetryAfterMs(text, provider) {
|
|
|
37
47
|
if (standaloneMatch) {
|
|
38
48
|
const n = Number(standaloneMatch[1]);
|
|
39
49
|
const unit = standaloneMatch[2].toLowerCase();
|
|
40
|
-
const multipliers = {
|
|
50
|
+
const multipliers = {
|
|
51
|
+
minute: 60_000,
|
|
52
|
+
hour: 3600_000,
|
|
53
|
+
day: 86400_000,
|
|
54
|
+
week: 604800_000,
|
|
55
|
+
};
|
|
41
56
|
const parsed = n * (multipliers[unit] || 3600_000);
|
|
42
57
|
if (parsed >= 3600_000) return parsed; // 1시간 이상만 신뢰
|
|
43
58
|
}
|
|
@@ -286,7 +301,12 @@ export async function executeWithCircuitBroker({
|
|
|
286
301
|
const text = `${lastResult.output || ""}\n${lastResult.stderr || ""}`;
|
|
287
302
|
const coolMs = parseRetryAfterMs(text, provider);
|
|
288
303
|
brokerMod.broker.markRateLimited(lease.id, coolMs);
|
|
289
|
-
brokerMod.broker.emit("cooldown", {
|
|
304
|
+
brokerMod.broker.emit("cooldown", {
|
|
305
|
+
id: lease.id,
|
|
306
|
+
provider,
|
|
307
|
+
coolMs,
|
|
308
|
+
reason: "quota_exhausted",
|
|
309
|
+
});
|
|
290
310
|
} else {
|
|
291
311
|
brokerMod.broker.release(lease.id, { ok: false });
|
|
292
312
|
}
|
package/hub/lib/env-detect.mjs
CHANGED
|
@@ -21,18 +21,36 @@ export function detectShell() {
|
|
|
21
21
|
|
|
22
22
|
if (platform === "win32") {
|
|
23
23
|
try {
|
|
24
|
-
const path = execFileSync("where", ["pwsh.exe"], PIPE_OPTS)
|
|
24
|
+
const path = execFileSync("where", ["pwsh.exe"], PIPE_OPTS)
|
|
25
|
+
.trim()
|
|
26
|
+
.split(/\r?\n/)[0];
|
|
25
27
|
let version = null;
|
|
26
28
|
try {
|
|
27
|
-
version = execFileSync(
|
|
29
|
+
version = execFileSync(
|
|
30
|
+
path,
|
|
31
|
+
[
|
|
32
|
+
"-NoLogo",
|
|
33
|
+
"-NoProfile",
|
|
34
|
+
"-Command",
|
|
35
|
+
"$PSVersionTable.PSVersion.ToString()",
|
|
36
|
+
],
|
|
37
|
+
PIPE_OPTS,
|
|
38
|
+
).trim();
|
|
28
39
|
} catch {}
|
|
29
40
|
_shellCache = { name: "pwsh", path, version };
|
|
30
41
|
} catch {
|
|
31
42
|
try {
|
|
32
|
-
const path = execFileSync("where", ["powershell.exe"], PIPE_OPTS)
|
|
43
|
+
const path = execFileSync("where", ["powershell.exe"], PIPE_OPTS)
|
|
44
|
+
.trim()
|
|
45
|
+
.split(/\r?\n/)[0];
|
|
33
46
|
_shellCache = { name: "powershell", path, version: null };
|
|
34
47
|
} catch {
|
|
35
|
-
_shellCache = {
|
|
48
|
+
_shellCache = {
|
|
49
|
+
name: "powershell",
|
|
50
|
+
path: "",
|
|
51
|
+
version: null,
|
|
52
|
+
installHint: "pwsh: winget install Microsoft.PowerShell",
|
|
53
|
+
};
|
|
36
54
|
}
|
|
37
55
|
}
|
|
38
56
|
return _shellCache;
|
|
@@ -60,7 +78,11 @@ export function detectTerminal() {
|
|
|
60
78
|
execFileSync("where", ["wt.exe"], PIPE_OPTS);
|
|
61
79
|
_terminalCache = { name: "windows-terminal", hasWt: true };
|
|
62
80
|
} catch {
|
|
63
|
-
_terminalCache = {
|
|
81
|
+
_terminalCache = {
|
|
82
|
+
name: "unknown",
|
|
83
|
+
hasWt: false,
|
|
84
|
+
installHint: "wt: winget install Microsoft.WindowsTerminal",
|
|
85
|
+
};
|
|
64
86
|
}
|
|
65
87
|
return _terminalCache;
|
|
66
88
|
}
|
|
@@ -92,10 +114,15 @@ export function detectMultiplexer() {
|
|
|
92
114
|
const path = execFileSync(cmd, ["tmux"], PIPE_OPTS).trim();
|
|
93
115
|
_multiplexerCache = { name: "tmux", path };
|
|
94
116
|
} catch {
|
|
95
|
-
const hint =
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
117
|
+
const hint =
|
|
118
|
+
osPlatform() === "win32"
|
|
119
|
+
? "tmux: install tmux in WSL or MSYS2"
|
|
120
|
+
: undefined;
|
|
121
|
+
_multiplexerCache = {
|
|
122
|
+
name: "none",
|
|
123
|
+
path: null,
|
|
124
|
+
...(hint ? { installHint: hint } : {}),
|
|
125
|
+
};
|
|
99
126
|
}
|
|
100
127
|
return _multiplexerCache;
|
|
101
128
|
}
|
package/hub/pipe.mjs
CHANGED
|
@@ -674,7 +674,10 @@ export function createPipeServer({
|
|
|
674
674
|
const frame = safeJsonParse(line);
|
|
675
675
|
await handleFrame(client, frame);
|
|
676
676
|
} catch (err) {
|
|
677
|
-
pipeLog.error(
|
|
677
|
+
pipeLog.error(
|
|
678
|
+
{ clientId: client.id, err: String(err?.message || err) },
|
|
679
|
+
"pipe.frame_handler_error",
|
|
680
|
+
);
|
|
678
681
|
}
|
|
679
682
|
}
|
|
680
683
|
newlineIndex = client.buffer.indexOf("\n");
|
package/hub/platform.mjs
CHANGED
|
@@ -178,7 +178,9 @@ export function killProcess(pid, options = {}) {
|
|
|
178
178
|
if (tree) {
|
|
179
179
|
try {
|
|
180
180
|
execSync(`pkill -P ${numericPid}`, { stdio: "ignore", timeout: 3000 });
|
|
181
|
-
} catch {
|
|
181
|
+
} catch {
|
|
182
|
+
/* 자식 없으면 무시 */
|
|
183
|
+
}
|
|
182
184
|
}
|
|
183
185
|
process.kill(numericPid, signal);
|
|
184
186
|
return true;
|