triflux 10.9.21 → 10.9.23

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 (100) hide show
  1. package/.claude-plugin/marketplace.json +34 -0
  2. package/.claude-plugin/plugin.json +22 -0
  3. package/config/mcp-registry.json +29 -0
  4. package/hub/account-broker.mjs +6 -4
  5. package/hub/cli-adapter-base.mjs +14 -14
  6. package/hub/lib/env-detect.mjs +47 -20
  7. package/hub/server.mjs +17 -15
  8. package/hub/team/headless.mjs +10 -0
  9. package/hub/team/swarm-hypervisor.mjs +2 -2
  10. package/hub/workers/delegator-mcp.mjs +129 -1
  11. package/hud/constants.mjs +24 -13
  12. package/hud/renderers.mjs +2 -1
  13. package/package.json +62 -21
  14. package/scripts/__tests__/keyword-detector.test.mjs +4 -4
  15. package/scripts/__tests__/release-governance.test.mjs +148 -0
  16. package/scripts/doctor-diagnose.mjs +6 -7
  17. package/scripts/lib/cross-review-utils.mjs +2 -2
  18. package/scripts/lib/mcp-filter.mjs +12 -24
  19. package/scripts/release/bump-version.mjs +77 -0
  20. package/scripts/release/check-sync.mjs +51 -0
  21. package/scripts/release/lib.mjs +303 -0
  22. package/scripts/release/prepare.mjs +85 -0
  23. package/scripts/release/publish.mjs +87 -0
  24. package/scripts/release/verify.mjs +81 -0
  25. package/scripts/release/version-manifest.json +26 -0
  26. package/scripts/remote-spawn.mjs +3 -3
  27. package/scripts/setup.mjs +18 -15
  28. package/scripts/tfx-route.sh +64 -8
  29. package/tui/codex-profile.mjs +457 -0
  30. package/tui/core.mjs +266 -0
  31. package/tui/doctor.mjs +375 -0
  32. package/tui/gemini-profile.mjs +299 -0
  33. package/tui/monitor-data.mjs +152 -0
  34. package/tui/monitor.mjs +339 -0
  35. package/tui/setup.mjs +598 -0
  36. package/CLAUDE.md +0 -212
  37. package/references/hosts.json +0 -46
  38. package/skills/tfx-workspace/async-tests/run-tests.sh +0 -203
  39. package/skills/tfx-workspace/evals/evals.json +0 -79
  40. package/skills/tfx-workspace/iteration-1/benchmark.json +0 -524
  41. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/eval_metadata.json +0 -11
  42. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/grading.json +0 -25
  43. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/outputs/analysis.md +0 -154
  44. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/timing.json +0 -5
  45. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/grading.json +0 -25
  46. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/outputs/analysis.md +0 -126
  47. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/timing.json +0 -5
  48. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/eval_metadata.json +0 -11
  49. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/grading.json +0 -25
  50. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/outputs/analysis.md +0 -119
  51. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/timing.json +0 -5
  52. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/grading.json +0 -25
  53. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/outputs/analysis.md +0 -115
  54. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/timing.json +0 -5
  55. package/skills/tfx-workspace/iteration-1/hub-start-sequence/eval_metadata.json +0 -10
  56. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/grading.json +0 -20
  57. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/outputs/analysis.md +0 -86
  58. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/timing.json +0 -5
  59. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/grading.json +0 -20
  60. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/outputs/analysis.md +0 -81
  61. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/timing.json +0 -5
  62. package/skills/tfx-workspace/iteration-1/multi-team-creation/eval_metadata.json +0 -12
  63. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/grading.json +0 -30
  64. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/outputs/analysis.md +0 -316
  65. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/timing.json +0 -5
  66. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/grading.json +0 -30
  67. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/outputs/analysis.md +0 -352
  68. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/timing.json +0 -5
  69. package/skills/tfx-workspace/iteration-1/review.html +0 -1325
  70. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/eval_metadata.json +0 -12
  71. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/grading.json +0 -30
  72. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/outputs/analysis.md +0 -97
  73. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/timing.json +0 -5
  74. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/grading.json +0 -30
  75. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/outputs/analysis.md +0 -94
  76. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/timing.json +0 -5
  77. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/eval_metadata.json +0 -12
  78. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/grading.json +0 -30
  79. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/outputs/analysis.md +0 -209
  80. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/timing.json +0 -5
  81. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/grading.json +0 -30
  82. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/outputs/analysis.md +0 -193
  83. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/timing.json +0 -5
  84. package/skills/tfx-workspace/iteration-2/benchmark.json +0 -144
  85. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/eval_metadata.json +0 -13
  86. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/grading.json +0 -35
  87. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/outputs/analysis.md +0 -382
  88. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/timing.json +0 -5
  89. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/grading.json +0 -35
  90. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/outputs/analysis.md +0 -333
  91. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/timing.json +0 -5
  92. package/skills/tfx-workspace/iteration-2/review.html +0 -1325
  93. package/skills/tfx-workspace/skill-snapshot/tfx-auto/SKILL.md +0 -217
  94. package/skills/tfx-workspace/skill-snapshot/tfx-auto-codex/SKILL.md +0 -77
  95. package/skills/tfx-workspace/skill-snapshot/tfx-codex/SKILL.md +0 -65
  96. package/skills/tfx-workspace/skill-snapshot/tfx-doctor/SKILL.md +0 -94
  97. package/skills/tfx-workspace/skill-snapshot/tfx-gemini/SKILL.md +0 -82
  98. package/skills/tfx-workspace/skill-snapshot/tfx-hub/SKILL.md +0 -133
  99. package/skills/tfx-workspace/skill-snapshot/tfx-multi/SKILL.md +0 -426
  100. package/skills/tfx-workspace/skill-snapshot/tfx-setup/SKILL.md +0 -101
@@ -1193,12 +1193,14 @@ resolve_mcp_policy() {
1193
1193
  fi
1194
1194
 
1195
1195
  available_servers=$(get_cached_servers "$CLI_TYPE")
1196
- if [[ "$CLI_TYPE" == "codex" && "${TFX_CODEX_TRANSPORT:-auto}" != "mcp" ]]; then
1197
- available_servers=""
1196
+ # Codex exec 모드에서도 config.toml의 MCP 서버를 전부 시작하므로,
1197
+ # transport 모드와 관계없이 registered servers를 전달하여 불필요한 서버를
1198
+ # enabled=false로 비활성화해야 한다.
1199
+ # 캐시가 비어있으면 config.toml에서 직접 서버 목록을 추출한다.
1200
+ if [[ -z "$available_servers" && "$CLI_TYPE" == "codex" && -f "$_CODEX_CONFIG" ]]; then
1201
+ available_servers=$(sed -n 's/^\[mcp_servers\.\([^].]*\)\]$/\1/p' "$_CODEX_CONFIG" 2>/dev/null \
1202
+ | sort -u | tr '\n' ',' | sed 's/,$//')
1198
1203
  fi
1199
- # Codex 0.115+: 미등록 서버에 config override(enabled=true/false 모두)를 보내면
1200
- # "invalid transport" 에러 발생. 캐시 비어있으면 빈 문자열이 유지되어
1201
- # mcp-filter가 override를 생성하지 않는다.
1202
1204
 
1203
1205
  local -a cmd=(
1204
1206
  "$NODE_BIN" "$filter_script" shell
@@ -1398,6 +1400,53 @@ resolve_codex_mcp_script() {
1398
1400
  "$sd/hub/workers/codex-mcp.mjs" "$sd/../hub/workers/codex-mcp.mjs"
1399
1401
  }
1400
1402
 
1403
+ ## ── Config Swap: 프로필별 MCP 서버 필터링 ──
1404
+ # codex exec는 -c flag로 MCP enabled/disabled를 제어할 수 없다.
1405
+ # config.toml을 원자적으로 교체하여 불필요한 서버 시작을 방지한다.
1406
+ _codex_config_swap() {
1407
+ local action="$1" # "filter" or "restore"
1408
+ local config="$_CODEX_CONFIG"
1409
+ local backup="${config}.pre-exec"
1410
+
1411
+ if [[ "$action" == "filter" && -f "$config" ]]; then
1412
+ # MCP 프로필에서 허용된 서버 목록 추출
1413
+ local allowed_pat=""
1414
+ for flag in "${CODEX_CONFIG_FLAGS[@]}"; do
1415
+ if [[ "$flag" =~ mcp_servers\.([^.]+)\.enabled=true ]]; then
1416
+ [[ -n "$allowed_pat" ]] && allowed_pat="${allowed_pat}|"
1417
+ allowed_pat="${allowed_pat}${BASH_REMATCH[1]}"
1418
+ fi
1419
+ done
1420
+
1421
+ # 백업 생성 (이미 있으면 다른 워커가 swap 중 — 건드리지 않음)
1422
+ if [[ -f "$backup" ]]; then
1423
+ echo "[tfx-route] config.toml swap 스킵: 다른 워커가 사용 중" >&2
1424
+ return 0
1425
+ fi
1426
+ cp "$config" "$backup"
1427
+
1428
+ # awk로 필터링: 비허용 MCP 서버 섹션 제거, 나머지 그대로 유지
1429
+ awk -v keep="$allowed_pat" '
1430
+ BEGIN { skip=0 }
1431
+ /^\[mcp_servers\./ {
1432
+ name=$0; gsub(/^\[mcp_servers\./, "", name); gsub(/[\].].*/, "", name)
1433
+ if (keep == "" || name !~ "^(" keep ")$") { skip=1; next }
1434
+ else { skip=0 }
1435
+ }
1436
+ /^\[/ && !/^\[mcp_servers\./ { skip=0 }
1437
+ !skip { print }
1438
+ ' "$backup" > "$config"
1439
+
1440
+ local kept=0
1441
+ [[ -n "$allowed_pat" ]] && kept=$(echo "$allowed_pat" | tr '|' '\n' | wc -l | tr -d ' ')
1442
+ echo "[tfx-route] config.toml swap: ${kept}개 MCP 서버만 활성" >&2
1443
+
1444
+ elif [[ "$action" == "restore" && -f "$backup" ]]; then
1445
+ mv "$backup" "$config" 2>/dev/null
1446
+ echo "[tfx-route] config.toml 복원 완료" >&2
1447
+ fi
1448
+ }
1449
+
1401
1450
  run_codex_exec() {
1402
1451
  local prompt="$1"
1403
1452
  local use_tee_flag="$2"
@@ -1405,9 +1454,8 @@ run_codex_exec() {
1405
1454
  local worker_pid
1406
1455
  local -a codex_args=()
1407
1456
  read -r -a codex_args <<< "$CLI_ARGS"
1408
- if [[ ${#CODEX_CONFIG_FLAGS[@]} -gt 0 ]]; then
1409
- codex_args+=("${CODEX_CONFIG_FLAGS[@]}")
1410
- fi
1457
+ # -c flags는 codex exec에서 MCP enabled 제어 불가 — config swap으로 대체
1458
+ # config swap은 codex 블록 최상단(_codex_config_swap "filter")에서 실행됨
1411
1459
 
1412
1460
  if [[ "$use_tee_flag" == "true" ]]; then
1413
1461
  "$TIMEOUT_BIN" "$TIMEOUT_SEC" "$CLI_CMD" "${codex_args[@]}" "$prompt" < /dev/null 2>"$STDERR_LOG" | tee "$STDOUT_LOG" &
@@ -1656,6 +1704,12 @@ FALLBACK_EOF
1656
1704
  fi
1657
1705
 
1658
1706
  if [[ "$CLI_TYPE" == "codex" ]]; then
1707
+ # Config swap: 프로필에 맞는 MCP 서버만 남긴 임시 config 적용
1708
+ # run_codex_mcp / run_codex_exec 어느 경로든 적용되도록 최상단에서 실행
1709
+ _codex_config_swap "filter"
1710
+ # swap 후 config override 플래그 클리어 — 제거된 서버에 override 보내면 "invalid transport" 에러
1711
+ CODEX_CONFIG_FLAGS=()
1712
+ CODEX_CONFIG_JSON="{}"
1659
1713
  codex_transport_effective="exec"
1660
1714
  if [[ "$TFX_CODEX_TRANSPORT" != "exec" ]]; then
1661
1715
  run_codex_mcp "$FULL_PROMPT" "$use_tee" || exit_code=$?
@@ -1676,6 +1730,8 @@ FALLBACK_EOF
1676
1730
  codex_transport_effective="exec"
1677
1731
  fi
1678
1732
  echo "[tfx-route] codex_transport_effective=$codex_transport_effective" >&2
1733
+ # Config swap 복원 (성공/실패 관계없이)
1734
+ _codex_config_swap "restore"
1679
1735
 
1680
1736
  elif [[ "$CLI_TYPE" == "gemini" ]]; then
1681
1737
  local gemini_model
@@ -0,0 +1,457 @@
1
+ #!/usr/bin/env node
2
+ // tui/codex-profile.mjs — Interactive Codex Profile Manager
3
+ import {
4
+ copyFileSync,
5
+ existsSync,
6
+ mkdirSync,
7
+ readFileSync,
8
+ writeFileSync,
9
+ } from "node:fs";
10
+ import { homedir } from "node:os";
11
+ import { join } from "node:path";
12
+ import {
13
+ BOLD,
14
+ box,
15
+ CYAN,
16
+ clear,
17
+ confirm,
18
+ DIM,
19
+ divider,
20
+ fail,
21
+ GREEN,
22
+ info,
23
+ input,
24
+ label,
25
+ ok,
26
+ onExit,
27
+ RED,
28
+ RESET,
29
+ select,
30
+ showCursor,
31
+ table,
32
+ WHITE,
33
+ warn,
34
+ YELLOW,
35
+ } from "./core.mjs";
36
+
37
+ const CODEX_DIR = join(homedir(), ".codex");
38
+ const CONFIG_PATH = join(CODEX_DIR, "config.toml");
39
+
40
+ const KNOWN_MODELS = [
41
+ { label: "gpt-5.4", hint: "최신 플래그십" },
42
+ { label: "gpt-5.3-codex", hint: "코딩 특화" },
43
+ { label: "gpt-5.1-codex-mini", hint: "경량 Spark" },
44
+ { label: "o3", hint: "추론 특화" },
45
+ { label: "o4-mini", hint: "추론 경량" },
46
+ { label: "직접 입력", hint: "" },
47
+ ];
48
+
49
+ const EFFORT_LEVELS = [
50
+ { label: "low", hint: "빠른 응답, 최소 추론" },
51
+ { label: "medium", hint: "균형 잡힌 추론" },
52
+ { label: "high", hint: "깊은 추론" },
53
+ { label: "xhigh", hint: "최대 추론 (느림)" },
54
+ ];
55
+
56
+ // ── TOML Parsing ──
57
+
58
+ function readConfig() {
59
+ if (!existsSync(CONFIG_PATH)) return { raw: "", defaults: {}, profiles: [] };
60
+ const raw = readFileSync(CONFIG_PATH, "utf8");
61
+ return { raw, ...parseConfig(raw) };
62
+ }
63
+
64
+ function parseConfig(raw) {
65
+ const lines = raw.split("\n");
66
+ const defaults = {};
67
+ const profiles = [];
68
+ let currentSection = null;
69
+ let currentProfile = null;
70
+
71
+ for (const line of lines) {
72
+ const trimmed = line.trim();
73
+ if (!trimmed || trimmed.startsWith("#")) continue;
74
+
75
+ const sectionMatch = trimmed.match(/^\[(.+)\]$/);
76
+ if (sectionMatch) {
77
+ const name = sectionMatch[1];
78
+ const profileMatch = name.match(/^profiles\.(\w+)$/);
79
+ if (profileMatch) {
80
+ currentSection = "profile";
81
+ currentProfile = { name: profileMatch[1] };
82
+ profiles.push(currentProfile);
83
+ } else {
84
+ currentSection = name;
85
+ currentProfile = null;
86
+ }
87
+ continue;
88
+ }
89
+
90
+ const kvMatch = trimmed.match(/^(\w+)\s*=\s*(.+)$/);
91
+ if (kvMatch) {
92
+ const [, key, rawVal] = kvMatch;
93
+ const value = rawVal.replace(/^["']|["']$/g, "").trim();
94
+ if (currentSection === "profile" && currentProfile) {
95
+ currentProfile[key] = value;
96
+ } else if (!currentSection) {
97
+ defaults[key] = value;
98
+ }
99
+ }
100
+ }
101
+
102
+ return { defaults, profiles };
103
+ }
104
+
105
+ function writeProfile(raw, profileName, props) {
106
+ const lines = raw.split("\n");
107
+ const sectionRe = new RegExp(`^\\[profiles\\.${escRe(profileName)}\\]\\s*$`);
108
+ let inSection = false;
109
+ let sectionStart = -1;
110
+ let sectionEnd = lines.length;
111
+
112
+ for (let i = 0; i < lines.length; i++) {
113
+ if (sectionRe.test(lines[i].trim())) {
114
+ inSection = true;
115
+ sectionStart = i;
116
+ continue;
117
+ }
118
+ if (inSection && lines[i].trim().startsWith("[")) {
119
+ sectionEnd = i;
120
+ break;
121
+ }
122
+ }
123
+
124
+ if (sectionStart === -1) {
125
+ // Append new profile section
126
+ const newLines = [`[profiles.${profileName}]`];
127
+ for (const [k, v] of Object.entries(props)) {
128
+ newLines.push(`${k} = "${v}"`);
129
+ }
130
+ return raw.trimEnd() + "\n" + newLines.join("\n") + "\n";
131
+ }
132
+
133
+ // Replace existing section body
134
+ const newBody = [];
135
+ for (const [k, v] of Object.entries(props)) {
136
+ newBody.push(`${k} = "${v}"`);
137
+ }
138
+ lines.splice(sectionStart + 1, sectionEnd - sectionStart - 1, ...newBody);
139
+ return lines.join("\n");
140
+ }
141
+
142
+ function deleteProfile(raw, profileName) {
143
+ const lines = raw.split("\n");
144
+ const sectionRe = new RegExp(`^\\[profiles\\.${escRe(profileName)}\\]\\s*$`);
145
+ let inSection = false;
146
+ let start = -1;
147
+ let end = lines.length;
148
+
149
+ for (let i = 0; i < lines.length; i++) {
150
+ if (sectionRe.test(lines[i].trim())) {
151
+ inSection = true;
152
+ start = i;
153
+ continue;
154
+ }
155
+ if (inSection && lines[i].trim().startsWith("[")) {
156
+ end = i;
157
+ break;
158
+ }
159
+ }
160
+
161
+ if (start === -1) return raw;
162
+ // Remove trailing blank lines too
163
+ while (end < lines.length && lines[end].trim() === "") end++;
164
+ lines.splice(start, end - start);
165
+ return lines.join("\n");
166
+ }
167
+
168
+ function setDefault(raw, key, value) {
169
+ const lines = raw.split("\n");
170
+ const keyRe = new RegExp(`^${escRe(key)}\\s*=`);
171
+
172
+ for (let i = 0; i < lines.length; i++) {
173
+ if (lines[i].trim().startsWith("[")) break; // hit first section
174
+ if (keyRe.test(lines[i].trim())) {
175
+ lines[i] = `${key} = "${value}"`;
176
+ return lines.join("\n");
177
+ }
178
+ }
179
+
180
+ // Key not found — insert before first section
181
+ for (let i = 0; i < lines.length; i++) {
182
+ if (lines[i].trim().startsWith("[")) {
183
+ lines.splice(i, 0, `${key} = "${value}"`);
184
+ return lines.join("\n");
185
+ }
186
+ }
187
+
188
+ return raw + `\n${key} = "${value}"\n`;
189
+ }
190
+
191
+ function escRe(s) {
192
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
193
+ }
194
+
195
+ // ── UI Flows ──
196
+
197
+ function showStatus(config) {
198
+ const { defaults, profiles } = config;
199
+
200
+ console.log();
201
+ label("기본 모델", `${WHITE}${defaults.model || "미설정"}${RESET}`);
202
+ label(
203
+ "기본 Effort",
204
+ `${WHITE}${defaults.model_reasoning_effort || "미설정"}${RESET}`,
205
+ );
206
+ console.log();
207
+
208
+ if (profiles.length === 0) {
209
+ warn("등록된 프로파일이 없습니다.");
210
+ return;
211
+ }
212
+
213
+ const headers = ["프로파일", "모델", "Effort", "기타"];
214
+ const rows = profiles.map((p) => {
215
+ const extras = Object.entries(p)
216
+ .filter(([k]) => !["name", "model", "model_reasoning_effort"].includes(k))
217
+ .map(([k, v]) => `${k}=${v}`)
218
+ .join(", ");
219
+ return [
220
+ `${CYAN}${p.name}${RESET}`,
221
+ p.model || DIM + "inherit" + RESET,
222
+ effortColor(p.model_reasoning_effort),
223
+ extras ? `${DIM}${extras}${RESET}` : "",
224
+ ];
225
+ });
226
+
227
+ table(headers, rows);
228
+ }
229
+
230
+ function effortColor(effort) {
231
+ if (!effort) return `${DIM}inherit${RESET}`;
232
+ const colors = { low: GREEN, medium: CYAN, high: YELLOW, xhigh: RED };
233
+ return `${colors[effort] || ""}${effort}${RESET}`;
234
+ }
235
+
236
+ async function pickModel(current) {
237
+ const idx = KNOWN_MODELS.findIndex((m) => m.label === current);
238
+ const choice = await select("모델 선택", KNOWN_MODELS, {
239
+ initial: Math.max(0, idx),
240
+ });
241
+ if (!choice) return null;
242
+ if (choice.value.label === "직접 입력") {
243
+ return await input("모델 ID", current || "");
244
+ }
245
+ return choice.value.label;
246
+ }
247
+
248
+ async function pickEffort(current) {
249
+ const idx = EFFORT_LEVELS.findIndex((e) => e.label === current);
250
+ const choice = await select("Reasoning Effort 선택", EFFORT_LEVELS, {
251
+ initial: Math.max(0, idx),
252
+ });
253
+ if (!choice) return null;
254
+ return choice.value.label;
255
+ }
256
+
257
+ async function editProfile(config) {
258
+ const { profiles } = config;
259
+ if (profiles.length === 0) {
260
+ warn("편집할 프로파일이 없습니다.");
261
+ return config;
262
+ }
263
+
264
+ const options = profiles.map((p) => ({
265
+ label: p.name,
266
+ hint: `${DIM}${p.model || "inherit"} / ${p.model_reasoning_effort || "inherit"}${RESET}`,
267
+ }));
268
+
269
+ const picked = await select("편집할 프로파일", options);
270
+ if (!picked) return config;
271
+
272
+ const profile = profiles[picked.index];
273
+ console.log();
274
+ info(
275
+ `현재: ${BOLD}${profile.name}${RESET} → ${profile.model} / ${profile.model_reasoning_effort}`,
276
+ );
277
+
278
+ const newModel = await pickModel(profile.model);
279
+ if (newModel === null) return config;
280
+
281
+ const newEffort = await pickEffort(profile.model_reasoning_effort);
282
+ if (newEffort === null) return config;
283
+
284
+ console.log();
285
+ info(
286
+ `변경: ${profile.model} → ${BOLD}${newModel}${RESET}, ${profile.model_reasoning_effort} → ${BOLD}${newEffort}${RESET}`,
287
+ );
288
+
289
+ if (!(await confirm("저장하시겠습니까?"))) return config;
290
+
291
+ const props = { model: newModel, model_reasoning_effort: newEffort };
292
+ // Preserve extra props (like model_temperature)
293
+ for (const [k, v] of Object.entries(profile)) {
294
+ if (!["name", "model", "model_reasoning_effort"].includes(k)) props[k] = v;
295
+ }
296
+
297
+ const raw = writeProfile(config.raw, profile.name, props);
298
+ save(raw);
299
+ ok(`${profile.name} 프로파일 저장 완료`);
300
+ return readConfig();
301
+ }
302
+
303
+ async function editDefault(config) {
304
+ const { defaults } = config;
305
+ info(`현재 기본 모델: ${BOLD}${defaults.model || "미설정"}${RESET}`);
306
+ info(
307
+ `현재 기본 Effort: ${BOLD}${defaults.model_reasoning_effort || "미설정"}${RESET}`,
308
+ );
309
+
310
+ const newModel = await pickModel(defaults.model);
311
+ if (newModel === null) return config;
312
+
313
+ const newEffort = await pickEffort(defaults.model_reasoning_effort);
314
+ if (newEffort === null) return config;
315
+
316
+ console.log();
317
+ info(
318
+ `변경: ${defaults.model} → ${BOLD}${newModel}${RESET}, ${defaults.model_reasoning_effort} → ${BOLD}${newEffort}${RESET}`,
319
+ );
320
+
321
+ if (!(await confirm("저장하시겠습니까?"))) return config;
322
+
323
+ let raw = setDefault(config.raw, "model", newModel);
324
+ raw = setDefault(raw, "model_reasoning_effort", newEffort);
325
+ save(raw);
326
+ ok("기본 설정 저장 완료");
327
+ return readConfig();
328
+ }
329
+
330
+ async function addProfile(config) {
331
+ const name = await input("새 프로파일 이름");
332
+ if (!name) return config;
333
+
334
+ if (config.profiles.some((p) => p.name === name)) {
335
+ fail(`'${name}' 프로파일이 이미 존재합니다.`);
336
+ return config;
337
+ }
338
+
339
+ const model = await pickModel("");
340
+ if (!model) return config;
341
+
342
+ const effort = await pickEffort("");
343
+ if (!effort) return config;
344
+
345
+ console.log();
346
+ info(`추가: ${BOLD}${name}${RESET} → ${model} / ${effort}`);
347
+ if (!(await confirm("저장하시겠습니까?"))) return config;
348
+
349
+ const raw = writeProfile(config.raw, name, {
350
+ model,
351
+ model_reasoning_effort: effort,
352
+ });
353
+ save(raw);
354
+ ok(`${name} 프로파일 추가 완료`);
355
+ return readConfig();
356
+ }
357
+
358
+ async function removeProfile(config) {
359
+ const { profiles } = config;
360
+ if (profiles.length === 0) {
361
+ warn("삭제할 프로파일이 없습니다.");
362
+ return config;
363
+ }
364
+
365
+ const options = profiles.map((p) => ({ label: p.name, hint: `${p.model}` }));
366
+ const picked = await select("삭제할 프로파일", options);
367
+ if (!picked) return config;
368
+
369
+ const name = profiles[picked.index].name;
370
+ if (
371
+ !(await confirm(
372
+ `${RED}${name}${RESET} 프로파일을 삭제하시겠습니까?`,
373
+ false,
374
+ ))
375
+ ) {
376
+ return config;
377
+ }
378
+
379
+ const raw = deleteProfile(config.raw, name);
380
+ save(raw);
381
+ ok(`${name} 프로파일 삭제 완료`);
382
+ return readConfig();
383
+ }
384
+
385
+ function save(content) {
386
+ if (!existsSync(CODEX_DIR)) mkdirSync(CODEX_DIR, { recursive: true });
387
+
388
+ // Backup before write
389
+ if (existsSync(CONFIG_PATH)) {
390
+ const backupPath = CONFIG_PATH + ".bak";
391
+ copyFileSync(CONFIG_PATH, backupPath);
392
+ }
393
+
394
+ writeFileSync(CONFIG_PATH, content, "utf8");
395
+ }
396
+
397
+ // ── Main Loop ──
398
+
399
+ const MENU = [
400
+ { label: "프로파일 모델 변경", hint: "모델/effort 수정" },
401
+ { label: "기본 모델 변경", hint: "top-level default" },
402
+ { label: "프로파일 추가", hint: "새 프로파일 생성" },
403
+ { label: "프로파일 삭제", hint: "기존 프로파일 제거" },
404
+ { label: "종료", hint: "Ctrl+C" },
405
+ ];
406
+
407
+ async function main() {
408
+ onExit(() => {});
409
+ clear();
410
+
411
+ let config = readConfig();
412
+
413
+ if (!existsSync(CONFIG_PATH)) {
414
+ fail(`config.toml 미존재: ${CONFIG_PATH}`);
415
+ info("codex를 먼저 설치하거나 /tfx-setup을 실행하세요.");
416
+ process.exit(1);
417
+ }
418
+
419
+ while (true) {
420
+ box("Codex Profile Manager", 46);
421
+ showStatus(config);
422
+ console.log();
423
+
424
+ const choice = await select("작업 선택", MENU);
425
+ if (!choice || choice.index === 4) {
426
+ console.log();
427
+ info("종료합니다.");
428
+ showCursor();
429
+ break;
430
+ }
431
+
432
+ console.log();
433
+ switch (choice.index) {
434
+ case 0:
435
+ config = await editProfile(config);
436
+ break;
437
+ case 1:
438
+ config = await editDefault(config);
439
+ break;
440
+ case 2:
441
+ config = await addProfile(config);
442
+ break;
443
+ case 3:
444
+ config = await removeProfile(config);
445
+ break;
446
+ }
447
+
448
+ console.log();
449
+ divider(46);
450
+ }
451
+ }
452
+
453
+ main().catch((e) => {
454
+ showCursor();
455
+ console.error(e);
456
+ process.exit(1);
457
+ });