triflux 7.1.4 → 7.2.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.
Files changed (73) hide show
  1. package/.claude-plugin/marketplace.json +31 -31
  2. package/.claude-plugin/plugin.json +22 -23
  3. package/bin/triflux.mjs +18 -5
  4. package/hooks/keyword-rules.json +393 -361
  5. package/hub/bridge.mjs +799 -786
  6. package/hub/delegator/contracts.mjs +37 -38
  7. package/hub/delegator/schema/delegator-tools.schema.json +250 -250
  8. package/hub/delegator/service.mjs +307 -302
  9. package/hub/intent.mjs +108 -11
  10. package/hub/lib/process-utils.mjs +20 -0
  11. package/hub/pipe.mjs +589 -589
  12. package/hub/pipeline/gates/confidence.mjs +1 -1
  13. package/hub/pipeline/gates/selfcheck.mjs +2 -4
  14. package/hub/pipeline/state.mjs +191 -187
  15. package/hub/pipeline/transitions.mjs +124 -120
  16. package/hub/public/dashboard.html +355 -349
  17. package/hub/quality/deslop.mjs +5 -3
  18. package/hub/reflexion.mjs +5 -1
  19. package/hub/research.mjs +6 -1
  20. package/hub/router.mjs +791 -782
  21. package/hub/server.mjs +893 -822
  22. package/hub/store.mjs +807 -778
  23. package/hub/team/agent-map.json +10 -0
  24. package/hub/team/ansi.mjs +3 -4
  25. package/hub/team/cli/commands/control.mjs +43 -43
  26. package/hub/team/cli/commands/interrupt.mjs +36 -36
  27. package/hub/team/cli/commands/kill.mjs +3 -3
  28. package/hub/team/cli/commands/send.mjs +37 -37
  29. package/hub/team/cli/commands/start/index.mjs +18 -8
  30. package/hub/team/cli/commands/start/parse-args.mjs +3 -1
  31. package/hub/team/cli/commands/start/start-headless.mjs +4 -1
  32. package/hub/team/cli/commands/status.mjs +87 -87
  33. package/hub/team/cli/commands/stop.mjs +1 -1
  34. package/hub/team/cli/commands/task.mjs +1 -1
  35. package/hub/team/cli/index.mjs +41 -39
  36. package/hub/team/cli/manifest.mjs +29 -28
  37. package/hub/team/cli/services/hub-client.mjs +37 -0
  38. package/hub/team/cli/services/state-store.mjs +26 -12
  39. package/hub/team/dashboard.mjs +11 -4
  40. package/hub/team/handoff.mjs +12 -0
  41. package/hub/team/headless.mjs +202 -200
  42. package/hub/team/native-supervisor.mjs +386 -346
  43. package/hub/team/nativeProxy.mjs +680 -692
  44. package/hub/team/staleState.mjs +361 -369
  45. package/hub/team/tui-viewer.mjs +27 -3
  46. package/hub/team/tui.mjs +1 -0
  47. package/hub/token-mode.mjs +114 -24
  48. package/hub/workers/delegator-mcp.mjs +1059 -1057
  49. package/hud/colors.mjs +88 -0
  50. package/hud/constants.mjs +78 -0
  51. package/hud/hud-qos-status.mjs +206 -1872
  52. package/hud/providers/claude.mjs +309 -0
  53. package/hud/providers/codex.mjs +151 -0
  54. package/hud/providers/gemini.mjs +320 -0
  55. package/hud/renderers.mjs +424 -0
  56. package/hud/terminal.mjs +140 -0
  57. package/hud/utils.mjs +271 -0
  58. package/package.json +1 -2
  59. package/scripts/__tests__/keyword-detector.test.mjs +234 -234
  60. package/scripts/headless-guard-fast.sh +21 -0
  61. package/scripts/headless-guard.mjs +26 -6
  62. package/scripts/lib/keyword-rules.mjs +166 -168
  63. package/scripts/setup.mjs +720 -690
  64. package/scripts/tfx-route-post.mjs +424 -424
  65. package/scripts/tfx-route.sh +1663 -1650
  66. package/scripts/tmp-cleanup.mjs +74 -0
  67. package/skills/tfx-auto/SKILL.md +279 -278
  68. package/skills/tfx-auto-codex/SKILL.md +98 -77
  69. package/skills/tfx-codex/SKILL.md +65 -65
  70. package/skills/tfx-gemini/SKILL.md +83 -82
  71. package/skills/tfx-hub/SKILL.md +205 -136
  72. package/skills/tfx-multi/SKILL.md +11 -5
  73. package/.mcp.json +0 -8
package/scripts/setup.mjs CHANGED
@@ -1,690 +1,720 @@
1
- #!/usr/bin/env node
2
- // triflux 세션 시작 시 자동 설정 스크립트
3
- // - tfx-route.sh를 ~/.claude/scripts/에 동기화
4
- // - hud-qos-status.mjs를 ~/.claude/hud/에 동기화
5
- // - skills/를 ~/.claude/skills/에 동기화
6
-
7
- import { copyFileSync, mkdirSync, readFileSync, writeFileSync, readdirSync, existsSync, chmodSync, unlinkSync } from "fs";
8
- import { join, dirname } from "path";
9
- import { homedir } from "os";
10
- import { spawn, execFileSync } from "child_process";
11
- import { fileURLToPath } from "url";
12
-
13
- const PLUGIN_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
14
- const CLAUDE_DIR = join(homedir(), ".claude");
15
- const CODEX_DIR = join(homedir(), ".codex");
16
- const CODEX_CONFIG_PATH = join(CODEX_DIR, "config.toml");
17
-
18
- // ── 로컬 개발 모드 감지 ──
19
-
20
- /**
21
- * PLUGIN_ROOT에 .git 디렉토리가 존재하면 dev mode (git clone 직접 사용)로 판정.
22
- * @param {string} [root] - 검사할 루트 경로 (기본: PLUGIN_ROOT)
23
- * @returns {boolean}
24
- */
25
- function detectDevMode(root = PLUGIN_ROOT) {
26
- return existsSync(join(root, ".git"));
27
- }
28
-
29
- const BREADCRUMB_PATH = join(CLAUDE_DIR, "scripts", ".tfx-pkg-root");
30
-
31
- const REQUIRED_CODEX_PROFILES = [
32
- {
33
- name: "high",
34
- lines: [
35
- 'model = "gpt-5.3-codex"',
36
- 'model_reasoning_effort = "high"',
37
- ],
38
- },
39
- {
40
- name: "xhigh",
41
- lines: [
42
- 'model = "gpt-5.3-codex"',
43
- 'model_reasoning_effort = "xhigh"',
44
- ],
45
- },
46
- {
47
- name: "spark_fast",
48
- lines: [
49
- 'model = "gpt-5.1-codex-mini"',
50
- 'model_reasoning_effort = "low"',
51
- ],
52
- },
53
- ];
54
-
55
- // ── 파일 동기화 ──
56
-
57
- const SYNC_MAP = [
58
- {
59
- src: join(PLUGIN_ROOT, "scripts", "tfx-route.sh"),
60
- dst: join(CLAUDE_DIR, "scripts", "tfx-route.sh"),
61
- label: "tfx-route.sh",
62
- },
63
- {
64
- src: join(PLUGIN_ROOT, "scripts", "tfx-route-post.mjs"),
65
- dst: join(CLAUDE_DIR, "scripts", "tfx-route-post.mjs"),
66
- label: "tfx-route-post.mjs",
67
- },
68
- {
69
- src: join(PLUGIN_ROOT, "scripts", "tfx-route-worker.mjs"),
70
- dst: join(CLAUDE_DIR, "scripts", "tfx-route-worker.mjs"),
71
- label: "tfx-route-worker.mjs",
72
- },
73
- {
74
- src: join(PLUGIN_ROOT, "hub", "workers", "codex-mcp.mjs"),
75
- dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "codex-mcp.mjs"),
76
- label: "hub/workers/codex-mcp.mjs",
77
- },
78
- {
79
- src: join(PLUGIN_ROOT, "hub", "workers", "delegator-mcp.mjs"),
80
- dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "delegator-mcp.mjs"),
81
- label: "hub/workers/delegator-mcp.mjs",
82
- },
83
- {
84
- src: join(PLUGIN_ROOT, "hub", "workers", "interface.mjs"),
85
- dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "interface.mjs"),
86
- label: "hub/workers/interface.mjs",
87
- },
88
- {
89
- src: join(PLUGIN_ROOT, "hub", "workers", "gemini-worker.mjs"),
90
- dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "gemini-worker.mjs"),
91
- label: "hub/workers/gemini-worker.mjs",
92
- },
93
- {
94
- src: join(PLUGIN_ROOT, "hub", "workers", "claude-worker.mjs"),
95
- dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "claude-worker.mjs"),
96
- label: "hub/workers/claude-worker.mjs",
97
- },
98
- {
99
- src: join(PLUGIN_ROOT, "hub", "workers", "factory.mjs"),
100
- dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "factory.mjs"),
101
- label: "hub/workers/factory.mjs",
102
- },
103
- {
104
- src: join(PLUGIN_ROOT, "hud", "hud-qos-status.mjs"),
105
- dst: join(CLAUDE_DIR, "hud", "hud-qos-status.mjs"),
106
- label: "hud-qos-status.mjs",
107
- },
108
- {
109
- src: join(PLUGIN_ROOT, "scripts", "notion-read.mjs"),
110
- dst: join(CLAUDE_DIR, "scripts", "notion-read.mjs"),
111
- label: "notion-read.mjs",
112
- },
113
- {
114
- src: join(PLUGIN_ROOT, "scripts", "tfx-batch-stats.mjs"),
115
- dst: join(CLAUDE_DIR, "scripts", "tfx-batch-stats.mjs"),
116
- label: "tfx-batch-stats.mjs",
117
- },
118
- {
119
- src: join(PLUGIN_ROOT, "scripts", "lib", "mcp-filter.mjs"),
120
- dst: join(CLAUDE_DIR, "scripts", "lib", "mcp-filter.mjs"),
121
- label: "lib/mcp-filter.mjs",
122
- },
123
- {
124
- src: join(PLUGIN_ROOT, "scripts", "lib", "mcp-server-catalog.mjs"),
125
- dst: join(CLAUDE_DIR, "scripts", "lib", "mcp-server-catalog.mjs"),
126
- label: "lib/mcp-server-catalog.mjs",
127
- },
128
- {
129
- src: join(PLUGIN_ROOT, "scripts", "lib", "keyword-rules.mjs"),
130
- dst: join(CLAUDE_DIR, "scripts", "lib", "keyword-rules.mjs"),
131
- label: "lib/keyword-rules.mjs",
132
- },
133
- {
134
- src: join(PLUGIN_ROOT, "scripts", "headless-guard.mjs"),
135
- dst: join(CLAUDE_DIR, "scripts", "headless-guard.mjs"),
136
- label: "headless-guard.mjs",
137
- },
138
- ];
139
-
140
- function getVersion(filePath) {
141
- try {
142
- const content = readFileSync(filePath, "utf8");
143
- const match = content.match(/VERSION\s*=\s*"([^"]+)"/);
144
- return match ? match[1] : null;
145
- } catch {
146
- return null;
147
- }
148
- }
149
-
150
- function shouldSyncTextFile(src, dst) {
151
- if (!existsSync(dst)) return true;
152
- try {
153
- return readFileSync(src, "utf8") !== readFileSync(dst, "utf8");
154
- } catch {
155
- return true;
156
- }
157
- }
158
-
159
- function escapeRegExp(value) {
160
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
161
- }
162
-
163
- function hasProfileSection(tomlContent, profileName) {
164
- const section = `^\\[profiles\\.${escapeRegExp(profileName)}\\]\\s*$`;
165
- return new RegExp(section, "m").test(tomlContent);
166
- }
167
-
168
- function replaceProfileSection(tomlContent, profileName, lines) {
169
- const header = `[profiles.${profileName}]`;
170
- const sectionRe = new RegExp(
171
- `^\\[profiles\\.${escapeRegExp(profileName)}\\]\\s*\\n(?:(?!\\[)[^\\n]*\\n?)*`,
172
- "m",
173
- );
174
- const replacement = `${header}\n${lines.join("\n")}\n`;
175
- return tomlContent.replace(sectionRe, replacement);
176
- }
177
-
178
- function ensureCodexProfiles() {
179
- try {
180
- if (!existsSync(CODEX_DIR)) mkdirSync(CODEX_DIR, { recursive: true });
181
-
182
- const original = existsSync(CODEX_CONFIG_PATH)
183
- ? readFileSync(CODEX_CONFIG_PATH, "utf8")
184
- : "";
185
-
186
- let updated = original;
187
- let changed = 0;
188
-
189
- for (const profile of REQUIRED_CODEX_PROFILES) {
190
- const desired = `[profiles.${profile.name}]\n${profile.lines.join("\n")}\n`;
191
-
192
- if (hasProfileSection(updated, profile.name)) {
193
- // 기존 프로필이 있으면 강제 갱신
194
- const before = updated;
195
- updated = replaceProfileSection(updated, profile.name, profile.lines);
196
- if (updated !== before) changed++;
197
- } else {
198
- // 없으면 추가
199
- if (updated.length > 0 && !updated.endsWith("\n")) updated += "\n";
200
- if (updated.trim().length > 0) updated += "\n";
201
- updated += desired;
202
- changed++;
203
- }
204
- }
205
-
206
- if (changed > 0) {
207
- writeFileSync(CODEX_CONFIG_PATH, updated, "utf8");
208
- }
209
-
210
- return changed;
211
- } catch {
212
- return 0;
213
- }
214
- }
215
-
216
- export { replaceProfileSection, hasProfileSection, detectDevMode, SYNC_MAP, BREADCRUMB_PATH, PLUGIN_ROOT, CLAUDE_DIR };
217
-
218
- async function main() {
219
- const isSync = process.argv.includes("--sync");
220
- const isDev = detectDevMode();
221
-
222
- if (isDev) {
223
- console.log(" [dev] \uB85C\uCEEC \uAC1C\uBC1C \uBAA8\uB4DC \uAC10\uC9C0");
224
- }
225
-
226
- if (isSync) {
227
- console.log(" [sync] \uBA85\uC2DC\uC801 \uC7AC\uB3D9\uAE30\uD654 \uC2E4\uD589");
228
- }
229
-
230
- let synced = 0;
231
-
232
- for (const { src, dst, label } of SYNC_MAP) {
233
- if (!existsSync(src)) continue;
234
-
235
- const dstDir = dirname(dst);
236
- if (!existsSync(dstDir)) {
237
- mkdirSync(dstDir, { recursive: true });
238
- }
239
-
240
- if (!existsSync(dst)) {
241
- copyFileSync(src, dst);
242
- try { chmodSync(dst, 0o755); } catch {}
243
- synced++;
244
- } else {
245
- if (shouldSyncTextFile(src, dst)) {
246
- copyFileSync(src, dst);
247
- try { chmodSync(dst, 0o755); } catch {}
248
- synced++;
249
- }
250
- }
251
- }
252
-
253
- // ── Worker 의존성 동기화 (MCP SDK + transitive deps) ──
254
-
255
- const workerNodeModules = join(CLAUDE_DIR, "scripts", "node_modules");
256
- const mcpSdkPath = join(workerNodeModules, "@modelcontextprotocol", "sdk");
257
- const srcNodeModules = join(PLUGIN_ROOT, "node_modules");
258
-
259
- // native 모듈은 제외 (플랫폼 의존적, worker에서 불필요)
260
- const SKIP_PACKAGES = new Set(["better-sqlite3", "prebuild-install", "node-abi", "node-addon-api"]);
261
-
262
- if (!existsSync(mcpSdkPath) && existsSync(srcNodeModules)) {
263
- try {
264
- const { cpSync } = await import("fs");
265
- for (const entry of readdirSync(srcNodeModules)) {
266
- if (SKIP_PACKAGES.has(entry)) continue;
267
-
268
- const src = join(srcNodeModules, entry);
269
- const dst = join(workerNodeModules, entry);
270
- if (existsSync(dst)) continue;
271
-
272
- mkdirSync(dirname(dst), { recursive: true });
273
- cpSync(src, dst, { recursive: true });
274
- }
275
- synced++;
276
- } catch {
277
- // best effort: 의존성 복사 실패 시 exec fallback으로 동작
278
- }
279
- }
280
-
281
- // ── 패키지 루트 breadcrumb 기록 ──
282
- // tfx-route.sh가 hub/server.mjs, hub/bridge.mjs를 찾을 수 있도록
283
- // 패키지 루트 경로를 ~/.claude/scripts/.tfx-pkg-root에 기록한다.
284
- // dev mode에서는 항상 최신 경로를 기록 (--sync 시 강제 갱신).
285
- {
286
- const pkgRootForward = PLUGIN_ROOT.replace(/\\/g, "/");
287
- const currentBreadcrumb = existsSync(BREADCRUMB_PATH)
288
- ? readFileSync(BREADCRUMB_PATH, "utf8").trim()
289
- : "";
290
- if (currentBreadcrumb !== pkgRootForward || isSync) {
291
- const breadcrumbDir = dirname(BREADCRUMB_PATH);
292
- if (!existsSync(breadcrumbDir)) mkdirSync(breadcrumbDir, { recursive: true });
293
- writeFileSync(BREADCRUMB_PATH, pkgRootForward + "\n", "utf8");
294
- synced++;
295
- }
296
- }
297
-
298
- // ── 에이전트 동기화 (.claude/agents/ → ~/.claude/agents/) ──
299
- // slim-wrapper 커스텀 에이전트를 글로벌에 배포하여
300
- // 다른 프로젝트에서도 subagent_type으로 참조 가능하게 한다.
301
-
302
- const agentsSrc = join(PLUGIN_ROOT, ".claude", "agents");
303
- const agentsDst = join(CLAUDE_DIR, "agents");
304
-
305
- if (existsSync(agentsSrc)) {
306
- if (!existsSync(agentsDst)) mkdirSync(agentsDst, { recursive: true });
307
-
308
- for (const name of readdirSync(agentsSrc)) {
309
- if (!name.endsWith(".md")) continue;
310
-
311
- const src = join(agentsSrc, name);
312
- const dst = join(agentsDst, name);
313
-
314
- if (!existsSync(dst)) {
315
- copyFileSync(src, dst);
316
- synced++;
317
- } else if (shouldSyncTextFile(src, dst)) {
318
- copyFileSync(src, dst);
319
- synced++;
320
- }
321
- }
322
- }
323
-
324
- // ── 스킬 동기화 ──
325
- // SKILL.md + 하위 디렉토리(references/ 등)를 재귀적으로 동기화
326
-
327
- const skillsSrc = join(PLUGIN_ROOT, "skills");
328
- const skillsDst = join(CLAUDE_DIR, "skills");
329
-
330
- function syncSkillDir(srcDir, dstDir) {
331
- if (!existsSync(dstDir)) mkdirSync(dstDir, { recursive: true });
332
-
333
- for (const entry of readdirSync(srcDir, { withFileTypes: true })) {
334
- const srcPath = join(srcDir, entry.name);
335
- const dstPath = join(dstDir, entry.name);
336
-
337
- if (entry.isDirectory()) {
338
- syncSkillDir(srcPath, dstPath);
339
- } else if (entry.name.endsWith(".md")) {
340
- if (shouldSyncTextFile(srcPath, dstPath)) {
341
- copyFileSync(srcPath, dstPath);
342
- synced++;
343
- }
344
- }
345
- }
346
- }
347
-
348
- if (existsSync(skillsSrc)) {
349
- for (const name of readdirSync(skillsSrc)) {
350
- const skillDir = join(skillsSrc, name);
351
- const skillMd = join(skillDir, "SKILL.md");
352
- if (!existsSync(skillMd)) continue;
353
-
354
- syncSkillDir(skillDir, join(skillsDst, name));
355
- }
356
- }
357
-
358
- // ── settings.json statusLine 자동 설정 ──
359
-
360
- const settingsPath = join(CLAUDE_DIR, "settings.json");
361
- const hudPath = join(CLAUDE_DIR, "hud", "hud-qos-status.mjs");
362
-
363
- if (existsSync(hudPath)) {
364
- try {
365
- let settings = {};
366
- if (existsSync(settingsPath)) {
367
- settings = JSON.parse(readFileSync(settingsPath, "utf8"));
368
- }
369
-
370
- // statusLine이 없거나 hud-qos-status.mjs를 가리키지 않는 경우에만 설정
371
- const currentCmd = settings.statusLine?.command || "";
372
- if (!currentCmd.includes("hud-qos-status.mjs")) {
373
- const nodePath = process.execPath.replace(/\\/g, "/");
374
- const hudForward = hudPath.replace(/\\/g, "/");
375
-
376
- // Windows: 경로에 공백이 있으면 큰따옴표 감싸기
377
- const nodeRef = nodePath.includes(" ") ? `"${nodePath}"` : nodePath;
378
- const hudRef = hudForward.includes(" ") ? `"${hudForward}"` : hudForward;
379
-
380
- settings.statusLine = {
381
- type: "command",
382
- command: `${nodeRef} ${hudRef}`,
383
- };
384
-
385
- writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
386
- synced++;
387
- }
388
- } catch {
389
- // settings.json 파싱 실패 시 무시 — 기존 설정 보존
390
- }
391
- }
392
-
393
- // ── Agent Teams 환경변수 자동 설정 ──
394
-
395
- try {
396
- let agentSettings = {};
397
- if (existsSync(settingsPath)) {
398
- agentSettings = JSON.parse(readFileSync(settingsPath, "utf8"));
399
- }
400
-
401
- if (!agentSettings.env) agentSettings.env = {};
402
- let agentSettingsChanged = false;
403
-
404
- if (agentSettings.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS !== "1") {
405
- agentSettings.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = "1";
406
- agentSettingsChanged = true;
407
- }
408
-
409
- // teammateMode: auto (tmux 밖이면 in-process, 안이면 split-pane)
410
- if (!agentSettings.teammateMode) {
411
- agentSettings.teammateMode = "auto";
412
- agentSettingsChanged = true;
413
- }
414
-
415
- if (agentSettingsChanged) {
416
- writeFileSync(settingsPath, JSON.stringify(agentSettings, null, 2) + "\n", "utf8");
417
- synced++;
418
- }
419
- } catch {
420
- // settings.json 파싱 실패 시 무시 — 기존 설정 보존
421
- }
422
-
423
- // ── Stale PID 파일 정리 (hub 좀비 방지) ──
424
-
425
- const HUB_PID_FILE = join(CLAUDE_DIR, "cache", "tfx-hub", "hub.pid");
426
- if (existsSync(HUB_PID_FILE)) {
427
- try {
428
- const pidInfo = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
429
- process.kill(pidInfo.pid, 0); // 프로세스 존재 확인 (신호 미전송)
430
- } catch {
431
- try { unlinkSync(HUB_PID_FILE); } catch {} // 죽은 프로세스면 PID 파일 삭제
432
- synced++;
433
- }
434
- }
435
-
436
- // ── psmux 자동 설치 (Windows, headless 모드용) ──
437
-
438
- if (process.platform === "win32") {
439
- try {
440
- execFileSync("where", ["psmux"], { stdio: "ignore" });
441
- } catch {
442
- // psmux 미설치 — winget으로 자동 설치 시도
443
- console.log(" psmux 미설치 — winget으로 설치 중...");
444
- try {
445
- execFileSync("winget", ["install", "--id", "marlocarlo.psmux", "--accept-package-agreements", "--accept-source-agreements"], {
446
- stdio: ["ignore", "pipe", "pipe"],
447
- timeout: 60000,
448
- });
449
- console.log(" \x1b[32m✓\x1b[0m psmux 설치 완료");
450
- synced++;
451
- } catch {
452
- console.log(" \x1b[33m⚠\x1b[0m psmux 자동 설치 실패 — 수동 설치: winget install psmux");
453
- }
454
- }
455
- }
456
-
457
- // ── HUD 에러 캐시 자동 클리어 (업데이트/재설치 시) ──
458
-
459
- const cacheDir = join(CLAUDE_DIR, "cache");
460
- const staleFiles = [
461
- "claude-usage-cache.json",
462
- ".claude-refresh-lock",
463
- "codex-rate-limits-cache.json",
464
- ];
465
-
466
- for (const name of staleFiles) {
467
- const fp = join(cacheDir, name);
468
- if (!existsSync(fp)) continue;
469
- try {
470
- const content = readFileSync(fp, "utf8");
471
- const parsed = JSON.parse(content);
472
- // 에러 상태이거나 파일이면 삭제 → 새 세션에서 fresh start
473
- if (parsed.error || name.startsWith(".")) {
474
- unlinkSync(fp);
475
- synced++;
476
- }
477
- } catch {
478
- // 파싱 실패 파일도 삭제
479
- try { unlinkSync(fp); } catch {}
480
- }
481
- }
482
-
483
- // ── Windows bash PATH 자동 설정 ──
484
- // Codex/Gemini가 cmd에는 있지만 bash에서 못 찾는 문제 해결
485
-
486
- if (process.platform === "win32") {
487
- const npmBin = join(process.env.APPDATA || "", "npm");
488
- if (existsSync(npmBin)) {
489
- const bashrcPath = join(homedir(), ".bashrc");
490
- const pathExport = 'export PATH="$PATH:$APPDATA/npm"';
491
- let needsUpdate = true;
492
-
493
- if (existsSync(bashrcPath)) {
494
- const content = readFileSync(bashrcPath, "utf8");
495
- if (content.includes("APPDATA/npm") || content.includes("APPDATA\\npm")) {
496
- needsUpdate = false;
497
- }
498
- }
499
-
500
- if (needsUpdate) {
501
- const line = `\n# triflux: Codex/Gemini CLI를 bash에서 사용하기 위한 PATH 설정\n${pathExport}\n`;
502
- try {
503
- writeFileSync(bashrcPath, (existsSync(bashrcPath) ? readFileSync(bashrcPath, "utf8") : "") + line, "utf8");
504
- synced++;
505
- } catch {}
506
- }
507
- }
508
- }
509
-
510
- // ── Codex 프로필 자동 보정 ──
511
-
512
- const codexProfilesAdded = ensureCodexProfiles();
513
- if (codexProfilesAdded > 0) {
514
- synced++;
515
- }
516
-
517
- // ── MCP 인벤토리 백그라운드 갱신 ──
518
-
519
- const mcpCheck = join(PLUGIN_ROOT, "scripts", "mcp-check.mjs");
520
- if (existsSync(mcpCheck)) {
521
- const child = spawn(process.execPath, [mcpCheck], {
522
- detached: true,
523
- stdio: "ignore",
524
- windowsHide: true,
525
- });
526
- child.unref(); // 부모 프로세스와 분리 — 비동기 실행
527
- }
528
-
529
- // ── SessionStart 훅 자동 등록 (settings.json) ──
530
- // .claude-plugin/ 개발 플러그인의 SessionStart 훅은 플러그인 로드 시점 문제로
531
- // 실행되지 않을 수 있으므로, settings.json에 직접 등록한다.
532
- // hub-ensure.mjs는 settings.json 훅으로만 실행 (이중 spawn 방지).
533
-
534
- try {
535
- let hookSettings = {};
536
- if (existsSync(settingsPath)) {
537
- hookSettings = JSON.parse(readFileSync(settingsPath, "utf8"));
538
- }
539
-
540
- if (!hookSettings.hooks) hookSettings.hooks = {};
541
- if (!Array.isArray(hookSettings.hooks.SessionStart)) {
542
- hookSettings.hooks.SessionStart = [];
543
- }
544
-
545
- const existingHooks = hookSettings.hooks.SessionStart;
546
- const hasTrifluxHooks = existingHooks.some((entry) =>
547
- Array.isArray(entry.hooks) &&
548
- entry.hooks.some((h) => typeof h.command === "string" && h.command.includes("triflux")),
549
- );
550
-
551
- if (!hasTrifluxHooks) {
552
- const nodePath = process.execPath.replace(/\\/g, "/");
553
- const nodeRef = nodePath.includes(" ") ? `"${nodePath}"` : nodePath;
554
- const pluginRoot = PLUGIN_ROOT.replace(/\\/g, "/");
555
-
556
- const trifluxHookEntry = {
557
- matcher: "*",
558
- hooks: [
559
- {
560
- type: "command",
561
- command: `${nodeRef} "${pluginRoot}/scripts/setup.mjs"`,
562
- timeout: 10,
563
- },
564
- {
565
- type: "command",
566
- command: `${nodeRef} "${pluginRoot}/scripts/hub-ensure.mjs"`,
567
- timeout: 8,
568
- },
569
- {
570
- type: "command",
571
- command: `${nodeRef} "${pluginRoot}/scripts/preflight-cache.mjs"`,
572
- timeout: 5,
573
- },
574
- ],
575
- };
576
-
577
- hookSettings.hooks.SessionStart.push(trifluxHookEntry);
578
- writeFileSync(settingsPath, JSON.stringify(hookSettings, null, 2) + "\n", "utf8");
579
- synced++;
580
- }
581
-
582
- // ── PreToolUse 훅: headless-guard (auto-route) ──
583
- // Phase 3 headless 모드 활성 중 tfx-route.sh 개별 호출을
584
- // headless 명령으로 자동 변환한다.
585
- if (!Array.isArray(hookSettings.hooks.PreToolUse)) {
586
- hookSettings.hooks.PreToolUse = [];
587
- }
588
-
589
- const guardScriptPath = join(CLAUDE_DIR, "scripts", "headless-guard.mjs").replace(/\\/g, "/");
590
- const hasGuardHook = hookSettings.hooks.PreToolUse.some((entry) =>
591
- Array.isArray(entry.hooks) &&
592
- entry.hooks.some((h) => typeof h.command === "string" && h.command.includes("headless-guard")),
593
- );
594
-
595
- if (!hasGuardHook && existsSync(guardScriptPath.replace(/\//g, "\\"))) {
596
- const nodePath = process.execPath.replace(/\\/g, "/");
597
- const nodeRef = nodePath.includes(" ") ? `"${nodePath}"` : nodePath;
598
-
599
- hookSettings.hooks.PreToolUse.push({
600
- matcher: "Bash|Agent",
601
- hooks: [
602
- {
603
- type: "command",
604
- command: `${nodeRef} "${guardScriptPath}"`,
605
- timeout: 3,
606
- },
607
- ],
608
- });
609
- writeFileSync(settingsPath, JSON.stringify(hookSettings, null, 2) + "\n", "utf8");
610
- synced++;
611
- } else if (hasGuardHook) {
612
- // 기존 훅 경로를 동기화된 경로로 업데이트
613
- let updated = false;
614
- for (const entry of hookSettings.hooks.PreToolUse) {
615
- if (!Array.isArray(entry.hooks)) continue;
616
- for (const h of entry.hooks) {
617
- if (typeof h.command === "string" && h.command.includes("headless-guard") && !h.command.includes(guardScriptPath)) {
618
- const nodePath = process.execPath.replace(/\\/g, "/");
619
- const nodeRef = nodePath.includes(" ") ? `"${nodePath}"` : nodePath;
620
- h.command = `${nodeRef} "${guardScriptPath}"`;
621
- updated = true;
622
- }
623
- }
624
- }
625
- if (updated) {
626
- writeFileSync(settingsPath, JSON.stringify(hookSettings, null, 2) + "\n", "utf8");
627
- synced++;
628
- }
629
- }
630
- } catch {
631
- // settings.json 파싱 실패 시 무시 — 기존 설정 보존
632
- }
633
-
634
- // ── postinstall 배너 (npm install 시에만 출력) ──
635
-
636
- if (process.env.npm_lifecycle_event === "postinstall") {
637
- const G = "\x1b[32m";
638
- const C = "\x1b[36m";
639
- const Y = "\x1b[33m";
640
- const D = "\x1b[2m";
641
- const B = "\x1b[1m";
642
- const R = "\x1b[0m";
643
-
644
- const ver = (() => {
645
- try {
646
- return JSON.parse(readFileSync(join(PLUGIN_ROOT, "package.json"), "utf8")).version;
647
- } catch { return "?"; }
648
- })();
649
-
650
- console.log(`
651
- ${B}╔═══════════════════════════════════════════════╗${R}
652
- ${B}║${R} ${C}triflux${R} ${D}v${ver}${R} ${B}— Setup Complete${R} ${B}║${R}
653
- ${B}╚═══════════════════════════════════════════════╝${R}
654
-
655
- ${G}✓${R} tfx-route.sh → ~/.claude/scripts/
656
- ${G}✓${R} hud-qos-status → ~/.claude/hud/
657
- ${G}✓${R} ${synced > 0 ? synced + " files synced" : "all files up to date"}
658
- ${G}✓${R} HUD statusLine → settings.json
659
-
660
- ${B}Commands:${R}
661
- ${C}triflux${R} setup 파일 동기화 + HUD 설정
662
- ${C}triflux${R} doctor CLI 진단 (Codex/Gemini 확인)
663
- ${C}triflux${R} list 설치된 스킬 목록
664
- ${C}triflux${R} update 최신 안정 버전으로 업데이트
665
- ${C}triflux${R} update --dev dev 채널로 업데이트 (${D}dev 별칭 지원${R})
666
-
667
- ${B}Shortcuts:${R}
668
- ${C}tfx${R} triflux 축약
669
- ${C}tfx-setup${R} triflux setup
670
- ${C}tfx-doctor${R} triflux doctor
671
-
672
- ${B}Skills (Claude Code):${R}
673
- ${C}/tfx-auto${R} "작업" 자동 분류 + 병렬 실행
674
- ${C}/tfx-auto-codex${R} "작업" Codex 리드 + Gemini 유지
675
- ${C}/tfx-codex${R} "작업" Codex 전용 모드
676
- ${C}/tfx-gemini${R} "작업" Gemini 전용 모드
677
- ${C}/tfx-setup${R} HUD 설정 + 진단
678
-
679
- ${Y}!${R} 세션 재시작 후 스킬이 활성화됩니다
680
- ${D}https://github.com/tellang/triflux${R}
681
- `);
682
- }
683
-
684
- process.exit(0);
685
- }
686
-
687
- if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
688
- main();
689
- }
690
-
1
+ #!/usr/bin/env node
2
+ // triflux 세션 시작 시 자동 설정 스크립트
3
+ // - tfx-route.sh를 ~/.claude/scripts/에 동기화
4
+ // - hud-qos-status.mjs를 ~/.claude/hud/에 동기화
5
+ // - skills/를 ~/.claude/skills/에 동기화
6
+
7
+ import { copyFileSync, mkdirSync, readFileSync, writeFileSync, readdirSync, existsSync, chmodSync, unlinkSync } from "fs";
8
+ import { join, dirname } from "path";
9
+ import { homedir } from "os";
10
+ import { spawn, execFileSync } from "child_process";
11
+ import { fileURLToPath } from "url";
12
+ import { cleanupTmpFiles } from "./tmp-cleanup.mjs";
13
+
14
+ const PLUGIN_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
15
+ const CLAUDE_DIR = join(homedir(), ".claude");
16
+ const CODEX_DIR = join(homedir(), ".codex");
17
+ const CODEX_CONFIG_PATH = join(CODEX_DIR, "config.toml");
18
+
19
+ // ── 로컬 개발 모드 감지 ──
20
+
21
+ /**
22
+ * PLUGIN_ROOT에 .git 디렉토리가 존재하면 dev mode (git clone 직접 사용)로 판정.
23
+ * @param {string} [root] - 검사할 루트 경로 (기본: PLUGIN_ROOT)
24
+ * @returns {boolean}
25
+ */
26
+ function detectDevMode(root = PLUGIN_ROOT) {
27
+ return existsSync(join(root, ".git"));
28
+ }
29
+
30
+ const BREADCRUMB_PATH = join(CLAUDE_DIR, "scripts", ".tfx-pkg-root");
31
+
32
+ const REQUIRED_CODEX_PROFILES = [
33
+ {
34
+ name: "high",
35
+ lines: [
36
+ 'model = "gpt-5.3-codex"',
37
+ 'model_reasoning_effort = "high"',
38
+ ],
39
+ },
40
+ {
41
+ name: "xhigh",
42
+ lines: [
43
+ 'model = "gpt-5.3-codex"',
44
+ 'model_reasoning_effort = "xhigh"',
45
+ ],
46
+ },
47
+ {
48
+ name: "spark_fast",
49
+ lines: [
50
+ 'model = "gpt-5.1-codex-mini"',
51
+ 'model_reasoning_effort = "low"',
52
+ ],
53
+ },
54
+ ];
55
+
56
+ // ── 파일 동기화 ──
57
+
58
+ const SYNC_MAP = [
59
+ {
60
+ src: join(PLUGIN_ROOT, "scripts", "tfx-route.sh"),
61
+ dst: join(CLAUDE_DIR, "scripts", "tfx-route.sh"),
62
+ label: "tfx-route.sh",
63
+ },
64
+ {
65
+ src: join(PLUGIN_ROOT, "scripts", "tfx-route-post.mjs"),
66
+ dst: join(CLAUDE_DIR, "scripts", "tfx-route-post.mjs"),
67
+ label: "tfx-route-post.mjs",
68
+ },
69
+ {
70
+ src: join(PLUGIN_ROOT, "scripts", "tfx-route-worker.mjs"),
71
+ dst: join(CLAUDE_DIR, "scripts", "tfx-route-worker.mjs"),
72
+ label: "tfx-route-worker.mjs",
73
+ },
74
+ {
75
+ src: join(PLUGIN_ROOT, "hub", "workers", "codex-mcp.mjs"),
76
+ dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "codex-mcp.mjs"),
77
+ label: "hub/workers/codex-mcp.mjs",
78
+ },
79
+ {
80
+ src: join(PLUGIN_ROOT, "hub", "workers", "delegator-mcp.mjs"),
81
+ dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "delegator-mcp.mjs"),
82
+ label: "hub/workers/delegator-mcp.mjs",
83
+ },
84
+ {
85
+ src: join(PLUGIN_ROOT, "hub", "workers", "interface.mjs"),
86
+ dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "interface.mjs"),
87
+ label: "hub/workers/interface.mjs",
88
+ },
89
+ {
90
+ src: join(PLUGIN_ROOT, "hub", "workers", "gemini-worker.mjs"),
91
+ dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "gemini-worker.mjs"),
92
+ label: "hub/workers/gemini-worker.mjs",
93
+ },
94
+ {
95
+ src: join(PLUGIN_ROOT, "hub", "workers", "claude-worker.mjs"),
96
+ dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "claude-worker.mjs"),
97
+ label: "hub/workers/claude-worker.mjs",
98
+ },
99
+ {
100
+ src: join(PLUGIN_ROOT, "hub", "workers", "factory.mjs"),
101
+ dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "factory.mjs"),
102
+ label: "hub/workers/factory.mjs",
103
+ },
104
+ {
105
+ src: join(PLUGIN_ROOT, "hud", "hud-qos-status.mjs"),
106
+ dst: join(CLAUDE_DIR, "hud", "hud-qos-status.mjs"),
107
+ label: "hud-qos-status.mjs",
108
+ },
109
+ {
110
+ src: join(PLUGIN_ROOT, "hud", "colors.mjs"),
111
+ dst: join(CLAUDE_DIR, "hud", "colors.mjs"),
112
+ label: "hud/colors.mjs",
113
+ },
114
+ {
115
+ src: join(PLUGIN_ROOT, "hud", "constants.mjs"),
116
+ dst: join(CLAUDE_DIR, "hud", "constants.mjs"),
117
+ label: "hud/constants.mjs",
118
+ },
119
+ {
120
+ src: join(PLUGIN_ROOT, "hud", "terminal.mjs"),
121
+ dst: join(CLAUDE_DIR, "hud", "terminal.mjs"),
122
+ label: "hud/terminal.mjs",
123
+ },
124
+ {
125
+ src: join(PLUGIN_ROOT, "hud", "utils.mjs"),
126
+ dst: join(CLAUDE_DIR, "hud", "utils.mjs"),
127
+ label: "hud/utils.mjs",
128
+ },
129
+ {
130
+ src: join(PLUGIN_ROOT, "hud", "renderers.mjs"),
131
+ dst: join(CLAUDE_DIR, "hud", "renderers.mjs"),
132
+ label: "hud/renderers.mjs",
133
+ },
134
+ {
135
+ src: join(PLUGIN_ROOT, "hud", "providers", "claude.mjs"),
136
+ dst: join(CLAUDE_DIR, "hud", "providers", "claude.mjs"),
137
+ label: "hud/providers/claude.mjs",
138
+ },
139
+ {
140
+ src: join(PLUGIN_ROOT, "hud", "providers", "codex.mjs"),
141
+ dst: join(CLAUDE_DIR, "hud", "providers", "codex.mjs"),
142
+ label: "hud/providers/codex.mjs",
143
+ },
144
+ {
145
+ src: join(PLUGIN_ROOT, "hud", "providers", "gemini.mjs"),
146
+ dst: join(CLAUDE_DIR, "hud", "providers", "gemini.mjs"),
147
+ label: "hud/providers/gemini.mjs",
148
+ },
149
+ {
150
+ src: join(PLUGIN_ROOT, "scripts", "notion-read.mjs"),
151
+ dst: join(CLAUDE_DIR, "scripts", "notion-read.mjs"),
152
+ label: "notion-read.mjs",
153
+ },
154
+ {
155
+ src: join(PLUGIN_ROOT, "scripts", "tfx-batch-stats.mjs"),
156
+ dst: join(CLAUDE_DIR, "scripts", "tfx-batch-stats.mjs"),
157
+ label: "tfx-batch-stats.mjs",
158
+ },
159
+ {
160
+ src: join(PLUGIN_ROOT, "scripts", "lib", "mcp-filter.mjs"),
161
+ dst: join(CLAUDE_DIR, "scripts", "lib", "mcp-filter.mjs"),
162
+ label: "lib/mcp-filter.mjs",
163
+ },
164
+ {
165
+ src: join(PLUGIN_ROOT, "scripts", "lib", "mcp-server-catalog.mjs"),
166
+ dst: join(CLAUDE_DIR, "scripts", "lib", "mcp-server-catalog.mjs"),
167
+ label: "lib/mcp-server-catalog.mjs",
168
+ },
169
+ {
170
+ src: join(PLUGIN_ROOT, "scripts", "lib", "keyword-rules.mjs"),
171
+ dst: join(CLAUDE_DIR, "scripts", "lib", "keyword-rules.mjs"),
172
+ label: "lib/keyword-rules.mjs",
173
+ },
174
+ {
175
+ src: join(PLUGIN_ROOT, "scripts", "headless-guard.mjs"),
176
+ dst: join(CLAUDE_DIR, "scripts", "headless-guard.mjs"),
177
+ label: "headless-guard.mjs",
178
+ },
179
+ {
180
+ src: join(PLUGIN_ROOT, "scripts", "headless-guard-fast.sh"),
181
+ dst: join(CLAUDE_DIR, "scripts", "headless-guard-fast.sh"),
182
+ label: "headless-guard-fast.sh",
183
+ },
184
+ ];
185
+
186
+ function getVersion(filePath) {
187
+ try {
188
+ const content = readFileSync(filePath, "utf8");
189
+ const match = content.match(/VERSION\s*=\s*"([^"]+)"/);
190
+ return match ? match[1] : null;
191
+ } catch {
192
+ return null;
193
+ }
194
+ }
195
+
196
+ function shouldSyncTextFile(src, dst) {
197
+ if (!existsSync(dst)) return true;
198
+ try {
199
+ return readFileSync(src, "utf8") !== readFileSync(dst, "utf8");
200
+ } catch {
201
+ return true;
202
+ }
203
+ }
204
+
205
+ function escapeRegExp(value) {
206
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
207
+ }
208
+
209
+ function hasProfileSection(tomlContent, profileName) {
210
+ const section = `^\\[profiles\\.${escapeRegExp(profileName)}\\]\\s*$`;
211
+ return new RegExp(section, "m").test(tomlContent);
212
+ }
213
+
214
+ function replaceProfileSection(tomlContent, profileName, lines) {
215
+ const header = `[profiles.${profileName}]`;
216
+ const sectionRe = new RegExp(
217
+ `^\\[profiles\\.${escapeRegExp(profileName)}\\]\\s*\\n?(?:(?!\\[)[^\\n]*\\n?)*`,
218
+ "m",
219
+ );
220
+ const replacement = `${header}\n${lines.join("\n")}\n`;
221
+ return tomlContent.replace(sectionRe, replacement);
222
+ }
223
+
224
+ function ensureCodexProfiles() {
225
+ try {
226
+ if (!existsSync(CODEX_DIR)) mkdirSync(CODEX_DIR, { recursive: true });
227
+
228
+ const original = existsSync(CODEX_CONFIG_PATH)
229
+ ? readFileSync(CODEX_CONFIG_PATH, "utf8")
230
+ : "";
231
+
232
+ let updated = original;
233
+ let changed = 0;
234
+
235
+ for (const profile of REQUIRED_CODEX_PROFILES) {
236
+ const desired = `[profiles.${profile.name}]\n${profile.lines.join("\n")}\n`;
237
+
238
+ if (hasProfileSection(updated, profile.name)) {
239
+ // 기존 프로필이 있으면 강제 갱신
240
+ const before = updated;
241
+ updated = replaceProfileSection(updated, profile.name, profile.lines);
242
+ if (updated !== before) changed++;
243
+ } else {
244
+ // 없으면 추가
245
+ if (updated.length > 0 && !updated.endsWith("\n")) updated += "\n";
246
+ if (updated.trim().length > 0) updated += "\n";
247
+ updated += desired;
248
+ changed++;
249
+ }
250
+ }
251
+
252
+ if (changed > 0) {
253
+ writeFileSync(CODEX_CONFIG_PATH, updated, "utf8");
254
+ }
255
+
256
+ return changed;
257
+ } catch {
258
+ return 0;
259
+ }
260
+ }
261
+
262
+ export { replaceProfileSection, hasProfileSection, detectDevMode, SYNC_MAP, BREADCRUMB_PATH, PLUGIN_ROOT, CLAUDE_DIR };
263
+
264
+ async function main() {
265
+ const isSync = process.argv.includes("--sync");
266
+ const isDev = detectDevMode();
267
+
268
+ if (isDev) {
269
+ console.log(" [dev] \uB85C\uCEEC \uAC1C\uBC1C \uBAA8\uB4DC \uAC10\uC9C0");
270
+ }
271
+
272
+ if (isSync) {
273
+ console.log(" [sync] \uBA85\uC2DC\uC801 \uC7AC\uB3D9\uAE30\uD654 \uC2E4\uD589");
274
+ }
275
+
276
+ let synced = 0;
277
+
278
+ for (const { src, dst, label } of SYNC_MAP) {
279
+ if (!existsSync(src)) continue;
280
+
281
+ const dstDir = dirname(dst);
282
+ if (!existsSync(dstDir)) {
283
+ mkdirSync(dstDir, { recursive: true });
284
+ }
285
+
286
+ if (!existsSync(dst)) {
287
+ copyFileSync(src, dst);
288
+ try { chmodSync(dst, 0o755); } catch {}
289
+ synced++;
290
+ } else {
291
+ if (shouldSyncTextFile(src, dst)) {
292
+ copyFileSync(src, dst);
293
+ try { chmodSync(dst, 0o755); } catch {}
294
+ synced++;
295
+ }
296
+ }
297
+ }
298
+
299
+ // ── Worker 의존성 동기화 (MCP SDK + transitive deps) ──
300
+
301
+ const workerNodeModules = join(CLAUDE_DIR, "scripts", "node_modules");
302
+ const mcpSdkPath = join(workerNodeModules, "@modelcontextprotocol", "sdk");
303
+ const srcNodeModules = join(PLUGIN_ROOT, "node_modules");
304
+
305
+ // native 모듈은 제외 (플랫폼 의존적, worker에서 불필요)
306
+ const SKIP_PACKAGES = new Set(["better-sqlite3", "prebuild-install", "node-abi", "node-addon-api"]);
307
+
308
+ if (!existsSync(mcpSdkPath) && existsSync(srcNodeModules)) {
309
+ try {
310
+ const { cpSync } = await import("fs");
311
+ for (const entry of readdirSync(srcNodeModules)) {
312
+ if (SKIP_PACKAGES.has(entry)) continue;
313
+
314
+ const src = join(srcNodeModules, entry);
315
+ const dst = join(workerNodeModules, entry);
316
+ if (existsSync(dst)) continue;
317
+
318
+ mkdirSync(dirname(dst), { recursive: true });
319
+ cpSync(src, dst, { recursive: true });
320
+ }
321
+ synced++;
322
+ } catch {
323
+ // best effort: 의존성 복사 실패 시 exec fallback으로 동작
324
+ }
325
+ }
326
+
327
+ // ── 패키지 루트 breadcrumb 기록 ──
328
+ // tfx-route.sh가 hub/server.mjs, hub/bridge.mjs를 찾을 수 있도록
329
+ // 패키지 루트 경로를 ~/.claude/scripts/.tfx-pkg-root에 기록한다.
330
+ // dev mode에서는 항상 최신 경로를 기록 (--sync 시 강제 갱신).
331
+ {
332
+ const pkgRootForward = PLUGIN_ROOT.replace(/\\/g, "/");
333
+ const currentBreadcrumb = existsSync(BREADCRUMB_PATH)
334
+ ? readFileSync(BREADCRUMB_PATH, "utf8").trim()
335
+ : "";
336
+ if (currentBreadcrumb !== pkgRootForward || isSync) {
337
+ const breadcrumbDir = dirname(BREADCRUMB_PATH);
338
+ if (!existsSync(breadcrumbDir)) mkdirSync(breadcrumbDir, { recursive: true });
339
+ writeFileSync(BREADCRUMB_PATH, pkgRootForward + "\n", "utf8");
340
+ synced++;
341
+ }
342
+ }
343
+
344
+ // ── 에이전트 동기화 (.claude/agents/ → ~/.claude/agents/) ──
345
+ // slim-wrapper 등 커스텀 에이전트를 글로벌에 배포하여
346
+ // 다른 프로젝트에서도 subagent_type으로 참조 가능하게 한다.
347
+
348
+ const agentsSrc = join(PLUGIN_ROOT, ".claude", "agents");
349
+ const agentsDst = join(CLAUDE_DIR, "agents");
350
+
351
+ if (existsSync(agentsSrc)) {
352
+ if (!existsSync(agentsDst)) mkdirSync(agentsDst, { recursive: true });
353
+
354
+ for (const name of readdirSync(agentsSrc)) {
355
+ if (!name.endsWith(".md")) continue;
356
+
357
+ const src = join(agentsSrc, name);
358
+ const dst = join(agentsDst, name);
359
+
360
+ if (!existsSync(dst)) {
361
+ copyFileSync(src, dst);
362
+ synced++;
363
+ } else if (shouldSyncTextFile(src, dst)) {
364
+ copyFileSync(src, dst);
365
+ synced++;
366
+ }
367
+ }
368
+ }
369
+
370
+ // ── 스킬 동기화 ──
371
+ // SKILL.md + 하위 디렉토리(references/ 등)를 재귀적으로 동기화
372
+
373
+ const skillsSrc = join(PLUGIN_ROOT, "skills");
374
+ const skillsDst = join(CLAUDE_DIR, "skills");
375
+
376
+ function syncSkillDir(srcDir, dstDir) {
377
+ if (!existsSync(dstDir)) mkdirSync(dstDir, { recursive: true });
378
+
379
+ let count = 0;
380
+ for (const entry of readdirSync(srcDir, { withFileTypes: true })) {
381
+ const srcPath = join(srcDir, entry.name);
382
+ const dstPath = join(dstDir, entry.name);
383
+
384
+ if (entry.isDirectory()) {
385
+ count += syncSkillDir(srcPath, dstPath);
386
+ } else if (entry.name.endsWith(".md")) {
387
+ if (shouldSyncTextFile(srcPath, dstPath)) {
388
+ copyFileSync(srcPath, dstPath);
389
+ count++;
390
+ }
391
+ }
392
+ }
393
+ return count;
394
+ }
395
+
396
+ if (existsSync(skillsSrc)) {
397
+ for (const name of readdirSync(skillsSrc)) {
398
+ const skillDir = join(skillsSrc, name);
399
+ const skillMd = join(skillDir, "SKILL.md");
400
+ if (!existsSync(skillMd)) continue;
401
+
402
+ synced += syncSkillDir(skillDir, join(skillsDst, name));
403
+ }
404
+ }
405
+
406
+ // ── settings.json 통합 R/W ──
407
+ // 3개 섹션(statusLine, agentTeams, hooks)을 1회 read → 일괄 수정 → 1회 write
408
+
409
+ const settingsPath = join(CLAUDE_DIR, "settings.json");
410
+ const hudPath = join(CLAUDE_DIR, "hud", "hud-qos-status.mjs");
411
+
412
+ /**
413
+ * statusLine 섹션 적용.
414
+ * @param {object} s - settings 객체 (직접 변경)
415
+ * @returns {boolean} 변경 여부
416
+ */
417
+ function applyStatusLine(s) {
418
+ if (!existsSync(hudPath)) return false;
419
+ const currentCmd = s.statusLine?.command || "";
420
+ if (currentCmd.includes("hud-qos-status.mjs")) return false;
421
+
422
+ const nodePath = process.execPath.replace(/\\/g, "/");
423
+ const hudForward = hudPath.replace(/\\/g, "/");
424
+ const nodeRef = nodePath.includes(" ") ? `"${nodePath}"` : nodePath;
425
+ const hudRef = hudForward.includes(" ") ? `"${hudForward}"` : hudForward;
426
+
427
+ s.statusLine = { type: "command", command: `${nodeRef} ${hudRef}` };
428
+ return true;
429
+ }
430
+
431
+ /**
432
+ * Agent Teams 환경변수 섹션 적용.
433
+ * @param {object} s - settings 객체 (직접 변경)
434
+ * @returns {boolean} 변경 여부
435
+ */
436
+ function applyAgentTeams(s) {
437
+ if (!s.env) s.env = {};
438
+ let changed = false;
439
+
440
+ if (s.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS !== "1") {
441
+ s.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = "1";
442
+ changed = true;
443
+ }
444
+ // teammateMode: auto (tmux 밖이면 in-process, 안이면 split-pane)
445
+ if (!s.teammateMode) {
446
+ s.teammateMode = "auto";
447
+ changed = true;
448
+ }
449
+ return changed;
450
+ }
451
+
452
+ /**
453
+ * SessionStart + PreToolUse 훅 섹션 적용.
454
+ * @param {object} s - settings 객체 (직접 변경)
455
+ * @returns {boolean} 변경 여부
456
+ */
457
+ function applyHooks(s) {
458
+ if (!s.hooks) s.hooks = {};
459
+ let changed = false;
460
+
461
+ // ── SessionStart 훅 ──
462
+ if (!Array.isArray(s.hooks.SessionStart)) s.hooks.SessionStart = [];
463
+
464
+ const hasTrifluxHooks = s.hooks.SessionStart.some((entry) =>
465
+ Array.isArray(entry.hooks) &&
466
+ entry.hooks.some((h) => typeof h.command === "string" && h.command.includes("triflux")),
467
+ );
468
+
469
+ if (!hasTrifluxHooks) {
470
+ const nodePath = process.execPath.replace(/\\/g, "/");
471
+ const nodeRef = nodePath.includes(" ") ? `"${nodePath}"` : nodePath;
472
+ const pluginRoot = PLUGIN_ROOT.replace(/\\/g, "/");
473
+
474
+ s.hooks.SessionStart.push({
475
+ matcher: "*",
476
+ hooks: [
477
+ {
478
+ type: "command",
479
+ command: `${nodeRef} "${pluginRoot}/scripts/setup.mjs"`,
480
+ timeout: 10,
481
+ },
482
+ {
483
+ type: "command",
484
+ command: `${nodeRef} "${pluginRoot}/scripts/hub-ensure.mjs"`,
485
+ timeout: 8,
486
+ },
487
+ {
488
+ type: "command",
489
+ command: `${nodeRef} "${pluginRoot}/scripts/preflight-cache.mjs"`,
490
+ timeout: 5,
491
+ },
492
+ ],
493
+ });
494
+ changed = true;
495
+ }
496
+
497
+ // ── PreToolUse 훅: headless-guard (auto-route) ──
498
+ if (!Array.isArray(s.hooks.PreToolUse)) s.hooks.PreToolUse = [];
499
+
500
+ const guardScriptPath = join(CLAUDE_DIR, "scripts", "headless-guard-fast.sh").replace(/\\/g, "/");
501
+ const hasGuardHook = s.hooks.PreToolUse.some((entry) =>
502
+ Array.isArray(entry.hooks) &&
503
+ entry.hooks.some((h) => typeof h.command === "string" && h.command.includes("headless-guard")),
504
+ );
505
+
506
+ if (!hasGuardHook && existsSync(guardScriptPath.replace(/\//g, "\\"))) {
507
+ s.hooks.PreToolUse.push({
508
+ matcher: "Bash|Agent",
509
+ hooks: [
510
+ {
511
+ type: "command",
512
+ command: `bash "${guardScriptPath}"`,
513
+ timeout: 3,
514
+ },
515
+ ],
516
+ });
517
+ changed = true;
518
+ } else if (hasGuardHook) {
519
+ // 기존 경로를 동기화된 경로로 업데이트
520
+ for (const entry of s.hooks.PreToolUse) {
521
+ if (!Array.isArray(entry.hooks)) continue;
522
+ for (const h of entry.hooks) {
523
+ if (typeof h.command === "string" && h.command.includes("headless-guard") && !h.command.includes(guardScriptPath)) {
524
+ h.command = `bash "${guardScriptPath}"`;
525
+ changed = true;
526
+ }
527
+ }
528
+ }
529
+ }
530
+
531
+ return changed;
532
+ }
533
+
534
+ // 1회 읽기
535
+ let settings = {};
536
+ if (existsSync(settingsPath)) {
537
+ try { settings = JSON.parse(readFileSync(settingsPath, "utf8")); } catch { /* 기존 설정 보존 */ }
538
+ }
539
+
540
+ // 3개 섹션 일괄 수정 (각각 try-catch로 독립 실행)
541
+ let settingsChanged = false;
542
+ try { if (applyStatusLine(settings)) { settingsChanged = true; synced++; } } catch {}
543
+ try { if (applyAgentTeams(settings)) { settingsChanged = true; synced++; } } catch {}
544
+ try { if (applyHooks(settings)) { settingsChanged = true; synced++; } } catch {}
545
+
546
+ // 1회 쓰기
547
+ if (settingsChanged) {
548
+ try {
549
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
550
+ } catch {
551
+ // settings.json 쓰기 실패 시 무시
552
+ }
553
+ }
554
+
555
+ // ── Stale PID 파일 정리 (hub 좀비 방지) ──
556
+
557
+ const HUB_PID_FILE = join(CLAUDE_DIR, "cache", "tfx-hub", "hub.pid");
558
+ if (existsSync(HUB_PID_FILE)) {
559
+ try {
560
+ const pidInfo = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
561
+ process.kill(pidInfo.pid, 0); // 프로세스 존재 확인 (신호 미전송)
562
+ } catch {
563
+ try { unlinkSync(HUB_PID_FILE); } catch {} // 죽은 프로세스면 PID 파일 삭제
564
+ synced++;
565
+ }
566
+ }
567
+
568
+ // ── psmux 자동 설치 (Windows, headless 모드용) ──
569
+
570
+ if (process.platform === "win32") {
571
+ try {
572
+ execFileSync("where", ["psmux"], { stdio: "ignore" });
573
+ } catch {
574
+ // psmux 미설치 — winget으로 자동 설치 시도
575
+ console.log(" psmux 미설치 — winget으로 설치 중...");
576
+ try {
577
+ execFileSync("winget", ["install", "--id", "marlocarlo.psmux", "--accept-package-agreements", "--accept-source-agreements"], {
578
+ stdio: ["ignore", "pipe", "pipe"],
579
+ timeout: 60000,
580
+ });
581
+ console.log(" \x1b[32m✓\x1b[0m psmux 설치 완료");
582
+ synced++;
583
+ } catch {
584
+ console.log(" \x1b[33m⚠\x1b[0m psmux 자동 설치 실패 — 수동 설치: winget install psmux");
585
+ }
586
+ }
587
+ }
588
+
589
+ // ── HUD 에러 캐시 자동 클리어 (업데이트/재설치 ) ──
590
+
591
+ const cacheDir = join(CLAUDE_DIR, "cache");
592
+ const staleFiles = [
593
+ "claude-usage-cache.json",
594
+ ".claude-refresh-lock",
595
+ "codex-rate-limits-cache.json",
596
+ ];
597
+
598
+ for (const name of staleFiles) {
599
+ const fp = join(cacheDir, name);
600
+ if (!existsSync(fp)) continue;
601
+ try {
602
+ const content = readFileSync(fp, "utf8");
603
+ const parsed = JSON.parse(content);
604
+ // 에러 상태이거나 락 파일이면 삭제 → 새 세션에서 fresh start
605
+ if (parsed.error || name.startsWith(".")) {
606
+ unlinkSync(fp);
607
+ synced++;
608
+ }
609
+ } catch {
610
+ // 파싱 실패 파일도 삭제
611
+ try { unlinkSync(fp); } catch {}
612
+ }
613
+ }
614
+
615
+ // ── Windows bash PATH 자동 설정 ──
616
+ // Codex/Gemini가 cmd에는 있지만 bash에서 못 찾는 문제 해결
617
+
618
+ if (process.platform === "win32") {
619
+ const npmBin = join(process.env.APPDATA || "", "npm");
620
+ if (existsSync(npmBin)) {
621
+ const bashrcPath = join(homedir(), ".bashrc");
622
+ const pathExport = 'export PATH="$PATH:$APPDATA/npm"';
623
+ let needsUpdate = true;
624
+
625
+ if (existsSync(bashrcPath)) {
626
+ const content = readFileSync(bashrcPath, "utf8");
627
+ if (content.includes("APPDATA/npm") || content.includes("APPDATA\\npm")) {
628
+ needsUpdate = false;
629
+ }
630
+ }
631
+
632
+ if (needsUpdate) {
633
+ const line = `\n# triflux: Codex/Gemini CLI를 bash에서 사용하기 위한 PATH 설정\n${pathExport}\n`;
634
+ try {
635
+ writeFileSync(bashrcPath, (existsSync(bashrcPath) ? readFileSync(bashrcPath, "utf8") : "") + line, "utf8");
636
+ synced++;
637
+ } catch {}
638
+ }
639
+ }
640
+ }
641
+
642
+ // ── Codex 프로필 자동 보정 ──
643
+
644
+ const codexProfilesAdded = ensureCodexProfiles();
645
+ if (codexProfilesAdded > 0) {
646
+ synced++;
647
+ }
648
+
649
+ // ── MCP 인벤토리 백그라운드 갱신 ──
650
+
651
+ const mcpCheck = join(PLUGIN_ROOT, "scripts", "mcp-check.mjs");
652
+ if (existsSync(mcpCheck)) {
653
+ const child = spawn(process.execPath, [mcpCheck], {
654
+ detached: true,
655
+ stdio: "ignore",
656
+ windowsHide: true,
657
+ });
658
+ child.unref(); // 부모 프로세스와 분리 — 비동기 실행
659
+ }
660
+
661
+ // ── /tmp 임시 파일 자동 정리 (setup 지연 방지: fire-and-forget) ──
662
+ cleanupTmpFiles().catch(() => {});
663
+
664
+ // ── postinstall 배너 (npm install 시에만 출력) ──
665
+
666
+ if (process.env.npm_lifecycle_event === "postinstall") {
667
+ const G = "\x1b[32m";
668
+ const C = "\x1b[36m";
669
+ const Y = "\x1b[33m";
670
+ const D = "\x1b[2m";
671
+ const B = "\x1b[1m";
672
+ const R = "\x1b[0m";
673
+
674
+ const ver = (() => {
675
+ try {
676
+ return JSON.parse(readFileSync(join(PLUGIN_ROOT, "package.json"), "utf8")).version;
677
+ } catch { return "?"; }
678
+ })();
679
+
680
+ console.log(`
681
+ ${B}╔═══════════════════════════════════════════════╗${R}
682
+ ${B}║${R} ${C}triflux${R} ${D}v${ver}${R} ${B}— Setup Complete${R} ${B}║${R}
683
+ ${B}╚═══════════════════════════════════════════════╝${R}
684
+
685
+ ${G}✓${R} tfx-route.sh → ~/.claude/scripts/
686
+ ${G}✓${R} hud-qos-status → ~/.claude/hud/
687
+ ${G}✓${R} ${synced > 0 ? synced + " files synced" : "all files up to date"}
688
+ ${G}✓${R} HUD statusLine → settings.json
689
+
690
+ ${B}Commands:${R}
691
+ ${C}triflux${R} setup 파일 동기화 + HUD 설정
692
+ ${C}triflux${R} doctor CLI 진단 (Codex/Gemini 확인)
693
+ ${C}triflux${R} list 설치된 스킬 목록
694
+ ${C}triflux${R} update 최신 안정 버전으로 업데이트
695
+ ${C}triflux${R} update --dev dev 채널로 업데이트 (${D}dev 별칭 지원${R})
696
+
697
+ ${B}Shortcuts:${R}
698
+ ${C}tfx${R} triflux 축약
699
+ ${C}tfx-setup${R} triflux setup
700
+ ${C}tfx-doctor${R} triflux doctor
701
+
702
+ ${B}Skills (Claude Code):${R}
703
+ ${C}/tfx-auto${R} "작업" 자동 분류 + 병렬 실행
704
+ ${C}/tfx-auto-codex${R} "작업" Codex 리드 + Gemini 유지
705
+ ${C}/tfx-codex${R} "작업" Codex 전용 모드
706
+ ${C}/tfx-gemini${R} "작업" Gemini 전용 모드
707
+ ${C}/tfx-setup${R} HUD 설정 + 진단
708
+
709
+ ${Y}!${R} 세션 재시작 후 스킬이 활성화됩니다
710
+ ${D}https://github.com/tellang/triflux${R}
711
+ `);
712
+ }
713
+
714
+ process.exit(0);
715
+ }
716
+
717
+ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
718
+ main();
719
+ }
720
+