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 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 { dryRun = false, overrideVersion, skipClaudeMdSync = false } = options;
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({ dryRun: cmdArgs.includes("--dry-run") });
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")) {
@@ -696,6 +696,8 @@ export function createConductor(opts = {}) {
696
696
  },
697
697
  {
698
698
  ...probeOpts,
699
+ writeStateFile:
700
+ probeOpts.writeStateFile ?? process.env.TFX_PROBE_WRITE_STATE === "1",
699
701
  onProbe: (result) => handleProbeResult(session, result),
700
702
  },
701
703
  );
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triflux",
3
- "version": "10.13.10",
3
+ "version": "10.14.1",
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": {
@@ -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
- await mod.syncProjectMcpJson({ hubUrl, projectRoot: PLUGIN_ROOT });
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 || 27888,
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;