triflux 9.3.0 → 9.5.0

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triflux",
3
- "version": "9.0.0",
3
+ "version": "9.3.0",
4
4
  "description": "CLI-first multi-model orchestrator for Claude Code — route tasks to Codex, Gemini, and Claude",
5
5
  "author": {
6
6
  "name": "tellang"
package/bin/triflux.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // triflux CLI — setup, doctor, version
3
- import { copyFileSync, existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync, readdirSync, unlinkSync, rmSync, statSync, openSync, closeSync } from "fs";
3
+ import { copyFileSync, existsSync, readFileSync, readSync, writeFileSync, mkdirSync, chmodSync, readdirSync, unlinkSync, statSync, openSync, closeSync } from "fs";
4
4
  import { join, dirname } from "path";
5
5
  import { homedir } from "os";
6
6
  import { execSync, execFileSync, spawn } from "child_process";
@@ -12,6 +12,11 @@ import { forceCleanupTeam } from "../hub/team/nativeProxy.mjs";
12
12
  import { cleanupStaleOmcTeams, inspectStaleOmcTeams } from "../hub/team/staleState.mjs";
13
13
  import { getPipelineStateDbPath } from "../hub/pipeline/state.mjs";
14
14
  import { ensureGeminiProfiles } from "../scripts/lib/gemini-profiles.mjs";
15
+ import {
16
+ SYNC_MAP, SKILL_ALIASES, REQUIRED_CODEX_PROFILES, LEGACY_CODEX_MODELS,
17
+ syncAliasedSkillDir, hasProfileSection, replaceProfileSection,
18
+ ensureCodexProfiles, getVersion, cleanupStaleSkills, DEPRECATED_SKILLS,
19
+ } from "../scripts/setup.mjs";
15
20
 
16
21
  const PKG_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
17
22
  const CLAUDE_DIR = join(homedir(), ".claude");
@@ -22,72 +27,6 @@ const PKG = JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf8"));
22
27
  // 이 배열에 포함된 버전에서만 star prompt를 표시한다 (빈 배열 = 모든 버전에서 표시)
23
28
  const STAR_PROMPT_VERSIONS = [];
24
29
 
25
- const REQUIRED_CODEX_PROFILES = [
26
- {
27
- name: "codex53_high",
28
- lines: [
29
- 'model = "gpt-5.3-codex"',
30
- 'model_reasoning_effort = "high"',
31
- ],
32
- },
33
- {
34
- name: "codex53_xhigh",
35
- lines: [
36
- 'model = "gpt-5.3-codex"',
37
- 'model_reasoning_effort = "xhigh"',
38
- ],
39
- },
40
- {
41
- name: "spark53_low",
42
- lines: [
43
- 'model = "gpt-5.3-codex-spark"',
44
- 'model_reasoning_effort = "low"',
45
- ],
46
- },
47
- ];
48
-
49
- const SKILL_ALIASES = [
50
- {
51
- alias: "tfx-ralph",
52
- source: "tfx-persist",
53
- },
54
- ];
55
-
56
- function buildAliasedSkillContent(srcContent, { alias, source }) {
57
- return srcContent
58
- .replace(/^name:\s*.+$/m, `name: ${alias}`)
59
- .replaceAll(source, alias)
60
- .replace(/^#\s+.+$/m, `# ${alias} — Compatibility Alias for ${source}`);
61
- }
62
-
63
- function syncAliasedSkillDir(srcDir, dstDir, { alias, source }) {
64
- if (!existsSync(dstDir)) mkdirSync(dstDir, { recursive: true });
65
-
66
- let count = 0;
67
- for (const entry of readdirSync(srcDir, { withFileTypes: true })) {
68
- const srcPath = join(srcDir, entry.name);
69
- const dstPath = join(dstDir, entry.name);
70
-
71
- if (entry.isDirectory()) {
72
- count += syncAliasedSkillDir(srcPath, dstPath, { alias, source });
73
- continue;
74
- }
75
-
76
- if (!entry.name.endsWith(".md")) continue;
77
-
78
- const rawContent = readFileSync(srcPath, "utf8");
79
- const nextContent = entry.name === "SKILL.md"
80
- ? buildAliasedSkillContent(rawContent, { alias, source })
81
- : rawContent;
82
-
83
- if (!existsSync(dstPath) || readFileSync(dstPath, "utf8") !== nextContent) {
84
- writeFileSync(dstPath, nextContent, "utf8");
85
- count++;
86
- }
87
- }
88
-
89
- return count;
90
- }
91
30
 
92
31
  // ── 색상 체계 (triflux brand: amber/orange accent) ──
93
32
  const CYAN = "\x1b[36m";
@@ -328,13 +267,6 @@ function checkShellAvailable(shell) {
328
267
  } catch { return false; }
329
268
  }
330
269
 
331
- function getVersion(filePath) {
332
- try {
333
- const content = readFileSync(filePath, "utf8");
334
- const match = content.match(/VERSION\s*=\s*"([^"]+)"/);
335
- return match ? match[1] : null;
336
- } catch { return null; }
337
- }
338
270
 
339
271
  function parseSessionCreated(rawValue) {
340
272
  const value = String(rawValue || "").trim();
@@ -459,57 +391,35 @@ async function cleanupStaleTeamSessions(staleSessions) {
459
391
  return { cleaned, failed };
460
392
  }
461
393
 
462
- function escapeRegExp(value) {
463
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
464
- }
465
-
466
- function hasProfileSection(tomlContent, profileName) {
467
- const section = `^\\[profiles\\.${escapeRegExp(profileName)}\\]\\s*$`;
468
- return new RegExp(section, "m").test(tomlContent);
469
- }
470
-
471
- function ensureCodexProfiles() {
472
- try {
473
- if (!existsSync(CODEX_DIR)) mkdirSync(CODEX_DIR, { recursive: true });
474
-
475
- const original = existsSync(CODEX_CONFIG_PATH)
476
- ? readFileSync(CODEX_CONFIG_PATH, "utf8")
477
- : "";
478
-
479
- let updated = original;
480
- let added = 0;
481
394
 
482
- for (const profile of REQUIRED_CODEX_PROFILES) {
483
- if (hasProfileSection(updated, profile.name)) continue;
395
+ function previewCodexProfiles() {
396
+ const original = existsSync(CODEX_CONFIG_PATH)
397
+ ? readFileSync(CODEX_CONFIG_PATH, "utf8")
398
+ : "";
399
+ let updated = original;
400
+ const profiles = [];
484
401
 
402
+ for (const profile of REQUIRED_CODEX_PROFILES) {
403
+ const before = updated;
404
+ if (hasProfileSection(updated, profile.name)) {
405
+ updated = replaceProfileSection(updated, profile.name, profile.lines);
406
+ } else {
485
407
  if (updated.length > 0 && !updated.endsWith("\n")) updated += "\n";
486
408
  if (updated.trim().length > 0) updated += "\n";
487
409
  updated += `[profiles.${profile.name}]\n${profile.lines.join("\n")}\n`;
488
- added++;
489
410
  }
490
-
491
- if (added > 0) {
492
- writeFileSync(CODEX_CONFIG_PATH, updated, "utf8");
411
+ if (updated !== before) {
412
+ profiles.push(profile.name);
493
413
  }
494
-
495
- return { ok: true, added };
496
- } catch (e) {
497
- return { ok: false, added: 0, message: e.message };
498
414
  }
499
- }
500
415
 
501
- function previewCodexProfiles() {
502
- const original = existsSync(CODEX_CONFIG_PATH)
503
- ? readFileSync(CODEX_CONFIG_PATH, "utf8")
504
- : "";
505
- const missingProfiles = REQUIRED_CODEX_PROFILES
506
- .filter((profile) => !hasProfileSection(original, profile.name))
507
- .map((profile) => profile.name);
416
+ const windowsSandbox = process.platform === "win32" && !updated.includes("[windows]");
508
417
 
509
418
  return {
510
419
  path: CODEX_CONFIG_PATH,
511
- missingProfiles,
512
- change: missingProfiles.length > 0 ? (original ? "update" : "create") : "noop",
420
+ profiles,
421
+ windowsSandbox,
422
+ change: profiles.length > 0 || windowsSandbox ? (original ? "update" : "create") : "noop",
513
423
  };
514
424
  }
515
425
 
@@ -637,101 +547,6 @@ function checkCliCrossShell(cmd, installHint) {
637
547
 
638
548
  // ── 명령어 ──
639
549
 
640
- function getSetupSyncTargets() {
641
- return [
642
- {
643
- src: join(PKG_ROOT, "scripts", "tfx-route.sh"),
644
- dst: join(CLAUDE_DIR, "scripts", "tfx-route.sh"),
645
- label: "tfx-route.sh",
646
- },
647
- {
648
- src: join(PKG_ROOT, "hud", "hud-qos-status.mjs"),
649
- dst: join(CLAUDE_DIR, "hud", "hud-qos-status.mjs"),
650
- label: "hud-qos-status.mjs",
651
- },
652
- {
653
- src: join(PKG_ROOT, "scripts", "notion-read.mjs"),
654
- dst: join(CLAUDE_DIR, "scripts", "notion-read.mjs"),
655
- label: "notion-read.mjs",
656
- },
657
- {
658
- src: join(PKG_ROOT, "scripts", "tfx-route-post.mjs"),
659
- dst: join(CLAUDE_DIR, "scripts", "tfx-route-post.mjs"),
660
- label: "tfx-route-post.mjs",
661
- },
662
- {
663
- src: join(PKG_ROOT, "scripts", "tfx-batch-stats.mjs"),
664
- dst: join(CLAUDE_DIR, "scripts", "tfx-batch-stats.mjs"),
665
- label: "tfx-batch-stats.mjs",
666
- },
667
- {
668
- src: join(PKG_ROOT, "scripts", "lib", "mcp-filter.mjs"),
669
- dst: join(CLAUDE_DIR, "scripts", "lib", "mcp-filter.mjs"),
670
- label: "lib/mcp-filter.mjs",
671
- },
672
- {
673
- src: join(PKG_ROOT, "scripts", "lib", "mcp-server-catalog.mjs"),
674
- dst: join(CLAUDE_DIR, "scripts", "lib", "mcp-server-catalog.mjs"),
675
- label: "lib/mcp-server-catalog.mjs",
676
- },
677
- {
678
- src: join(PKG_ROOT, "scripts", "lib", "keyword-rules.mjs"),
679
- dst: join(CLAUDE_DIR, "scripts", "lib", "keyword-rules.mjs"),
680
- label: "lib/keyword-rules.mjs",
681
- },
682
- {
683
- src: join(PKG_ROOT, "scripts", "lib", "gemini-profiles.mjs"),
684
- dst: join(CLAUDE_DIR, "scripts", "lib", "gemini-profiles.mjs"),
685
- label: "lib/gemini-profiles.mjs",
686
- },
687
- {
688
- src: join(PKG_ROOT, "scripts", "tfx-route-worker.mjs"),
689
- dst: join(CLAUDE_DIR, "scripts", "tfx-route-worker.mjs"),
690
- label: "tfx-route-worker.mjs",
691
- },
692
- {
693
- src: join(PKG_ROOT, "hub", "workers", "codex-mcp.mjs"),
694
- dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "codex-mcp.mjs"),
695
- label: "hub/workers/codex-mcp.mjs",
696
- },
697
- {
698
- src: join(PKG_ROOT, "hub", "workers", "delegator-mcp.mjs"),
699
- dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "delegator-mcp.mjs"),
700
- label: "hub/workers/delegator-mcp.mjs",
701
- },
702
- {
703
- src: join(PKG_ROOT, "hub", "workers", "interface.mjs"),
704
- dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "interface.mjs"),
705
- label: "hub/workers/interface.mjs",
706
- },
707
- {
708
- src: join(PKG_ROOT, "hub", "workers", "gemini-worker.mjs"),
709
- dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "gemini-worker.mjs"),
710
- label: "hub/workers/gemini-worker.mjs",
711
- },
712
- {
713
- src: join(PKG_ROOT, "hub", "workers", "claude-worker.mjs"),
714
- dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "claude-worker.mjs"),
715
- label: "hub/workers/claude-worker.mjs",
716
- },
717
- {
718
- src: join(PKG_ROOT, "hub", "workers", "factory.mjs"),
719
- dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "factory.mjs"),
720
- label: "hub/workers/factory.mjs",
721
- },
722
- {
723
- src: join(PKG_ROOT, "scripts", "remote-spawn.mjs"),
724
- dst: join(CLAUDE_DIR, "scripts", "remote-spawn.mjs"),
725
- label: "remote-spawn.mjs",
726
- },
727
- {
728
- src: join(PKG_ROOT, "hub", "team", "psmux.mjs"),
729
- dst: join(CLAUDE_DIR, "hub", "team", "psmux.mjs"),
730
- label: "hub/team/psmux.mjs",
731
- },
732
- ];
733
- }
734
-
735
550
  function listSkillSyncActions() {
736
551
  const skillsSrc = join(PKG_ROOT, "skills");
737
552
  if (!existsSync(skillsSrc)) return [];
@@ -816,7 +631,7 @@ function previewMcpRegistrationActions(mcpUrl) {
816
631
 
817
632
  function buildSetupDryRunPlan() {
818
633
  const actions = [
819
- ...getSetupSyncTargets().map(({ src, dst, label }) => describeSyncAction(src, dst, label)),
634
+ ...SYNC_MAP.map(({ src, dst, label }) => describeSyncAction(src, dst, label)),
820
635
  ...listSkillSyncActions(),
821
636
  ];
822
637
  const codexProfiles = previewCodexProfiles();
@@ -824,7 +639,8 @@ function buildSetupDryRunPlan() {
824
639
  type: "codex-profiles",
825
640
  path: codexProfiles.path,
826
641
  change: codexProfiles.change,
827
- profiles: codexProfiles.missingProfiles,
642
+ profiles: codexProfiles.profiles,
643
+ windowsSandbox: codexProfiles.windowsSandbox,
828
644
  });
829
645
 
830
646
  const defaultHubUrl = `http://127.0.0.1:${process.env.TFX_HUB_PORT || "27888"}/mcp`;
@@ -846,7 +662,7 @@ function cmdSetup(options = {}) {
846
662
 
847
663
  console.log(`\n${BOLD}triflux setup${RESET}\n`);
848
664
 
849
- for (const target of getSetupSyncTargets()) {
665
+ for (const target of SYNC_MAP) {
850
666
  syncFile(target.src, target.dst, target.label);
851
667
  }
852
668
 
@@ -904,6 +720,11 @@ function cmdSetup(options = {}) {
904
720
  } else {
905
721
  ok(`스킬: ${skillTotal}개 최신 상태`);
906
722
  }
723
+ // Stale 스킬 정리 (패키지에서 제거된 tfx-* 스킬 삭제)
724
+ const staleCleanup = cleanupStaleSkills(skillsDst, skillsSrc);
725
+ if (staleCleanup.count > 0) {
726
+ ok(`구형 스킬 ${staleCleanup.count}개 제거: ${staleCleanup.removed.join(", ")}`);
727
+ }
907
728
  }
908
729
 
909
730
  // ── 결과 추적 ──
@@ -913,9 +734,9 @@ function cmdSetup(options = {}) {
913
734
  if (!codexProfileResult.ok) {
914
735
  warn(`Codex profiles 설정 실패: ${codexProfileResult.message}`);
915
736
  summary.push({ item: "Codex profiles", status: "⚠️", detail: codexProfileResult.message });
916
- } else if (codexProfileResult.added > 0) {
917
- ok(`Codex profiles: ${codexProfileResult.added}개 추가됨 (~/.codex/config.toml)`);
918
- summary.push({ item: "Codex profiles", status: "✅", detail: `${codexProfileResult.added}개 추가됨` });
737
+ } else if (codexProfileResult.changed > 0) {
738
+ ok(`Codex profiles: ${codexProfileResult.changed}개 반영됨 (~/.codex/config.toml)`);
739
+ summary.push({ item: "Codex profiles", status: "✅", detail: `${codexProfileResult.changed}개 반영됨` });
919
740
  } else {
920
741
  ok("Codex profiles: 이미 준비됨");
921
742
  summary.push({ item: "Codex profiles", status: "✅", detail: "이미 준비됨" });
@@ -1006,22 +827,55 @@ function cmdSetup(options = {}) {
1006
827
  }
1007
828
  }
1008
829
 
1009
- // Star request (버전 게이팅)
830
+ // Star request (버전 게이팅 + 인터랙티브 [y/n])
1010
831
  const showStar = STAR_PROMPT_VERSIONS.length === 0 || STAR_PROMPT_VERSIONS.includes(PKG.version);
1011
832
  if (showStar) {
833
+ let ghOk = false;
1012
834
  try {
1013
835
  execFileSync("gh", ["auth", "status"], { timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
836
+ ghOk = true;
837
+ } catch {}
838
+
839
+ if (!ghOk) {
840
+ // gh 미설치/미인증 — URL만 표시
841
+ console.log();
842
+ info(`${AMBER}⭐${RESET} 하나가 큰 차이를 만듭니다. ${CYAN}https://github.com/tellang/triflux${RESET}`);
843
+ } else {
844
+ let alreadyStarred = false;
1014
845
  try {
1015
846
  execFileSync("gh", ["api", "user/starred/tellang/triflux"], { timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
847
+ alreadyStarred = true;
848
+ } catch {}
849
+
850
+ if (alreadyStarred) {
1016
851
  console.log();
1017
852
  ok(`이미 함께하고 계시군요. ${AMBER}⭐${RESET}`);
1018
- } catch {
853
+ } else {
854
+ // 인터랙티브 confirm
1019
855
  console.log();
1020
- info(`${AMBER}⭐${RESET} 하나가 큰 차이를 만듭니다. ${CYAN}https://github.com/tellang/triflux${RESET}`);
856
+ process.stdout.write(` ${AMBER}⭐${RESET} 하나가 큰 차이를 만듭니다. Star? ${DIM}[y/N]${RESET} `);
857
+ let answer = "";
858
+ try {
859
+ const buf = Buffer.alloc(128);
860
+ const n = readSync(0, buf, 0, 128);
861
+ answer = buf.toString("utf8", 0, n).trim().toLowerCase();
862
+ } catch {
863
+ // non-interactive stdin — 건너뜀
864
+ }
865
+ if (answer.startsWith("y")) {
866
+ try {
867
+ execFileSync("gh", ["api", "-X", "PUT", "/user/starred/tellang/triflux"], {
868
+ timeout: 5000, stdio: ["pipe", "pipe", "pipe"],
869
+ });
870
+ ok(`함께해 주셔서 감사합니다. ${AMBER}⭐${RESET}`);
871
+ } catch {
872
+ info(`${CYAN}https://github.com/tellang/triflux${RESET}`);
873
+ }
874
+ } else if (answer === "") {
875
+ // 아무 입력 없이 Enter — 조용히 URL만
876
+ console.log(` ${DIM}https://github.com/tellang/triflux${RESET}`);
877
+ }
1021
878
  }
1022
- } catch {
1023
- console.log();
1024
- info(`${AMBER}⭐${RESET} 하나가 큰 차이를 만듭니다. ${CYAN}https://github.com/tellang/triflux${RESET}`);
1025
879
  }
1026
880
  }
1027
881
 
@@ -1158,7 +1012,7 @@ async function cmdDoctor(options = {}) {
1158
1012
  // ── fix 모드: 파일 동기화 + 캐시 정리 후 진단 ──
1159
1013
  if (fix) {
1160
1014
  section("Auto Fix");
1161
- for (const target of getSetupSyncTargets()) {
1015
+ for (const target of SYNC_MAP) {
1162
1016
  syncFile(target.src, target.dst, target.label);
1163
1017
  }
1164
1018
  // 스킬 동기화
@@ -1182,8 +1036,8 @@ async function cmdDoctor(options = {}) {
1182
1036
  const profileFix = ensureCodexProfiles();
1183
1037
  if (!profileFix.ok) {
1184
1038
  warn(`Codex Profiles 자동 복구 실패: ${profileFix.message}`);
1185
- } else if (profileFix.added > 0) {
1186
- ok(`Codex Profiles: ${profileFix.added}개 추가됨`);
1039
+ } else if (profileFix.changed > 0) {
1040
+ ok(`Codex Profiles: ${profileFix.changed}개 반영됨`);
1187
1041
  } else {
1188
1042
  info("Codex Profiles: 이미 최신 상태");
1189
1043
  }
@@ -1263,7 +1117,9 @@ async function cmdDoctor(options = {}) {
1263
1117
  const missingProfiles = [];
1264
1118
  for (const profile of REQUIRED_CODEX_PROFILES) {
1265
1119
  if (hasProfileSection(codexConfig, profile.name)) {
1266
- ok(`${profile.name}: 정상`);
1120
+ ok(`${profile.name}: 정상${profile.proOnly ? ` ${DIM}(Pro 전용)${RESET}` : ""}`);
1121
+ } else if (profile.proOnly) {
1122
+ info(`${profile.name}: 미설정 ${DIM}(Pro 전용 — Plus/기본에서는 불필요)${RESET}`);
1267
1123
  } else {
1268
1124
  missingProfiles.push(profile.name);
1269
1125
  warn(`${profile.name}: 미설정`);
@@ -1283,6 +1139,18 @@ async function cmdDoctor(options = {}) {
1283
1139
  issues++;
1284
1140
  }
1285
1141
 
1142
+ // Codex 구형 모델 감지
1143
+ if (existsSync(CODEX_CONFIG_PATH)) {
1144
+ const codexContent = readFileSync(CODEX_CONFIG_PATH, "utf8");
1145
+ const legacyFound = LEGACY_CODEX_MODELS.filter(m => codexContent.includes(`"${m}"`));
1146
+ if (legacyFound.length > 0) {
1147
+ warn(`구형 모델 감지: ${legacyFound.join(", ")}`);
1148
+ info("최신 프로필로 마이그레이션: tfx setup 또는 tfx profile");
1149
+ addDoctorCheck(report, { name: "codex-legacy-models", status: "issues", models: legacyFound, fix: "tfx setup" });
1150
+ issues++;
1151
+ }
1152
+ }
1153
+
1286
1154
  // 5. Gemini CLI
1287
1155
  section(`Gemini CLI ${BLUE}●${RESET}`);
1288
1156
  const geminiCli = checkCliCrossShell("gemini", "npm install -g @google/gemini-cli");
@@ -1295,6 +1163,22 @@ async function cmdDoctor(options = {}) {
1295
1163
  });
1296
1164
  // API 키 검사 제거 — bash exec 기반이므로 API 키 불필요
1297
1165
 
1166
+ // Gemini 구형 모델 감지
1167
+ const geminiProfilesPath = join(homedir(), ".gemini", "triflux-profiles.json");
1168
+ const LEGACY_GEMINI_MODELS = ["gemini-2.0-flash", "gemini-1.5-pro", "gemini-1.5-flash", "gemini-2.5-pro-preview"];
1169
+ if (existsSync(geminiProfilesPath)) {
1170
+ try {
1171
+ const geminiContent = readFileSync(geminiProfilesPath, "utf8");
1172
+ const geminiLegacy = LEGACY_GEMINI_MODELS.filter(m => geminiContent.includes(m));
1173
+ if (geminiLegacy.length > 0) {
1174
+ warn(`구형 모델 감지: ${geminiLegacy.join(", ")}`);
1175
+ info("최신 프로필로 마이그레이션: tfx setup 또는 tfx profile");
1176
+ addDoctorCheck(report, { name: "gemini-legacy-models", status: "issues", models: geminiLegacy, fix: "tfx setup" });
1177
+ issues++;
1178
+ }
1179
+ } catch {}
1180
+ }
1181
+
1298
1182
  // 6. Claude Code
1299
1183
  section(`Claude Code ${AMBER}●${RESET}`);
1300
1184
  const claudePath = which("claude");
@@ -1307,7 +1191,38 @@ async function cmdDoctor(options = {}) {
1307
1191
  issues++;
1308
1192
  }
1309
1193
 
1310
- // 7. 스킬 설치 상태
1194
+ // 7. psmux (Windows only)
1195
+ if (process.platform === "win32") {
1196
+ section("psmux (터미널 멀티플렉서)");
1197
+ const psmuxPath = which("psmux");
1198
+ if (psmuxPath) {
1199
+ ok("설치됨");
1200
+ // 기본 셸 확인: psmux 세션의 기본 셸이 PowerShell인지 cmd.exe인지
1201
+ let shellOk = false;
1202
+ try {
1203
+ const defaultShell = execSync("psmux show-options -g default-shell 2>NUL", { encoding: "utf8", timeout: 3000 }).trim();
1204
+ shellOk = /powershell|pwsh/i.test(defaultShell);
1205
+ } catch {
1206
+ // show-options 실패 시 pwsh/powershell 존재 여부로 판단
1207
+ shellOk = !!which("pwsh") || !!which("powershell.exe");
1208
+ }
1209
+ if (shellOk) {
1210
+ ok("기본 셸: PowerShell");
1211
+ addDoctorCheck(report, { name: "psmux", status: "ok", path: psmuxPath, shell: "powershell" });
1212
+ } else {
1213
+ warn("기본 셸이 cmd.exe — headless 명령 실패 가능");
1214
+ info("수정: psmux set-option -g default-shell \"powershell.exe\"");
1215
+ addDoctorCheck(report, { name: "psmux", status: "issues", path: psmuxPath, shell: "cmd", fix: 'psmux set-option -g default-shell "powershell.exe"' });
1216
+ issues++;
1217
+ }
1218
+ } else {
1219
+ info(`미설치 ${GRAY}(선택 — 멀티모델 병렬 실행에 필요)${RESET}`);
1220
+ info(`설치: winget install marlocarlo.psmux`);
1221
+ addDoctorCheck(report, { name: "psmux", status: "skipped", detail: "미설치 (선택)", fix: "winget install marlocarlo.psmux" });
1222
+ }
1223
+ }
1224
+
1225
+ // 8. 스킬 설치 상태
1311
1226
  section("Skills");
1312
1227
  const skillsSrc = join(PKG_ROOT, "skills");
1313
1228
  const skillsDst = join(CLAUDE_DIR, "skills");
@@ -1337,7 +1252,32 @@ async function cmdDoctor(options = {}) {
1337
1252
  addDoctorCheck(report, { name: "skills", status: "missing", installed: 0, total: 0, fix: "패키지 skills 디렉토리를 확인하세요." });
1338
1253
  }
1339
1254
 
1340
- // 8. 플러그인 등록
1255
+ // Stale 스킬 체크
1256
+ const staleSkills = [];
1257
+ const userSkillsDir = join(CLAUDE_DIR, "skills");
1258
+ if (existsSync(userSkillsDir)) {
1259
+ const pkgSkillsDir = join(PKG_ROOT, "skills");
1260
+ const pkgSkills = new Set();
1261
+ if (existsSync(pkgSkillsDir)) {
1262
+ for (const n of readdirSync(pkgSkillsDir)) pkgSkills.add(n);
1263
+ }
1264
+ for (const { alias } of SKILL_ALIASES) pkgSkills.add(alias);
1265
+
1266
+ for (const n of readdirSync(userSkillsDir)) {
1267
+ if (!n.startsWith("tfx-")) continue;
1268
+ if (!pkgSkills.has(n)) staleSkills.push(n);
1269
+ }
1270
+ }
1271
+ if (staleSkills.length > 0) {
1272
+ warn(`구형 스킬 ${staleSkills.length}개 감지: ${staleSkills.join(", ")}`);
1273
+ info("제거: tfx setup 또는 tfx update");
1274
+ addDoctorCheck(report, { name: "stale-skills", status: "issues", skills: staleSkills, fix: "tfx setup" });
1275
+ issues++;
1276
+ } else {
1277
+ addDoctorCheck(report, { name: "stale-skills", status: "ok" });
1278
+ }
1279
+
1280
+ // 9. 플러그인 등록
1341
1281
  section("Plugin");
1342
1282
  const pluginsFile = join(CLAUDE_DIR, "plugins", "installed_plugins.json");
1343
1283
  if (existsSync(pluginsFile)) {
@@ -1355,7 +1295,7 @@ async function cmdDoctor(options = {}) {
1355
1295
  info("플러그인 시스템 감지 안 됨 — npm 단독 사용");
1356
1296
  }
1357
1297
 
1358
- // 9. MCP 인벤토리
1298
+ // 10. MCP 인벤토리
1359
1299
  section("MCP Inventory");
1360
1300
  const mcpCache = join(CLAUDE_DIR, "cache", "mcp-inventory.json");
1361
1301
  if (existsSync(mcpCache)) {
@@ -1424,7 +1364,7 @@ async function cmdDoctor(options = {}) {
1424
1364
  issues++;
1425
1365
  }
1426
1366
 
1427
- // 10. CLI 이슈 트래커
1367
+ // 11. CLI 이슈 트래커
1428
1368
  section("CLI Issues");
1429
1369
  const issuesFile = join(CLAUDE_DIR, "cache", "cli-issues.jsonl");
1430
1370
  if (existsSync(issuesFile)) {
@@ -1504,7 +1444,7 @@ async function cmdDoctor(options = {}) {
1504
1444
  ok("이슈 로그 없음 (정상)");
1505
1445
  }
1506
1446
 
1507
- // 11. Team Sessions
1447
+ // 12. Team Sessions
1508
1448
  section("Team Sessions");
1509
1449
  const teamSessionReport = inspectTeamSessions();
1510
1450
  if (!teamSessionReport.mux) {
@@ -1554,7 +1494,7 @@ async function cmdDoctor(options = {}) {
1554
1494
  }
1555
1495
  }
1556
1496
 
1557
- // 12. OMC stale team 상태
1497
+ // 13. OMC stale team 상태
1558
1498
  section("OMC Stale Teams");
1559
1499
  const omcTeamReport = inspectStaleOmcTeams({
1560
1500
  startDir: process.cwd(),
@@ -1644,7 +1584,7 @@ async function cmdDoctor(options = {}) {
1644
1584
  ok("Windows 전용 검사 — 건너뜀");
1645
1585
  }
1646
1586
 
1647
- // 13. Stale Teams (Claude teams/ + tasks/ 자동 감지)
1587
+ // 14. Stale Teams (Claude teams/ + tasks/ 자동 감지)
1648
1588
  section("Stale Teams");
1649
1589
  const teamsDir = join(CLAUDE_DIR, "teams");
1650
1590
  const tasksDir = join(CLAUDE_DIR, "tasks");
package/hub/pipe.mjs CHANGED
@@ -18,6 +18,7 @@ import {
18
18
  listPipelineStates,
19
19
  readPipelineState,
20
20
  } from './pipeline/state.mjs';
21
+ import { safeJsonParse } from './workers/worker-utils.mjs';
21
22
 
22
23
  const DEFAULT_HEARTBEAT_TTL_MS = 60000;
23
24
 
@@ -29,14 +30,6 @@ export function getPipePath(sessionId = process.pid) {
29
30
  return join('/tmp', `triflux-${sessionId}.sock`);
30
31
  }
31
32
 
32
- function safeJsonParse(line) {
33
- try {
34
- return JSON.parse(line);
35
- } catch {
36
- return null;
37
- }
38
- }
39
-
40
33
  function normalizeTopics(topics) {
41
34
  if (!Array.isArray(topics)) return [];
42
35
  return topics