triflux 10.13.10 → 10.14.1
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 +90 -3
- package/hub/team/conductor.mjs +2 -0
- package/hub/team/health-probe.mjs +56 -0
- package/hub/team/retry-state-machine.mjs +2 -2
- package/package.json +1 -1
- package/scripts/hub-ensure.mjs +12 -7
- package/scripts/lib/mcp-health.mjs +770 -0
- package/scripts/setup.mjs +117 -1
- package/scripts/sync-hub-mcp-settings.mjs +63 -22
- package/scripts/tfx-route.sh +269 -19
- package/skills/tfx-auto/SKILL.md +3 -3
- package/skills/tfx-profile/SKILL.md +8 -4
package/bin/triflux.mjs
CHANGED
|
@@ -61,6 +61,7 @@ import {
|
|
|
61
61
|
probePsmuxSupport,
|
|
62
62
|
} from "../scripts/lib/psmux-info.mjs";
|
|
63
63
|
import {
|
|
64
|
+
buildWindowsHubAutostartCommand,
|
|
64
65
|
cleanupStaleSkills,
|
|
65
66
|
ensureCodexHubServerConfig,
|
|
66
67
|
ensureCodexProfiles,
|
|
@@ -68,6 +69,7 @@ import {
|
|
|
68
69
|
extractManagedHookFilename,
|
|
69
70
|
getManagedRegistryHooks,
|
|
70
71
|
getVersion,
|
|
72
|
+
getWindowsHubAutostartStatus,
|
|
71
73
|
hasProfileSection,
|
|
72
74
|
LEGACY_CODEX_MODELS,
|
|
73
75
|
REQUIRED_CODEX_PROFILES,
|
|
@@ -123,7 +125,7 @@ const NORMALIZED_ARGS = RAW_ARGS.filter((arg) => arg !== "--json");
|
|
|
123
125
|
|
|
124
126
|
const CLI_COMMAND_SCHEMAS = Object.freeze({
|
|
125
127
|
setup: {
|
|
126
|
-
usage: "tfx setup [--dry-run]",
|
|
128
|
+
usage: "tfx setup [--dry-run] [--enable-hub-autostart]",
|
|
127
129
|
description: "파일 동기화 + HUD/MCP 설정",
|
|
128
130
|
options: [
|
|
129
131
|
{
|
|
@@ -131,6 +133,12 @@ const CLI_COMMAND_SCHEMAS = Object.freeze({
|
|
|
131
133
|
type: "boolean",
|
|
132
134
|
description: "실제 변경 없이 예정 작업을 JSON으로 출력",
|
|
133
135
|
},
|
|
136
|
+
{
|
|
137
|
+
name: "--enable-hub-autostart",
|
|
138
|
+
type: "boolean",
|
|
139
|
+
description:
|
|
140
|
+
"Windows 로그인 시 tfx-hub를 보장하는 Task Scheduler 항목 등록",
|
|
141
|
+
},
|
|
134
142
|
],
|
|
135
143
|
},
|
|
136
144
|
doctor: {
|
|
@@ -1115,6 +1123,16 @@ function buildSetupDryRunPlan() {
|
|
|
1115
1123
|
const defaultHubUrl = `http://127.0.0.1:${process.env.TFX_HUB_PORT || "27888"}/mcp`;
|
|
1116
1124
|
actions.push(...previewMcpRegistrationActions(defaultHubUrl));
|
|
1117
1125
|
actions.push(previewStatusLineAction());
|
|
1126
|
+
const autostart = getWindowsHubAutostartStatus();
|
|
1127
|
+
actions.push({
|
|
1128
|
+
type: "hub-autostart",
|
|
1129
|
+
platform: process.platform,
|
|
1130
|
+
taskName: autostart.taskName,
|
|
1131
|
+
change: autostart.supported && !autostart.registered ? "available" : "noop",
|
|
1132
|
+
registered: autostart.registered,
|
|
1133
|
+
command: autostart.supported ? buildWindowsHubAutostartCommand() : null,
|
|
1134
|
+
enableWith: "tfx setup --enable-hub-autostart",
|
|
1135
|
+
});
|
|
1118
1136
|
|
|
1119
1137
|
return {
|
|
1120
1138
|
dry_run: true,
|
|
@@ -1123,7 +1141,12 @@ function buildSetupDryRunPlan() {
|
|
|
1123
1141
|
}
|
|
1124
1142
|
|
|
1125
1143
|
function cmdSetup(options = {}) {
|
|
1126
|
-
const {
|
|
1144
|
+
const {
|
|
1145
|
+
dryRun = false,
|
|
1146
|
+
overrideVersion,
|
|
1147
|
+
skipClaudeMdSync = false,
|
|
1148
|
+
enableHubAutostart = false,
|
|
1149
|
+
} = options;
|
|
1127
1150
|
if (dryRun) {
|
|
1128
1151
|
printJson(buildSetupDryRunPlan());
|
|
1129
1152
|
return;
|
|
@@ -1351,6 +1374,67 @@ function cmdSetup(options = {}) {
|
|
|
1351
1374
|
console.log("");
|
|
1352
1375
|
}
|
|
1353
1376
|
|
|
1377
|
+
if (process.platform === "win32") {
|
|
1378
|
+
const status = getWindowsHubAutostartStatus();
|
|
1379
|
+
if (enableHubAutostart) {
|
|
1380
|
+
try {
|
|
1381
|
+
const script = join(PKG_ROOT, "scripts", "setup.mjs");
|
|
1382
|
+
execFileSync(
|
|
1383
|
+
process.execPath,
|
|
1384
|
+
[script, "--enable-hub-autostart", "--sync"],
|
|
1385
|
+
{
|
|
1386
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1387
|
+
timeout: 10000,
|
|
1388
|
+
windowsHide: true,
|
|
1389
|
+
},
|
|
1390
|
+
);
|
|
1391
|
+
// subprocess silent-catch 회귀 가드: schtasks /Query 로 실제 등록 재검증.
|
|
1392
|
+
const verified = getWindowsHubAutostartStatus();
|
|
1393
|
+
if (verified.registered) {
|
|
1394
|
+
ok(`Hub autostart: ${verified.taskName} 등록됨`);
|
|
1395
|
+
summary.push({
|
|
1396
|
+
item: "Hub autostart",
|
|
1397
|
+
status: "✅",
|
|
1398
|
+
detail: `${verified.taskName} 등록됨`,
|
|
1399
|
+
});
|
|
1400
|
+
} else {
|
|
1401
|
+
warn(
|
|
1402
|
+
"Hub autostart 등록 실패: subprocess 성공했으나 /Query 에서 미발견",
|
|
1403
|
+
);
|
|
1404
|
+
summary.push({
|
|
1405
|
+
item: "Hub autostart",
|
|
1406
|
+
status: "⚠️",
|
|
1407
|
+
detail: "등록 실패 (subprocess silent catch 의심)",
|
|
1408
|
+
});
|
|
1409
|
+
}
|
|
1410
|
+
} catch (error) {
|
|
1411
|
+
warn(`Hub autostart 등록 실패: ${renderErrorMessage(error.message)}`);
|
|
1412
|
+
summary.push({
|
|
1413
|
+
item: "Hub autostart",
|
|
1414
|
+
status: "⚠️",
|
|
1415
|
+
detail: "등록 실패",
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
} else if (status.registered) {
|
|
1419
|
+
ok(`Hub autostart: ${status.taskName} 이미 등록됨`);
|
|
1420
|
+
summary.push({
|
|
1421
|
+
item: "Hub autostart",
|
|
1422
|
+
status: "✅",
|
|
1423
|
+
detail: "이미 등록됨",
|
|
1424
|
+
});
|
|
1425
|
+
} else {
|
|
1426
|
+
warn(
|
|
1427
|
+
"Hub autostart 미등록 — Codex 단독 시작 전 hub가 죽어 있으면 MCP가 실패할 수 있음",
|
|
1428
|
+
);
|
|
1429
|
+
info("등록: tfx setup --enable-hub-autostart");
|
|
1430
|
+
summary.push({
|
|
1431
|
+
item: "Hub autostart",
|
|
1432
|
+
status: "⏭️",
|
|
1433
|
+
detail: "미등록",
|
|
1434
|
+
});
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1354
1438
|
// HUD statusLine 설정
|
|
1355
1439
|
console.log(`${CYAN}[HUD 설정]${RESET}`);
|
|
1356
1440
|
const settingsPath = join(CLAUDE_DIR, "settings.json");
|
|
@@ -5579,7 +5663,10 @@ async function main() {
|
|
|
5579
5663
|
|
|
5580
5664
|
switch (cmd) {
|
|
5581
5665
|
case "setup":
|
|
5582
|
-
cmdSetup({
|
|
5666
|
+
cmdSetup({
|
|
5667
|
+
dryRun: cmdArgs.includes("--dry-run"),
|
|
5668
|
+
enableHubAutostart: cmdArgs.includes("--enable-hub-autostart"),
|
|
5669
|
+
});
|
|
5583
5670
|
return;
|
|
5584
5671
|
case "doctor": {
|
|
5585
5672
|
if (cmdArgs.includes("--audit")) {
|
package/hub/team/conductor.mjs
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
// 기존 cli-adapter-base.mjs:stallThresholdMs(30s)와 headless.mjs:STALL_DEFAULTS(120s)를
|
|
3
3
|
// 4단계 probe 모델로 교체. stdout+stderr 통합 스트림으로 평가 (F3 해결).
|
|
4
4
|
|
|
5
|
+
import { mkdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
8
|
+
|
|
5
9
|
/**
|
|
6
10
|
* Health probe level 정의.
|
|
7
11
|
* L0: Process alive (PID 존재 + exit code 없음)
|
|
@@ -25,6 +29,8 @@ export const PROBE_DEFAULTS = Object.freeze({
|
|
|
25
29
|
l2ThresholdMs: 30_000,
|
|
26
30
|
l3ThresholdMs: 120_000,
|
|
27
31
|
enableL2: false,
|
|
32
|
+
writeStateFile: false,
|
|
33
|
+
stateDir: join(tmpdir(), "tfx-probe"),
|
|
28
34
|
});
|
|
29
35
|
|
|
30
36
|
/**
|
|
@@ -96,6 +102,49 @@ export function createHealthProbe(session, opts = {}) {
|
|
|
96
102
|
inputWaitPattern: null,
|
|
97
103
|
};
|
|
98
104
|
|
|
105
|
+
function getStateFilePath() {
|
|
106
|
+
if (typeof config.stateFile === "string" && config.stateFile.length > 0) {
|
|
107
|
+
return config.stateFile;
|
|
108
|
+
}
|
|
109
|
+
const pid = session.pid;
|
|
110
|
+
if (pid == null || pid <= 0) return null;
|
|
111
|
+
return join(config.stateDir, `${pid}.json`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function deriveState(result) {
|
|
115
|
+
if (result.l0 === "fail") return "exited";
|
|
116
|
+
if (result.l1 === "input_wait") return "input_wait";
|
|
117
|
+
if (result.l2 === "fail") return "mcp_initializing";
|
|
118
|
+
if (result.l1 === "stall") return "stalled";
|
|
119
|
+
if (result.l3 === "timeout") return "reasoning";
|
|
120
|
+
return "active";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function writeState(result) {
|
|
124
|
+
if (!config.writeStateFile && !config.stateFile) return;
|
|
125
|
+
const stateFile = getStateFilePath();
|
|
126
|
+
if (!stateFile) return;
|
|
127
|
+
try {
|
|
128
|
+
mkdirSync(dirname(stateFile), { recursive: true });
|
|
129
|
+
writeFileSync(
|
|
130
|
+
stateFile,
|
|
131
|
+
JSON.stringify(
|
|
132
|
+
{
|
|
133
|
+
pid: session.pid ?? null,
|
|
134
|
+
state: deriveState(result),
|
|
135
|
+
result,
|
|
136
|
+
updatedAt: new Date(result.ts).toISOString(),
|
|
137
|
+
},
|
|
138
|
+
null,
|
|
139
|
+
2,
|
|
140
|
+
) + "\n",
|
|
141
|
+
"utf8",
|
|
142
|
+
);
|
|
143
|
+
} catch {
|
|
144
|
+
// probe state is advisory only.
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
99
148
|
/**
|
|
100
149
|
* L0: Process alive check.
|
|
101
150
|
*/
|
|
@@ -227,6 +276,7 @@ export function createHealthProbe(session, opts = {}) {
|
|
|
227
276
|
ts: Date.now(),
|
|
228
277
|
};
|
|
229
278
|
status.lastProbeAt = result.ts;
|
|
279
|
+
writeState(result);
|
|
230
280
|
|
|
231
281
|
if (typeof config.onProbe === "function") {
|
|
232
282
|
config.onProbe(result);
|
|
@@ -259,6 +309,12 @@ export function createHealthProbe(session, opts = {}) {
|
|
|
259
309
|
clearInterval(timer);
|
|
260
310
|
timer = null;
|
|
261
311
|
}
|
|
312
|
+
if (config.writeStateFile || config.stateFile) {
|
|
313
|
+
try {
|
|
314
|
+
const stateFile = getStateFilePath();
|
|
315
|
+
if (stateFile) unlinkSync(stateFile);
|
|
316
|
+
} catch {}
|
|
317
|
+
}
|
|
262
318
|
}
|
|
263
319
|
|
|
264
320
|
/** L1 tracking 리셋 (restart 후 호출) */
|
|
@@ -39,8 +39,8 @@ export const MODES = Object.freeze({
|
|
|
39
39
|
});
|
|
40
40
|
|
|
41
41
|
const DEFAULT_ESCALATION_CHAIN = Object.freeze([
|
|
42
|
-
Object.freeze({ cli: "codex", model: "gpt-5-mini" }),
|
|
43
|
-
Object.freeze({ cli: "codex", model: "gpt-5" }),
|
|
42
|
+
Object.freeze({ cli: "codex", model: "gpt-5.4-mini" }),
|
|
43
|
+
Object.freeze({ cli: "codex", model: "gpt-5.5" }),
|
|
44
44
|
Object.freeze({ cli: "claude", model: "sonnet-4-6" }),
|
|
45
45
|
Object.freeze({ cli: "claude", model: "opus-4-7" }),
|
|
46
46
|
]);
|
package/package.json
CHANGED
package/scripts/hub-ensure.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import { fileURLToPath } from "url";
|
|
|
13
13
|
const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1"]);
|
|
14
14
|
const PLUGIN_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
15
15
|
const HUB_PID_FILE = join(homedir(), ".claude", "cache", "tfx-hub", "hub.pid");
|
|
16
|
+
const HUB_DEFAULT_PORT = 27888;
|
|
16
17
|
|
|
17
18
|
function formatHostForUrl(host) {
|
|
18
19
|
return host.includes(":") ? `[${host}]` : host;
|
|
@@ -34,29 +35,33 @@ async function syncHubConfigsIfAvailable({ hubUrl }) {
|
|
|
34
35
|
await mod.syncCodexHubUrl({ hubUrl });
|
|
35
36
|
}
|
|
36
37
|
if (typeof mod?.syncProjectMcpJson === "function") {
|
|
37
|
-
|
|
38
|
+
// 사용자 작업 디렉토리의 .mcp.json 을 sync 대상으로 한다.
|
|
39
|
+
// 이전에는 PLUGIN_ROOT(triflux 설치 경로)를 넘겨서 설치 경로의 .mcp.json
|
|
40
|
+
// 만 sync 되고 사용자 실제 프로젝트는 drift 되던 증상이 있었다.
|
|
41
|
+
await mod.syncProjectMcpJson({ hubUrl, projectRoot: process.cwd() });
|
|
38
42
|
}
|
|
39
43
|
} catch {
|
|
40
44
|
// sync는 best-effort이며 hub-ensure 성공/실패를 좌우하지 않는다.
|
|
41
45
|
}
|
|
42
46
|
}
|
|
43
47
|
|
|
44
|
-
function resolveHubTarget() {
|
|
48
|
+
export function resolveHubTarget() {
|
|
45
49
|
const envPortRaw = Number(process.env.TFX_HUB_PORT || "");
|
|
46
50
|
const envPort =
|
|
47
51
|
Number.isFinite(envPortRaw) && envPortRaw > 0 ? envPortRaw : null;
|
|
48
52
|
const target = {
|
|
49
53
|
host: "127.0.0.1",
|
|
50
|
-
port: envPort
|
|
54
|
+
port: envPort ?? HUB_DEFAULT_PORT,
|
|
51
55
|
};
|
|
52
56
|
|
|
57
|
+
// PID 파일의 port는 source of truth가 아니다. host 힌트만 재사용한다.
|
|
58
|
+
// 과거에는 `!envPort`일 때 PID file의 port로 target.port를 덮었으나,
|
|
59
|
+
// 이는 이전 세션의 오염된 port(비표준 포트)가 cascade로 영속화되는 버그 원인이었다.
|
|
60
|
+
// 포트는 오직 TFX_HUB_PORT env(없으면 HUB_DEFAULT_PORT=27888)만 source of truth다.
|
|
61
|
+
// client config 는 sync-hub-mcp-settings.mjs가 이 hubUrl로 재동기화한다.
|
|
53
62
|
if (existsSync(HUB_PID_FILE)) {
|
|
54
63
|
try {
|
|
55
64
|
const info = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
|
|
56
|
-
if (!envPort) {
|
|
57
|
-
const pidPort = Number(info?.port);
|
|
58
|
-
if (Number.isFinite(pidPort) && pidPort > 0) target.port = pidPort;
|
|
59
|
-
}
|
|
60
65
|
if (typeof info?.host === "string") {
|
|
61
66
|
const host = info.host.trim();
|
|
62
67
|
if (LOOPBACK_HOSTS.has(host)) target.host = host;
|