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.
- package/.claude-plugin/marketplace.json +34 -0
- package/.claude-plugin/plugin.json +22 -0
- package/config/mcp-registry.json +29 -0
- package/hub/account-broker.mjs +6 -4
- package/hub/cli-adapter-base.mjs +14 -14
- package/hub/lib/env-detect.mjs +47 -20
- package/hub/server.mjs +17 -15
- package/hub/team/headless.mjs +10 -0
- package/hub/team/swarm-hypervisor.mjs +2 -2
- package/hub/workers/delegator-mcp.mjs +129 -1
- package/hud/constants.mjs +24 -13
- package/hud/renderers.mjs +2 -1
- package/package.json +62 -21
- package/scripts/__tests__/keyword-detector.test.mjs +4 -4
- package/scripts/__tests__/release-governance.test.mjs +148 -0
- package/scripts/doctor-diagnose.mjs +6 -7
- package/scripts/lib/cross-review-utils.mjs +2 -2
- package/scripts/lib/mcp-filter.mjs +12 -24
- package/scripts/release/bump-version.mjs +77 -0
- package/scripts/release/check-sync.mjs +51 -0
- package/scripts/release/lib.mjs +303 -0
- package/scripts/release/prepare.mjs +85 -0
- package/scripts/release/publish.mjs +87 -0
- package/scripts/release/verify.mjs +81 -0
- package/scripts/release/version-manifest.json +26 -0
- package/scripts/remote-spawn.mjs +3 -3
- package/scripts/setup.mjs +18 -15
- package/scripts/tfx-route.sh +64 -8
- package/tui/codex-profile.mjs +457 -0
- package/tui/core.mjs +266 -0
- package/tui/doctor.mjs +375 -0
- package/tui/gemini-profile.mjs +299 -0
- package/tui/monitor-data.mjs +152 -0
- package/tui/monitor.mjs +339 -0
- package/tui/setup.mjs +598 -0
- package/CLAUDE.md +0 -212
- package/references/hosts.json +0 -46
- package/skills/tfx-workspace/async-tests/run-tests.sh +0 -203
- package/skills/tfx-workspace/evals/evals.json +0 -79
- package/skills/tfx-workspace/iteration-1/benchmark.json +0 -524
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/eval_metadata.json +0 -11
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/grading.json +0 -25
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/outputs/analysis.md +0 -154
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/grading.json +0 -25
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/outputs/analysis.md +0 -126
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/eval_metadata.json +0 -11
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/grading.json +0 -25
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/outputs/analysis.md +0 -119
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/grading.json +0 -25
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/outputs/analysis.md +0 -115
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/eval_metadata.json +0 -10
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/grading.json +0 -20
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/outputs/analysis.md +0 -86
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/grading.json +0 -20
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/outputs/analysis.md +0 -81
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-1/multi-team-creation/eval_metadata.json +0 -12
- package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/grading.json +0 -30
- package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/outputs/analysis.md +0 -316
- package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/grading.json +0 -30
- package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/outputs/analysis.md +0 -352
- package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-1/review.html +0 -1325
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/eval_metadata.json +0 -12
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/grading.json +0 -30
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/outputs/analysis.md +0 -97
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/grading.json +0 -30
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/outputs/analysis.md +0 -94
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/eval_metadata.json +0 -12
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/grading.json +0 -30
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/outputs/analysis.md +0 -209
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/grading.json +0 -30
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/outputs/analysis.md +0 -193
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-2/benchmark.json +0 -144
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/eval_metadata.json +0 -13
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/grading.json +0 -35
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/outputs/analysis.md +0 -382
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/grading.json +0 -35
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/outputs/analysis.md +0 -333
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-2/review.html +0 -1325
- package/skills/tfx-workspace/skill-snapshot/tfx-auto/SKILL.md +0 -217
- package/skills/tfx-workspace/skill-snapshot/tfx-auto-codex/SKILL.md +0 -77
- package/skills/tfx-workspace/skill-snapshot/tfx-codex/SKILL.md +0 -65
- package/skills/tfx-workspace/skill-snapshot/tfx-doctor/SKILL.md +0 -94
- package/skills/tfx-workspace/skill-snapshot/tfx-gemini/SKILL.md +0 -82
- package/skills/tfx-workspace/skill-snapshot/tfx-hub/SKILL.md +0 -133
- package/skills/tfx-workspace/skill-snapshot/tfx-multi/SKILL.md +0 -426
- package/skills/tfx-workspace/skill-snapshot/tfx-setup/SKILL.md +0 -101
package/scripts/tfx-route.sh
CHANGED
|
@@ -1193,12 +1193,14 @@ resolve_mcp_policy() {
|
|
|
1193
1193
|
fi
|
|
1194
1194
|
|
|
1195
1195
|
available_servers=$(get_cached_servers "$CLI_TYPE")
|
|
1196
|
-
|
|
1197
|
-
|
|
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
|
-
|
|
1409
|
-
|
|
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
|
+
});
|