triflux 3.2.0-dev.1 → 3.2.0-dev.11
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/README.ko.md +26 -18
- package/README.md +26 -18
- package/bin/triflux.mjs +1614 -1084
- package/hooks/hooks.json +12 -0
- package/hooks/keyword-rules.json +354 -0
- package/hub/bridge.mjs +371 -193
- package/hub/hitl.mjs +45 -31
- package/hub/pipe.mjs +457 -0
- package/hub/router.mjs +422 -161
- package/hub/server.mjs +429 -344
- package/hub/store.mjs +388 -314
- package/hub/team/cli-team-common.mjs +348 -0
- package/hub/team/cli-team-control.mjs +393 -0
- package/hub/team/cli-team-start.mjs +516 -0
- package/hub/team/cli-team-status.mjs +269 -0
- package/hub/team/cli.mjs +99 -368
- package/hub/team/dashboard.mjs +165 -64
- package/hub/team/native-supervisor.mjs +300 -0
- package/hub/team/native.mjs +62 -0
- package/hub/team/nativeProxy.mjs +534 -0
- package/hub/team/orchestrator.mjs +90 -31
- package/hub/team/pane.mjs +149 -101
- package/hub/team/psmux.mjs +297 -0
- package/hub/team/session.mjs +608 -186
- package/hub/team/shared.mjs +13 -0
- package/hub/team/staleState.mjs +299 -0
- package/hub/tools.mjs +140 -53
- package/hub/workers/claude-worker.mjs +446 -0
- package/hub/workers/codex-mcp.mjs +414 -0
- package/hub/workers/factory.mjs +18 -0
- package/hub/workers/gemini-worker.mjs +349 -0
- package/hub/workers/interface.mjs +41 -0
- package/hud/hud-qos-status.mjs +1789 -1732
- package/package.json +6 -2
- package/scripts/__tests__/keyword-detector.test.mjs +234 -0
- package/scripts/hub-ensure.mjs +83 -0
- package/scripts/keyword-detector.mjs +272 -0
- package/scripts/keyword-rules-expander.mjs +521 -0
- package/scripts/lib/keyword-rules.mjs +168 -0
- package/scripts/psmux-steering-prototype.sh +368 -0
- package/scripts/run.cjs +62 -0
- package/scripts/setup.mjs +189 -7
- package/scripts/test-tfx-route-no-claude-native.mjs +49 -0
- package/scripts/tfx-route-worker.mjs +161 -0
- package/scripts/tfx-route.sh +943 -508
- package/skills/tfx-auto/SKILL.md +90 -564
- package/skills/tfx-auto-codex/SKILL.md +77 -0
- package/skills/tfx-codex/SKILL.md +1 -4
- package/skills/tfx-doctor/SKILL.md +1 -0
- package/skills/tfx-gemini/SKILL.md +1 -4
- package/skills/tfx-multi/SKILL.md +296 -0
- package/skills/tfx-setup/SKILL.md +1 -4
- package/skills/tfx-team/SKILL.md +0 -172
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# psmux-steering-prototype.sh
|
|
3
|
+
# Windows psmux 환경에서 lead/codex-worker/gemini-worker pane을 만들고
|
|
4
|
+
# send-keys + pipe-pane 기반으로 실시간 CLI 스티어링을 실험하는 프로토타입.
|
|
5
|
+
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
PSMUX_BIN="${PSMUX_BIN:-psmux}"
|
|
9
|
+
SESSION_NAME="${PSMUX_SESSION_NAME:-triflux-steering}"
|
|
10
|
+
WINDOW_NAME="${PSMUX_WINDOW_NAME:-control}"
|
|
11
|
+
PANE_LEAD="lead"
|
|
12
|
+
PANE_CODEX="codex-worker"
|
|
13
|
+
PANE_GEMINI="gemini-worker"
|
|
14
|
+
SHELL_COMMAND="${PSMUX_SHELL_COMMAND:-powershell.exe -NoLogo}"
|
|
15
|
+
CAPTURE_ROOT="${PSMUX_CAPTURE_ROOT:-${TMPDIR:-/tmp}/psmux-steering}"
|
|
16
|
+
CAPTURE_DIR="${CAPTURE_ROOT}/${SESSION_NAME}"
|
|
17
|
+
CAPTURE_HELPER_PATH="${CAPTURE_ROOT}/pipe-pane-capture.ps1"
|
|
18
|
+
COMPLETION_PREFIX="__TRIFLUX_DONE__:"
|
|
19
|
+
POLL_INTERVAL_SEC="${PSMUX_POLL_INTERVAL_SEC:-1}"
|
|
20
|
+
|
|
21
|
+
usage() {
|
|
22
|
+
cat <<'EOF'
|
|
23
|
+
Usage:
|
|
24
|
+
scripts/psmux-steering-prototype.sh start
|
|
25
|
+
scripts/psmux-steering-prototype.sh demo
|
|
26
|
+
scripts/psmux-steering-prototype.sh attach
|
|
27
|
+
scripts/psmux-steering-prototype.sh send <pane-name> <command text>
|
|
28
|
+
scripts/psmux-steering-prototype.sh send-no-enter <pane-name> <text>
|
|
29
|
+
scripts/psmux-steering-prototype.sh steer-ps <pane-name> <powershell command>
|
|
30
|
+
scripts/psmux-steering-prototype.sh wait <pane-name> <regex> [timeout-sec]
|
|
31
|
+
scripts/psmux-steering-prototype.sh logs
|
|
32
|
+
scripts/psmux-steering-prototype.sh cleanup
|
|
33
|
+
|
|
34
|
+
Pane names:
|
|
35
|
+
lead | codex-worker | gemini-worker
|
|
36
|
+
|
|
37
|
+
Environment overrides:
|
|
38
|
+
PSMUX_BIN
|
|
39
|
+
PSMUX_SESSION_NAME
|
|
40
|
+
PSMUX_WINDOW_NAME
|
|
41
|
+
PSMUX_SHELL_COMMAND
|
|
42
|
+
PSMUX_CAPTURE_ROOT
|
|
43
|
+
PSMUX_POLL_INTERVAL_SEC
|
|
44
|
+
EOF
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
log() {
|
|
48
|
+
printf '[psmux-steering] %s\n' "$*"
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
die() {
|
|
52
|
+
printf '[psmux-steering] ERROR: %s\n' "$*" >&2
|
|
53
|
+
exit 1
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
require_psmux() {
|
|
57
|
+
command -v "$PSMUX_BIN" >/dev/null 2>&1 || die "Cannot find '$PSMUX_BIN' in PATH."
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
session_target() {
|
|
61
|
+
printf '%s:%s' "$SESSION_NAME" "$WINDOW_NAME"
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
pane_target_from_index() {
|
|
65
|
+
local pane_index="$1"
|
|
66
|
+
printf '%s.%s' "$(session_target)" "$pane_index"
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
log_file_for() {
|
|
70
|
+
local pane_name="$1"
|
|
71
|
+
printf '%s/%s.log' "$CAPTURE_DIR" "$pane_name"
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
to_windows_path() {
|
|
75
|
+
local path_value="$1"
|
|
76
|
+
|
|
77
|
+
if command -v cygpath >/dev/null 2>&1; then
|
|
78
|
+
cygpath -aw "$path_value"
|
|
79
|
+
return 0
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
printf '%s\n' "$path_value"
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
ensure_capture_helper() {
|
|
86
|
+
mkdir -p "$CAPTURE_ROOT"
|
|
87
|
+
|
|
88
|
+
cat >"$CAPTURE_HELPER_PATH" <<'EOF'
|
|
89
|
+
param(
|
|
90
|
+
[Parameter(Mandatory = $true)][string]$Path
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
$parent = Split-Path -Parent $Path
|
|
94
|
+
if ($parent) {
|
|
95
|
+
New-Item -ItemType Directory -Force -Path $parent | Out-Null
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
$reader = [Console]::In
|
|
99
|
+
while (($line = $reader.ReadLine()) -ne $null) {
|
|
100
|
+
Add-Content -LiteralPath $Path -Value $line -Encoding utf8
|
|
101
|
+
}
|
|
102
|
+
EOF
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
session_exists() {
|
|
106
|
+
"$PSMUX_BIN" has-session -t "$SESSION_NAME" >/dev/null 2>&1
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
resolve_pane_target() {
|
|
110
|
+
local pane_name="$1"
|
|
111
|
+
local pane_index
|
|
112
|
+
|
|
113
|
+
pane_index="$("$PSMUX_BIN" list-panes -t "$(session_target)" -F '#{pane_index} #{pane_title}' \
|
|
114
|
+
| awk -v wanted="$pane_name" '$2 == wanted { print $1; exit }')"
|
|
115
|
+
|
|
116
|
+
[[ -n "$pane_index" ]] || return 1
|
|
117
|
+
pane_target_from_index "$pane_index"
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
require_pane_target() {
|
|
121
|
+
local pane_name="$1"
|
|
122
|
+
local pane_target
|
|
123
|
+
|
|
124
|
+
pane_target="$(resolve_pane_target "$pane_name")"
|
|
125
|
+
[[ -n "$pane_target" ]] || die "Pane '$pane_name' not found in session '$SESSION_NAME'."
|
|
126
|
+
printf '%s\n' "$pane_target"
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
set_pane_title() {
|
|
130
|
+
local pane_target="$1"
|
|
131
|
+
local pane_name="$2"
|
|
132
|
+
|
|
133
|
+
"$PSMUX_BIN" select-pane -t "$pane_target" -T "$pane_name" >/dev/null
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
start_capture_for_pane() {
|
|
137
|
+
local pane_name="$1"
|
|
138
|
+
local pane_target log_file helper_windows_path log_windows_path
|
|
139
|
+
|
|
140
|
+
pane_target="$(require_pane_target "$pane_name")"
|
|
141
|
+
log_file="$(log_file_for "$pane_name")"
|
|
142
|
+
ensure_capture_helper
|
|
143
|
+
helper_windows_path="$(to_windows_path "$CAPTURE_HELPER_PATH")"
|
|
144
|
+
log_windows_path="$(to_windows_path "$log_file")"
|
|
145
|
+
|
|
146
|
+
mkdir -p "$CAPTURE_DIR"
|
|
147
|
+
: >"$log_file"
|
|
148
|
+
|
|
149
|
+
"$PSMUX_BIN" pipe-pane -t "$pane_target" >/dev/null 2>&1 || true
|
|
150
|
+
"$PSMUX_BIN" pipe-pane -t "$pane_target" powershell.exe -NoLogo -NoProfile -File "$helper_windows_path" "$log_windows_path" >/dev/null
|
|
151
|
+
refresh_snapshot_for_pane "$pane_name"
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
start_capture_for_all_panes() {
|
|
155
|
+
start_capture_for_pane "$PANE_LEAD"
|
|
156
|
+
start_capture_for_pane "$PANE_CODEX"
|
|
157
|
+
start_capture_for_pane "$PANE_GEMINI"
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
stop_capture_for_pane() {
|
|
161
|
+
local pane_name="$1"
|
|
162
|
+
local pane_target
|
|
163
|
+
|
|
164
|
+
pane_target="$(resolve_pane_target "$pane_name" || true)"
|
|
165
|
+
[[ -n "$pane_target" ]] || return 0
|
|
166
|
+
"$PSMUX_BIN" pipe-pane -t "$pane_target" >/dev/null 2>&1 || true
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
refresh_snapshot_for_pane() {
|
|
170
|
+
local pane_name="$1"
|
|
171
|
+
local pane_target log_file
|
|
172
|
+
|
|
173
|
+
pane_target="$(require_pane_target "$pane_name")"
|
|
174
|
+
log_file="$(log_file_for "$pane_name")"
|
|
175
|
+
mkdir -p "$CAPTURE_DIR"
|
|
176
|
+
|
|
177
|
+
# Detached Windows sessions may not flush pipe-pane reliably yet.
|
|
178
|
+
# Overwriting the log with a fresh capture-pane snapshot keeps
|
|
179
|
+
# completion detection deterministic for the prototype.
|
|
180
|
+
"$PSMUX_BIN" capture-pane -t "$pane_target" -p >"$log_file"
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
send_keys_to_pane() {
|
|
184
|
+
local pane_name="$1"
|
|
185
|
+
local text="$2"
|
|
186
|
+
local submit="${3:-1}"
|
|
187
|
+
local pane_target
|
|
188
|
+
|
|
189
|
+
pane_target="$(require_pane_target "$pane_name")"
|
|
190
|
+
"$PSMUX_BIN" send-keys -t "$pane_target" -l "$text"
|
|
191
|
+
if [[ "$submit" != "0" ]]; then
|
|
192
|
+
"$PSMUX_BIN" send-keys -t "$pane_target" C-m
|
|
193
|
+
fi
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
dispatch_powershell_command() {
|
|
197
|
+
local pane_name="$1"
|
|
198
|
+
local command_text="$2"
|
|
199
|
+
local token wrapped
|
|
200
|
+
|
|
201
|
+
token="${pane_name}-$(date +%s)-$RANDOM"
|
|
202
|
+
wrapped="${command_text}; \$trifluxExit = if (\$null -ne \$LASTEXITCODE) { [int]\$LASTEXITCODE } else { 0 }; Write-Output \"${COMPLETION_PREFIX}${token}:\$trifluxExit\""
|
|
203
|
+
|
|
204
|
+
send_keys_to_pane "$pane_name" "$wrapped" 1
|
|
205
|
+
printf '%s\n' "$token"
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
wait_for_pattern() {
|
|
209
|
+
local pane_name="$1"
|
|
210
|
+
local pattern="$2"
|
|
211
|
+
local timeout_sec="${3:-300}"
|
|
212
|
+
local log_file deadline
|
|
213
|
+
|
|
214
|
+
log_file="$(log_file_for "$pane_name")"
|
|
215
|
+
[[ -f "$log_file" ]] || die "Log file for pane '$pane_name' does not exist. Start capture first."
|
|
216
|
+
|
|
217
|
+
deadline=$((SECONDS + timeout_sec))
|
|
218
|
+
while (( SECONDS <= deadline )); do
|
|
219
|
+
refresh_snapshot_for_pane "$pane_name"
|
|
220
|
+
if grep -Eq -- "$pattern" "$log_file"; then
|
|
221
|
+
return 0
|
|
222
|
+
fi
|
|
223
|
+
sleep "$POLL_INTERVAL_SEC"
|
|
224
|
+
done
|
|
225
|
+
|
|
226
|
+
return 1
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
wait_for_completion_token() {
|
|
230
|
+
local pane_name="$1"
|
|
231
|
+
local token="$2"
|
|
232
|
+
local timeout_sec="${3:-300}"
|
|
233
|
+
local pattern
|
|
234
|
+
|
|
235
|
+
pattern="${COMPLETION_PREFIX}${token}:[0-9]+"
|
|
236
|
+
wait_for_pattern "$pane_name" "$pattern" "$timeout_sec"
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
print_log_locations() {
|
|
240
|
+
mkdir -p "$CAPTURE_DIR"
|
|
241
|
+
printf '%s\t%s\n' "$PANE_LEAD" "$(log_file_for "$PANE_LEAD")"
|
|
242
|
+
printf '%s\t%s\n' "$PANE_CODEX" "$(log_file_for "$PANE_CODEX")"
|
|
243
|
+
printf '%s\t%s\n' "$PANE_GEMINI" "$(log_file_for "$PANE_GEMINI")"
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
create_session_layout() {
|
|
247
|
+
local lead_index codex_index gemini_index
|
|
248
|
+
|
|
249
|
+
require_psmux
|
|
250
|
+
|
|
251
|
+
if session_exists; then
|
|
252
|
+
die "Session '$SESSION_NAME' already exists. Run cleanup first or set PSMUX_SESSION_NAME."
|
|
253
|
+
fi
|
|
254
|
+
|
|
255
|
+
mkdir -p "$CAPTURE_DIR"
|
|
256
|
+
|
|
257
|
+
lead_index="$("$PSMUX_BIN" new-session -d -P -F '#{pane_index}' -s "$SESSION_NAME" -n "$WINDOW_NAME" -- $SHELL_COMMAND)"
|
|
258
|
+
codex_index="$("$PSMUX_BIN" split-window -h -P -F '#{pane_index}' -t "$(session_target)" -- $SHELL_COMMAND)"
|
|
259
|
+
gemini_index="$("$PSMUX_BIN" split-window -v -P -F '#{pane_index}' -t "$(pane_target_from_index "$codex_index")" -- $SHELL_COMMAND)"
|
|
260
|
+
|
|
261
|
+
set_pane_title "$(pane_target_from_index "$lead_index")" "$PANE_LEAD"
|
|
262
|
+
set_pane_title "$(pane_target_from_index "$codex_index")" "$PANE_CODEX"
|
|
263
|
+
set_pane_title "$(pane_target_from_index "$gemini_index")" "$PANE_GEMINI"
|
|
264
|
+
|
|
265
|
+
"$PSMUX_BIN" select-layout -t "$(session_target)" tiled >/dev/null
|
|
266
|
+
"$PSMUX_BIN" select-pane -t "$(pane_target_from_index "$lead_index")" >/dev/null
|
|
267
|
+
|
|
268
|
+
start_capture_for_all_panes
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
show_start_summary() {
|
|
272
|
+
log "Session created: $SESSION_NAME"
|
|
273
|
+
log "Window: $WINDOW_NAME"
|
|
274
|
+
log "Attach with: $PSMUX_BIN attach -t $SESSION_NAME"
|
|
275
|
+
print_log_locations
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
run_demo() {
|
|
279
|
+
local lead_token codex_token gemini_token
|
|
280
|
+
|
|
281
|
+
create_session_layout
|
|
282
|
+
|
|
283
|
+
lead_token="$(dispatch_powershell_command "$PANE_LEAD" 'Write-Host "lead pane ready"')"
|
|
284
|
+
codex_token="$(dispatch_powershell_command "$PANE_CODEX" 'Write-Host "codex-worker pane ready"')"
|
|
285
|
+
gemini_token="$(dispatch_powershell_command "$PANE_GEMINI" 'Write-Host "gemini-worker pane ready"')"
|
|
286
|
+
|
|
287
|
+
wait_for_completion_token "$PANE_LEAD" "$lead_token" 30 || die "Lead pane demo command timed out."
|
|
288
|
+
wait_for_completion_token "$PANE_CODEX" "$codex_token" 30 || die "Codex pane demo command timed out."
|
|
289
|
+
wait_for_completion_token "$PANE_GEMINI" "$gemini_token" 30 || die "Gemini pane demo command timed out."
|
|
290
|
+
|
|
291
|
+
show_start_summary
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
cleanup() {
|
|
295
|
+
stop_capture_for_pane "$PANE_LEAD"
|
|
296
|
+
stop_capture_for_pane "$PANE_CODEX"
|
|
297
|
+
stop_capture_for_pane "$PANE_GEMINI"
|
|
298
|
+
|
|
299
|
+
if session_exists; then
|
|
300
|
+
"$PSMUX_BIN" kill-session -t "$SESSION_NAME" >/dev/null 2>&1 || true
|
|
301
|
+
fi
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
main() {
|
|
305
|
+
local action="${1:-demo}"
|
|
306
|
+
|
|
307
|
+
case "$action" in
|
|
308
|
+
start)
|
|
309
|
+
create_session_layout
|
|
310
|
+
show_start_summary
|
|
311
|
+
;;
|
|
312
|
+
demo)
|
|
313
|
+
run_demo
|
|
314
|
+
;;
|
|
315
|
+
attach)
|
|
316
|
+
require_psmux
|
|
317
|
+
"$PSMUX_BIN" attach -t "$SESSION_NAME"
|
|
318
|
+
;;
|
|
319
|
+
send)
|
|
320
|
+
[[ $# -ge 3 ]] || die "Usage: $0 send <pane-name> <command text>"
|
|
321
|
+
shift
|
|
322
|
+
local pane_name="$1"
|
|
323
|
+
shift
|
|
324
|
+
send_keys_to_pane "$pane_name" "$*" 1
|
|
325
|
+
;;
|
|
326
|
+
send-no-enter)
|
|
327
|
+
[[ $# -ge 3 ]] || die "Usage: $0 send-no-enter <pane-name> <text>"
|
|
328
|
+
shift
|
|
329
|
+
local pane_name="$1"
|
|
330
|
+
shift
|
|
331
|
+
send_keys_to_pane "$pane_name" "$*" 0
|
|
332
|
+
;;
|
|
333
|
+
steer-ps)
|
|
334
|
+
[[ $# -ge 3 ]] || die "Usage: $0 steer-ps <pane-name> <powershell command>"
|
|
335
|
+
shift
|
|
336
|
+
local pane_name="$1"
|
|
337
|
+
shift
|
|
338
|
+
dispatch_powershell_command "$pane_name" "$*"
|
|
339
|
+
;;
|
|
340
|
+
wait)
|
|
341
|
+
[[ $# -ge 3 ]] || die "Usage: $0 wait <pane-name> <regex> [timeout-sec]"
|
|
342
|
+
shift
|
|
343
|
+
local pane_name="$1"
|
|
344
|
+
local pattern="$2"
|
|
345
|
+
local timeout_sec="${3:-300}"
|
|
346
|
+
if wait_for_pattern "$pane_name" "$pattern" "$timeout_sec"; then
|
|
347
|
+
log "Matched pattern for pane '$pane_name': $pattern"
|
|
348
|
+
else
|
|
349
|
+
die "Timed out waiting for pane '$pane_name' pattern: $pattern"
|
|
350
|
+
fi
|
|
351
|
+
;;
|
|
352
|
+
logs)
|
|
353
|
+
print_log_locations
|
|
354
|
+
;;
|
|
355
|
+
cleanup)
|
|
356
|
+
cleanup
|
|
357
|
+
;;
|
|
358
|
+
-h|--help|help)
|
|
359
|
+
usage
|
|
360
|
+
;;
|
|
361
|
+
*)
|
|
362
|
+
usage
|
|
363
|
+
die "Unknown action: $action"
|
|
364
|
+
;;
|
|
365
|
+
esac
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
main "$@"
|
package/scripts/run.cjs
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const { execFileSync } = require("child_process");
|
|
5
|
+
const { existsSync, readFileSync } = require("fs");
|
|
6
|
+
const { dirname, isAbsolute, join, resolve } = require("path");
|
|
7
|
+
|
|
8
|
+
function resolvePluginRoot() {
|
|
9
|
+
if (process.env.CLAUDE_PLUGIN_ROOT) return process.env.CLAUDE_PLUGIN_ROOT;
|
|
10
|
+
return dirname(__dirname);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function resolveTargetPath(rawTarget) {
|
|
14
|
+
if (!rawTarget || typeof rawTarget !== "string") return null;
|
|
15
|
+
|
|
16
|
+
const pluginRoot = resolvePluginRoot();
|
|
17
|
+
const trimmed = rawTarget.trim();
|
|
18
|
+
|
|
19
|
+
if (trimmed.startsWith("${CLAUDE_PLUGIN_ROOT}/")) {
|
|
20
|
+
return join(pluginRoot, trimmed.replace("${CLAUDE_PLUGIN_ROOT}/", ""));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (trimmed.startsWith("/scripts/")) {
|
|
24
|
+
return join(pluginRoot, trimmed.replace(/^\/+/, ""));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (isAbsolute(trimmed)) return trimmed;
|
|
28
|
+
return resolve(process.cwd(), trimmed);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const targetArg = process.argv[2];
|
|
32
|
+
if (!targetArg) {
|
|
33
|
+
process.exit(0);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const targetPath = resolveTargetPath(targetArg);
|
|
37
|
+
if (!targetPath || !existsSync(targetPath)) {
|
|
38
|
+
process.exit(0);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const stdinBuffer = (() => {
|
|
42
|
+
try {
|
|
43
|
+
return readFileSync(0);
|
|
44
|
+
} catch {
|
|
45
|
+
return Buffer.alloc(0);
|
|
46
|
+
}
|
|
47
|
+
})();
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
execFileSync(process.execPath, [targetPath, ...process.argv.slice(3)], {
|
|
51
|
+
env: process.env,
|
|
52
|
+
stdio: ["pipe", "inherit", "inherit"],
|
|
53
|
+
input: stdinBuffer,
|
|
54
|
+
windowsHide: true
|
|
55
|
+
});
|
|
56
|
+
process.exit(0);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (typeof error?.status === "number") {
|
|
59
|
+
process.exit(error.status);
|
|
60
|
+
}
|
|
61
|
+
process.exit(0);
|
|
62
|
+
}
|
package/scripts/setup.mjs
CHANGED
|
@@ -7,9 +7,30 @@
|
|
|
7
7
|
import { copyFileSync, mkdirSync, readFileSync, writeFileSync, readdirSync, existsSync, chmodSync, unlinkSync } from "fs";
|
|
8
8
|
import { join, dirname } from "path";
|
|
9
9
|
import { homedir } from "os";
|
|
10
|
+
import { spawn } from "child_process";
|
|
11
|
+
import { fileURLToPath } from "url";
|
|
10
12
|
|
|
11
|
-
const PLUGIN_ROOT = dirname(dirname(
|
|
13
|
+
const PLUGIN_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
12
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
|
+
const REQUIRED_CODEX_PROFILES = [
|
|
19
|
+
{
|
|
20
|
+
name: "xhigh",
|
|
21
|
+
lines: [
|
|
22
|
+
'model = "gpt-5.3-codex"',
|
|
23
|
+
'model_reasoning_effort = "xhigh"',
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: "spark_fast",
|
|
28
|
+
lines: [
|
|
29
|
+
'model = "gpt-5.1-codex-mini"',
|
|
30
|
+
'model_reasoning_effort = "low"',
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
];
|
|
13
34
|
|
|
14
35
|
// ── 파일 동기화 ──
|
|
15
36
|
|
|
@@ -24,6 +45,36 @@ const SYNC_MAP = [
|
|
|
24
45
|
dst: join(CLAUDE_DIR, "scripts", "tfx-route-post.mjs"),
|
|
25
46
|
label: "tfx-route-post.mjs",
|
|
26
47
|
},
|
|
48
|
+
{
|
|
49
|
+
src: join(PLUGIN_ROOT, "scripts", "tfx-route-worker.mjs"),
|
|
50
|
+
dst: join(CLAUDE_DIR, "scripts", "tfx-route-worker.mjs"),
|
|
51
|
+
label: "tfx-route-worker.mjs",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
src: join(PLUGIN_ROOT, "hub", "workers", "codex-mcp.mjs"),
|
|
55
|
+
dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "codex-mcp.mjs"),
|
|
56
|
+
label: "hub/workers/codex-mcp.mjs",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
src: join(PLUGIN_ROOT, "hub", "workers", "interface.mjs"),
|
|
60
|
+
dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "interface.mjs"),
|
|
61
|
+
label: "hub/workers/interface.mjs",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
src: join(PLUGIN_ROOT, "hub", "workers", "gemini-worker.mjs"),
|
|
65
|
+
dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "gemini-worker.mjs"),
|
|
66
|
+
label: "hub/workers/gemini-worker.mjs",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
src: join(PLUGIN_ROOT, "hub", "workers", "claude-worker.mjs"),
|
|
70
|
+
dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "claude-worker.mjs"),
|
|
71
|
+
label: "hub/workers/claude-worker.mjs",
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
src: join(PLUGIN_ROOT, "hub", "workers", "factory.mjs"),
|
|
75
|
+
dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "factory.mjs"),
|
|
76
|
+
label: "hub/workers/factory.mjs",
|
|
77
|
+
},
|
|
27
78
|
{
|
|
28
79
|
src: join(PLUGIN_ROOT, "hud", "hud-qos-status.mjs"),
|
|
29
80
|
dst: join(CLAUDE_DIR, "hud", "hud-qos-status.mjs"),
|
|
@@ -41,6 +92,54 @@ function getVersion(filePath) {
|
|
|
41
92
|
}
|
|
42
93
|
}
|
|
43
94
|
|
|
95
|
+
function shouldSyncTextFile(src, dst) {
|
|
96
|
+
if (!existsSync(dst)) return true;
|
|
97
|
+
try {
|
|
98
|
+
return readFileSync(src, "utf8") !== readFileSync(dst, "utf8");
|
|
99
|
+
} catch {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function escapeRegExp(value) {
|
|
105
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function hasProfileSection(tomlContent, profileName) {
|
|
109
|
+
const section = `^\\[profiles\\.${escapeRegExp(profileName)}\\]\\s*$`;
|
|
110
|
+
return new RegExp(section, "m").test(tomlContent);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function ensureCodexProfiles() {
|
|
114
|
+
try {
|
|
115
|
+
if (!existsSync(CODEX_DIR)) mkdirSync(CODEX_DIR, { recursive: true });
|
|
116
|
+
|
|
117
|
+
const original = existsSync(CODEX_CONFIG_PATH)
|
|
118
|
+
? readFileSync(CODEX_CONFIG_PATH, "utf8")
|
|
119
|
+
: "";
|
|
120
|
+
|
|
121
|
+
let updated = original;
|
|
122
|
+
let added = 0;
|
|
123
|
+
|
|
124
|
+
for (const profile of REQUIRED_CODEX_PROFILES) {
|
|
125
|
+
if (hasProfileSection(updated, profile.name)) continue;
|
|
126
|
+
|
|
127
|
+
if (updated.length > 0 && !updated.endsWith("\n")) updated += "\n";
|
|
128
|
+
if (updated.trim().length > 0) updated += "\n";
|
|
129
|
+
updated += `[profiles.${profile.name}]\n${profile.lines.join("\n")}\n`;
|
|
130
|
+
added++;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (added > 0) {
|
|
134
|
+
writeFileSync(CODEX_CONFIG_PATH, updated, "utf8");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return added;
|
|
138
|
+
} catch {
|
|
139
|
+
return 0;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
44
143
|
let synced = 0;
|
|
45
144
|
|
|
46
145
|
for (const { src, dst, label } of SYNC_MAP) {
|
|
@@ -56,9 +155,7 @@ for (const { src, dst, label } of SYNC_MAP) {
|
|
|
56
155
|
try { chmodSync(dst, 0o755); } catch {}
|
|
57
156
|
synced++;
|
|
58
157
|
} else {
|
|
59
|
-
|
|
60
|
-
const dstVersion = getVersion(dst);
|
|
61
|
-
if (srcVersion && dstVersion && srcVersion !== dstVersion) {
|
|
158
|
+
if (shouldSyncTextFile(src, dst)) {
|
|
62
159
|
copyFileSync(src, dst);
|
|
63
160
|
try { chmodSync(dst, 0o755); } catch {}
|
|
64
161
|
synced++;
|
|
@@ -66,6 +163,34 @@ for (const { src, dst, label } of SYNC_MAP) {
|
|
|
66
163
|
}
|
|
67
164
|
}
|
|
68
165
|
|
|
166
|
+
// ── Worker 의존성 동기화 (MCP SDK + transitive deps) ──
|
|
167
|
+
|
|
168
|
+
const workerNodeModules = join(CLAUDE_DIR, "scripts", "node_modules");
|
|
169
|
+
const mcpSdkPath = join(workerNodeModules, "@modelcontextprotocol", "sdk");
|
|
170
|
+
const srcNodeModules = join(PLUGIN_ROOT, "node_modules");
|
|
171
|
+
|
|
172
|
+
// native 모듈은 제외 (플랫폼 의존적, worker에서 불필요)
|
|
173
|
+
const SKIP_PACKAGES = new Set(["better-sqlite3", "prebuild-install", "node-abi", "node-addon-api"]);
|
|
174
|
+
|
|
175
|
+
if (!existsSync(mcpSdkPath) && existsSync(srcNodeModules)) {
|
|
176
|
+
try {
|
|
177
|
+
const { cpSync } = await import("fs");
|
|
178
|
+
for (const entry of readdirSync(srcNodeModules)) {
|
|
179
|
+
if (SKIP_PACKAGES.has(entry)) continue;
|
|
180
|
+
|
|
181
|
+
const src = join(srcNodeModules, entry);
|
|
182
|
+
const dst = join(workerNodeModules, entry);
|
|
183
|
+
if (existsSync(dst)) continue;
|
|
184
|
+
|
|
185
|
+
mkdirSync(dirname(dst), { recursive: true });
|
|
186
|
+
cpSync(src, dst, { recursive: true });
|
|
187
|
+
}
|
|
188
|
+
synced++;
|
|
189
|
+
} catch {
|
|
190
|
+
// best effort: 의존성 복사 실패 시 exec fallback으로 동작
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
69
194
|
// ── 스킬 동기화 ──
|
|
70
195
|
|
|
71
196
|
const skillsSrc = join(PLUGIN_ROOT, "skills");
|
|
@@ -130,6 +255,36 @@ if (existsSync(hudPath)) {
|
|
|
130
255
|
}
|
|
131
256
|
}
|
|
132
257
|
|
|
258
|
+
// ── Agent Teams 환경변수 자동 설정 ──
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
let agentSettings = {};
|
|
262
|
+
if (existsSync(settingsPath)) {
|
|
263
|
+
agentSettings = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (!agentSettings.env) agentSettings.env = {};
|
|
267
|
+
let agentSettingsChanged = false;
|
|
268
|
+
|
|
269
|
+
if (agentSettings.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS !== "1") {
|
|
270
|
+
agentSettings.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = "1";
|
|
271
|
+
agentSettingsChanged = true;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// teammateMode: auto (tmux 밖이면 in-process, 안이면 split-pane)
|
|
275
|
+
if (!agentSettings.teammateMode) {
|
|
276
|
+
agentSettings.teammateMode = "auto";
|
|
277
|
+
agentSettingsChanged = true;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (agentSettingsChanged) {
|
|
281
|
+
writeFileSync(settingsPath, JSON.stringify(agentSettings, null, 2) + "\n", "utf8");
|
|
282
|
+
synced++;
|
|
283
|
+
}
|
|
284
|
+
} catch {
|
|
285
|
+
// settings.json 파싱 실패 시 무시 — 기존 설정 보존
|
|
286
|
+
}
|
|
287
|
+
|
|
133
288
|
// ── Stale PID 파일 정리 (hub 좀비 방지) ──
|
|
134
289
|
|
|
135
290
|
const HUB_PID_FILE = join(CLAUDE_DIR, "cache", "tfx-hub", "hub.pid");
|
|
@@ -196,9 +351,14 @@ if (process.platform === "win32") {
|
|
|
196
351
|
}
|
|
197
352
|
}
|
|
198
353
|
|
|
199
|
-
// ──
|
|
354
|
+
// ── Codex 프로필 자동 보정 ──
|
|
200
355
|
|
|
201
|
-
|
|
356
|
+
const codexProfilesAdded = ensureCodexProfiles();
|
|
357
|
+
if (codexProfilesAdded > 0) {
|
|
358
|
+
synced++;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ── MCP 인벤토리 백그라운드 갱신 ──
|
|
202
362
|
|
|
203
363
|
const mcpCheck = join(PLUGIN_ROOT, "scripts", "mcp-check.mjs");
|
|
204
364
|
if (existsSync(mcpCheck)) {
|
|
@@ -209,6 +369,26 @@ if (existsSync(mcpCheck)) {
|
|
|
209
369
|
child.unref(); // 부모 프로세스와 분리 — 비동기 실행
|
|
210
370
|
}
|
|
211
371
|
|
|
372
|
+
// ── Hub 헬스체크 + 자동 기동 (세션 시작 백그라운드) ──
|
|
373
|
+
// setup 훅이 포그라운드 지연을 만들지 않도록 별도 detached 프로세스로 처리한다.
|
|
374
|
+
const hubEnsure = join(PLUGIN_ROOT, "scripts", "hub-ensure.mjs");
|
|
375
|
+
const isPostinstall = process.env.npm_lifecycle_event === "postinstall";
|
|
376
|
+
const isCi = /^(1|true)$/i.test(process.env.CI || "");
|
|
377
|
+
const disableHubAutostart = process.env.TFX_DISABLE_HUB_AUTOSTART === "1";
|
|
378
|
+
|
|
379
|
+
if (!isPostinstall && !isCi && !disableHubAutostart && existsSync(hubEnsure)) {
|
|
380
|
+
try {
|
|
381
|
+
const child = spawn(process.execPath, [hubEnsure], {
|
|
382
|
+
env: process.env,
|
|
383
|
+
detached: true,
|
|
384
|
+
stdio: "ignore",
|
|
385
|
+
});
|
|
386
|
+
child.unref();
|
|
387
|
+
} catch {
|
|
388
|
+
// best effort: 실패해도 setup 흐름은 지속
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
212
392
|
// ── postinstall 배너 (npm install 시에만 출력) ──
|
|
213
393
|
|
|
214
394
|
if (process.env.npm_lifecycle_event === "postinstall") {
|
|
@@ -239,7 +419,8 @@ ${B}Commands:${R}
|
|
|
239
419
|
${C}triflux${R} setup 파일 동기화 + HUD 설정
|
|
240
420
|
${C}triflux${R} doctor CLI 진단 (Codex/Gemini 확인)
|
|
241
421
|
${C}triflux${R} list 설치된 스킬 목록
|
|
242
|
-
${C}triflux${R} update 최신 버전으로 업데이트
|
|
422
|
+
${C}triflux${R} update 최신 안정 버전으로 업데이트
|
|
423
|
+
${C}triflux${R} update --dev dev 채널로 업데이트 (${D}dev 별칭 지원${R})
|
|
243
424
|
|
|
244
425
|
${B}Shortcuts:${R}
|
|
245
426
|
${C}tfx${R} triflux 축약
|
|
@@ -248,6 +429,7 @@ ${B}Shortcuts:${R}
|
|
|
248
429
|
|
|
249
430
|
${B}Skills (Claude Code):${R}
|
|
250
431
|
${C}/tfx-auto${R} "작업" 자동 분류 + 병렬 실행
|
|
432
|
+
${C}/tfx-auto-codex${R} "작업" Codex 리드 + Gemini 유지
|
|
251
433
|
${C}/tfx-codex${R} "작업" Codex 전용 모드
|
|
252
434
|
${C}/tfx-gemini${R} "작업" Gemini 전용 모드
|
|
253
435
|
${C}/tfx-setup${R} HUD 설정 + 진단
|