triflux 3.2.0-dev.7 → 3.2.0-dev.9

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 (42) hide show
  1. package/bin/triflux.mjs +557 -251
  2. package/hooks/keyword-rules.json +16 -0
  3. package/hub/bridge.mjs +410 -318
  4. package/hub/hitl.mjs +45 -31
  5. package/hub/pipe.mjs +457 -0
  6. package/hub/router.mjs +422 -161
  7. package/hub/server.mjs +429 -424
  8. package/hub/store.mjs +388 -314
  9. package/hub/team/cli-team-common.mjs +348 -0
  10. package/hub/team/cli-team-control.mjs +393 -0
  11. package/hub/team/cli-team-start.mjs +512 -0
  12. package/hub/team/cli-team-status.mjs +269 -0
  13. package/hub/team/cli.mjs +59 -1459
  14. package/hub/team/dashboard.mjs +1 -9
  15. package/hub/team/native.mjs +12 -80
  16. package/hub/team/nativeProxy.mjs +121 -47
  17. package/hub/team/pane.mjs +66 -43
  18. package/hub/team/psmux.mjs +297 -0
  19. package/hub/team/session.mjs +354 -291
  20. package/hub/team/shared.mjs +13 -0
  21. package/hub/team/staleState.mjs +299 -0
  22. package/hub/tools.mjs +41 -52
  23. package/hub/workers/claude-worker.mjs +446 -0
  24. package/hub/workers/codex-mcp.mjs +414 -0
  25. package/hub/workers/factory.mjs +18 -0
  26. package/hub/workers/gemini-worker.mjs +349 -0
  27. package/hub/workers/interface.mjs +41 -0
  28. package/hud/hud-qos-status.mjs +4 -2
  29. package/package.json +4 -1
  30. package/scripts/keyword-detector.mjs +15 -0
  31. package/scripts/lib/keyword-rules.mjs +4 -1
  32. package/scripts/psmux-steering-prototype.sh +368 -0
  33. package/scripts/setup.mjs +128 -70
  34. package/scripts/tfx-route-worker.mjs +161 -0
  35. package/scripts/tfx-route.sh +415 -80
  36. package/skills/tfx-auto/SKILL.md +90 -564
  37. package/skills/tfx-auto-codex/SKILL.md +1 -3
  38. package/skills/tfx-codex/SKILL.md +1 -4
  39. package/skills/tfx-doctor/SKILL.md +1 -0
  40. package/skills/tfx-gemini/SKILL.md +1 -4
  41. package/skills/tfx-setup/SKILL.md +1 -4
  42. package/skills/tfx-team/SKILL.md +53 -62
@@ -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/setup.mjs CHANGED
@@ -4,13 +4,13 @@
4
4
  // - hud-qos-status.mjs를 ~/.claude/hud/에 동기화
5
5
  // - skills/를 ~/.claude/skills/에 동기화
6
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 } from "child_process";
11
- import { fileURLToPath } from "url";
12
-
13
- const PLUGIN_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
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 } from "child_process";
11
+ import { fileURLToPath } from "url";
12
+
13
+ const PLUGIN_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
14
14
  const CLAUDE_DIR = join(homedir(), ".claude");
15
15
  const CODEX_DIR = join(homedir(), ".codex");
16
16
  const CODEX_CONFIG_PATH = join(CODEX_DIR, "config.toml");
@@ -45,6 +45,36 @@ const SYNC_MAP = [
45
45
  dst: join(CLAUDE_DIR, "scripts", "tfx-route-post.mjs"),
46
46
  label: "tfx-route-post.mjs",
47
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
+ },
48
78
  {
49
79
  src: join(PLUGIN_ROOT, "hud", "hud-qos-status.mjs"),
50
80
  dst: join(CLAUDE_DIR, "hud", "hud-qos-status.mjs"),
@@ -52,24 +82,24 @@ const SYNC_MAP = [
52
82
  },
53
83
  ];
54
84
 
55
- function getVersion(filePath) {
56
- try {
57
- const content = readFileSync(filePath, "utf8");
58
- const match = content.match(/VERSION\s*=\s*"([^"]+)"/);
59
- return match ? match[1] : null;
60
- } catch {
61
- return null;
62
- }
63
- }
64
-
65
- function shouldSyncTextFile(src, dst) {
66
- if (!existsSync(dst)) return true;
67
- try {
68
- return readFileSync(src, "utf8") !== readFileSync(dst, "utf8");
69
- } catch {
70
- return true;
71
- }
72
- }
85
+ function getVersion(filePath) {
86
+ try {
87
+ const content = readFileSync(filePath, "utf8");
88
+ const match = content.match(/VERSION\s*=\s*"([^"]+)"/);
89
+ return match ? match[1] : null;
90
+ } catch {
91
+ return null;
92
+ }
93
+ }
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
+ }
73
103
 
74
104
  function escapeRegExp(value) {
75
105
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -124,13 +154,41 @@ for (const { src, dst, label } of SYNC_MAP) {
124
154
  copyFileSync(src, dst);
125
155
  try { chmodSync(dst, 0o755); } catch {}
126
156
  synced++;
127
- } else {
128
- if (shouldSyncTextFile(src, dst)) {
129
- copyFileSync(src, dst);
130
- try { chmodSync(dst, 0o755); } catch {}
131
- synced++;
132
- }
133
- }
157
+ } else {
158
+ if (shouldSyncTextFile(src, dst)) {
159
+ copyFileSync(src, dst);
160
+ try { chmodSync(dst, 0o755); } catch {}
161
+ synced++;
162
+ }
163
+ }
164
+ }
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
+ }
134
192
  }
135
193
 
136
194
  // ── 스킬 동기화 ──
@@ -300,36 +358,36 @@ if (codexProfilesAdded > 0) {
300
358
  synced++;
301
359
  }
302
360
 
303
- // ── MCP 인벤토리 백그라운드 갱신 ──
304
-
305
- const mcpCheck = join(PLUGIN_ROOT, "scripts", "mcp-check.mjs");
306
- if (existsSync(mcpCheck)) {
307
- const child = spawn(process.execPath, [mcpCheck], {
361
+ // ── MCP 인벤토리 백그라운드 갱신 ──
362
+
363
+ const mcpCheck = join(PLUGIN_ROOT, "scripts", "mcp-check.mjs");
364
+ if (existsSync(mcpCheck)) {
365
+ const child = spawn(process.execPath, [mcpCheck], {
308
366
  detached: true,
309
367
  stdio: "ignore",
310
368
  });
311
- child.unref(); // 부모 프로세스와 분리 — 비동기 실행
312
- }
313
-
314
- // ── Hub 헬스체크 + 자동 기동 (세션 시작 백그라운드) ──
315
- // setup 훅이 포그라운드 지연을 만들지 않도록 별도 detached 프로세스로 처리한다.
316
- const hubEnsure = join(PLUGIN_ROOT, "scripts", "hub-ensure.mjs");
317
- const isPostinstall = process.env.npm_lifecycle_event === "postinstall";
318
- const isCi = /^(1|true)$/i.test(process.env.CI || "");
319
- const disableHubAutostart = process.env.TFX_DISABLE_HUB_AUTOSTART === "1";
320
-
321
- if (!isPostinstall && !isCi && !disableHubAutostart && existsSync(hubEnsure)) {
322
- try {
323
- const child = spawn(process.execPath, [hubEnsure], {
324
- env: process.env,
325
- detached: true,
326
- stdio: "ignore",
327
- });
328
- child.unref();
329
- } catch {
330
- // best effort: 실패해도 setup 흐름은 지속
331
- }
332
- }
369
+ child.unref(); // 부모 프로세스와 분리 — 비동기 실행
370
+ }
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
+ }
333
391
 
334
392
  // ── postinstall 배너 (npm install 시에만 출력) ──
335
393
 
@@ -357,23 +415,23 @@ ${B}╚════════════════════════
357
415
  ${G}✓${R} ${synced > 0 ? synced + " files synced" : "all files up to date"}
358
416
  ${G}✓${R} HUD statusLine → settings.json
359
417
 
360
- ${B}Commands:${R}
361
- ${C}triflux${R} setup 파일 동기화 + HUD 설정
362
- ${C}triflux${R} doctor CLI 진단 (Codex/Gemini 확인)
363
- ${C}triflux${R} list 설치된 스킬 목록
364
- ${C}triflux${R} update 최신 안정 버전으로 업데이트
365
- ${C}triflux${R} update --dev dev 채널로 업데이트 (${D}dev 별칭 지원${R})
418
+ ${B}Commands:${R}
419
+ ${C}triflux${R} setup 파일 동기화 + HUD 설정
420
+ ${C}triflux${R} doctor CLI 진단 (Codex/Gemini 확인)
421
+ ${C}triflux${R} list 설치된 스킬 목록
422
+ ${C}triflux${R} update 최신 안정 버전으로 업데이트
423
+ ${C}triflux${R} update --dev dev 채널로 업데이트 (${D}dev 별칭 지원${R})
366
424
 
367
425
  ${B}Shortcuts:${R}
368
426
  ${C}tfx${R} triflux 축약
369
427
  ${C}tfx-setup${R} triflux setup
370
428
  ${C}tfx-doctor${R} triflux doctor
371
429
 
372
- ${B}Skills (Claude Code):${R}
373
- ${C}/tfx-auto${R} "작업" 자동 분류 + 병렬 실행
374
- ${C}/tfx-auto-codex${R} "작업" Codex 리드 + Gemini 유지
375
- ${C}/tfx-codex${R} "작업" Codex 전용 모드
376
- ${C}/tfx-gemini${R} "작업" Gemini 전용 모드
430
+ ${B}Skills (Claude Code):${R}
431
+ ${C}/tfx-auto${R} "작업" 자동 분류 + 병렬 실행
432
+ ${C}/tfx-auto-codex${R} "작업" Codex 리드 + Gemini 유지
433
+ ${C}/tfx-codex${R} "작업" Codex 전용 모드
434
+ ${C}/tfx-gemini${R} "작업" Gemini 전용 모드
377
435
  ${C}/tfx-setup${R} HUD 설정 + 진단
378
436
 
379
437
  ${Y}!${R} 세션 재시작 후 스킬이 활성화됩니다