triflux 10.18.0 → 10.18.2

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.md CHANGED
@@ -157,6 +157,7 @@ background로 실행한 headless 결과는 **반드시 task-notification 완료
157
157
  | `.claude/rules/tfx-autoplan-principles.md` | gstack autoplan의 6 decision principles, phase 우선순위, 충돌 해소 규칙 추출본 |
158
158
  | `.claude/rules/tfx-update-logic.md` | triflux / OMC / gstack / Codex / Gemini 업데이트 로직 |
159
159
  | `.claude/rules/tfx-stack-coexistence.md` | gstack / superpowers / triflux 공존 원칙, 레이어 분리, 의존 방향, 충돌 해소 |
160
+ | `.claude/rules/tfx-mirror-policy.md` | packages/ 3-layer mirror 정책 (core 단순 cp / remote import 변환 / triflux byte-identical), tests 제외 룰, drift 차단 |
160
161
 
161
162
  Claude Code는 `.claude/rules/*.md` 를 자동 로드한다. Codex CLI는 `@import` 미지원이므로 필요 시 `AGENTS.md` 를 독립 유지한다.
162
163
 
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2025 tellang
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2025 tellang
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/bin/triflux.mjs CHANGED
@@ -43,6 +43,12 @@ import {
43
43
  ensureTfxSection,
44
44
  getLatestRoutingTable,
45
45
  } from "../scripts/claudemd-sync.mjs";
46
+ import {
47
+ applyPluginRootHookFallbacks,
48
+ commandExists,
49
+ findPluginRootHookIssues,
50
+ inspectMacTimeoutDependency,
51
+ } from "../scripts/lib/doctor-env-checks.mjs";
46
52
  import { ensureGeminiProfiles } from "../scripts/lib/gemini-profiles.mjs";
47
53
  import { serializeHandoff } from "../scripts/lib/handoff.mjs";
48
54
  import {
@@ -2041,6 +2047,60 @@ async function cmdDoctor(options = {}) {
2041
2047
  for (const target of SYNC_MAP) {
2042
2048
  syncFile(target.src, target.dst, target.label);
2043
2049
  }
2050
+ const macTimeoutForFix = inspectMacTimeoutDependency();
2051
+ if (!macTimeoutForFix.ok) {
2052
+ if (commandExists("brew")) {
2053
+ if (process.stdin.isTTY && process.stdout.isTTY) {
2054
+ info("macOS GNU timeout 부재: coreutils 설치 가능");
2055
+ process.stdout.write(
2056
+ " brew install coreutils 실행할까요? [y/N] ",
2057
+ );
2058
+ let answer = "";
2059
+ try {
2060
+ const buf = Buffer.alloc(128);
2061
+ const n = readSync(0, buf, 0, 128);
2062
+ answer = buf.toString("utf8", 0, n).trim().toLowerCase();
2063
+ } catch {
2064
+ answer = "";
2065
+ }
2066
+ if (answer.startsWith("y")) {
2067
+ try {
2068
+ execFileSync("brew", ["install", "coreutils"], {
2069
+ stdio: "inherit",
2070
+ timeout: 600000,
2071
+ windowsHide: true,
2072
+ });
2073
+ report.actions.push({
2074
+ type: "install",
2075
+ name: "coreutils",
2076
+ status: "ok",
2077
+ });
2078
+ ok("coreutils 설치 완료");
2079
+ } catch (error) {
2080
+ report.actions.push({
2081
+ type: "install",
2082
+ name: "coreutils",
2083
+ status: "failed",
2084
+ message: error.message,
2085
+ });
2086
+ warn(
2087
+ `coreutils 설치 실패: ${renderErrorMessage(error.message)}`,
2088
+ );
2089
+ }
2090
+ } else {
2091
+ info("건너뜀: brew install coreutils");
2092
+ }
2093
+ } else {
2094
+ warn(
2095
+ "macOS GNU timeout 부재 — 비대화형 모드에서는 자동 설치하지 않습니다.",
2096
+ );
2097
+ info("수동 설치: brew install coreutils");
2098
+ }
2099
+ } else {
2100
+ warn("macOS GNU timeout 부재 — Homebrew를 찾지 못했습니다.");
2101
+ info("Homebrew 설치 후: brew install coreutils");
2102
+ }
2103
+ }
2044
2104
  {
2045
2105
  const claudeGuide = ensureGlobalClaudeRoutingSection(CLAUDE_DIR);
2046
2106
  if (
@@ -2197,6 +2257,34 @@ async function cmdDoctor(options = {}) {
2197
2257
  issues++;
2198
2258
  }
2199
2259
 
2260
+ // macOS GNU timeout/coreutils
2261
+ section("macOS timeout");
2262
+ const macTimeout = inspectMacTimeoutDependency();
2263
+ if (macTimeout.status === "skipped") {
2264
+ addDoctorCheck(report, {
2265
+ name: "macos-timeout",
2266
+ status: "skipped",
2267
+ platform: process.platform,
2268
+ });
2269
+ info("macOS 아님 — 건너뜀");
2270
+ } else if (macTimeout.ok) {
2271
+ addDoctorCheck(report, {
2272
+ name: "macos-timeout",
2273
+ status: "ok",
2274
+ provider: macTimeout.provider,
2275
+ });
2276
+ ok(`timeout provider: ${macTimeout.provider}`);
2277
+ } else {
2278
+ addDoctorCheck(report, {
2279
+ name: "macos-timeout",
2280
+ status: "missing",
2281
+ fix: macTimeout.fix,
2282
+ });
2283
+ warn("GNU timeout/gtimeout 미설치");
2284
+ info("수정: brew install coreutils");
2285
+ issues++;
2286
+ }
2287
+
2200
2288
  // 2. HUD
2201
2289
  section("HUD");
2202
2290
  const hud = join(CLAUDE_DIR, "hud", "hud-qos-status.mjs");
@@ -3813,6 +3901,55 @@ async function cmdDoctor(options = {}) {
3813
3901
  }
3814
3902
 
3815
3903
  if (settings) {
3904
+ let pluginRootHookIssues = findPluginRootHookIssues(settings);
3905
+ if (pluginRootHookIssues.length > 0 && fix) {
3906
+ const fallbackResult = applyPluginRootHookFallbacks(settings, {
3907
+ pluginRoot: PKG_ROOT,
3908
+ });
3909
+ if (fallbackResult.changed) {
3910
+ writeFileSync(
3911
+ settingsPath,
3912
+ JSON.stringify(settings, null, 2) + "\n",
3913
+ "utf8",
3914
+ );
3915
+ ok(
3916
+ `PLUGIN_ROOT fallback ${fallbackResult.count}개 hook command에 적용됨`,
3917
+ );
3918
+ try {
3919
+ settings = JSON.parse(readFileSync(settingsPath, "utf8"));
3920
+ pluginRootHookIssues = findPluginRootHookIssues(settings);
3921
+ } catch (error) {
3922
+ warn(`PLUGIN_ROOT fallback 재검증 실패: ${error.message}`);
3923
+ }
3924
+ }
3925
+ }
3926
+
3927
+ addDoctorCheck(report, {
3928
+ name: "hook-plugin-root",
3929
+ status:
3930
+ pluginRootHookIssues.length === 0 ? "ok" : "missing-fallback",
3931
+ count: pluginRootHookIssues.length,
3932
+ examples: pluginRootHookIssues.slice(0, 3).map((issue) => ({
3933
+ event: issue.event,
3934
+ reason: issue.reason,
3935
+ command: issue.command,
3936
+ })),
3937
+ ...(pluginRootHookIssues.length > 0
3938
+ ? { fix: "tfx doctor --fix 또는 tfx setup" }
3939
+ : {}),
3940
+ });
3941
+ if (pluginRootHookIssues.length === 0) {
3942
+ ok("PLUGIN_ROOT hook fallback 확인됨");
3943
+ } else {
3944
+ warn(
3945
+ `PLUGIN_ROOT fallback 없는 hook ${pluginRootHookIssues.length}개 감지`,
3946
+ );
3947
+ info(
3948
+ "영향: npm 단독 설치에서 /hooks/... 경로로 붕괴할 수 있습니다.",
3949
+ );
3950
+ issues += pluginRootHookIssues.length;
3951
+ }
3952
+
3816
3953
  let coverage = computeHookCoverage(settings, managedHooks);
3817
3954
 
3818
3955
  if (coverage.missing.length > 0 && fix) {
@@ -206,10 +206,11 @@
206
206
  {
207
207
  "id": "ext-session-vault-start",
208
208
  "source": "session-vault",
209
+ "requires": "$HOME/Desktop/Projects/tools/session-vault",
209
210
  "matcher": "*",
210
211
  "command": "bash \"${HOME}/Desktop/Projects/tools/session-vault/scripts/start_hook.sh\"",
211
212
  "priority": 100,
212
- "enabled": true,
213
+ "enabled": false,
213
214
  "timeout": 10,
214
215
  "blocking": false,
215
216
  "description": "세션 볼트 로깅 시작"
@@ -230,10 +231,11 @@
230
231
  {
231
232
  "id": "ext-session-vault-export",
232
233
  "source": "session-vault",
234
+ "requires": "$HOME/Desktop/Projects/tools/session-vault",
233
235
  "matcher": "*",
234
236
  "command": "bash \"${HOME}/Desktop/Projects/tools/session-vault/scripts/export_hook.sh\"",
235
237
  "priority": 100,
236
- "enabled": true,
238
+ "enabled": false,
237
239
  "timeout": 30,
238
240
  "blocking": false,
239
241
  "description": "세션 트랜스크립트 내보내기"
@@ -184,7 +184,10 @@ function buildLeaseSpawnEnv(lease) {
184
184
 
185
185
  function dirnameOf(filePath) {
186
186
  if (typeof filePath !== "string" || !filePath) return null;
187
- const lastSep = Math.max(filePath.lastIndexOf("/"), filePath.lastIndexOf("\\"));
187
+ const lastSep = Math.max(
188
+ filePath.lastIndexOf("/"),
189
+ filePath.lastIndexOf("\\"),
190
+ );
188
191
  if (lastSep < 0) return null;
189
192
  return filePath.slice(0, lastSep);
190
193
  }
@@ -2,7 +2,7 @@
2
2
  // Claude Usage API (api.anthropic.com/api/oauth/usage)
3
3
  // ============================================================================
4
4
 
5
- import { spawn } from "node:child_process";
5
+ import { execFileSync, spawn } from "node:child_process";
6
6
  import { existsSync, writeFileSync } from "node:fs";
7
7
  import https from "node:https";
8
8
  import {
@@ -31,6 +31,7 @@ import {
31
31
 
32
32
  export const CLAUDE_USAGE_POLL_BASE_MS = 5_000;
33
33
  export const CLAUDE_USAGE_POLL_JITTER_RATIO = 0.2;
34
+ const CLAUDE_KEYCHAIN_SERVICE = "Claude Code-credentials";
34
35
  export const CLAUDE_USAGE_RATE_LIMIT_BACKOFF_MS = [
35
36
  CLAUDE_USAGE_POLL_BASE_MS,
36
37
  10_000,
@@ -98,8 +99,7 @@ function getSnapshotSchedule(cache) {
98
99
  };
99
100
  }
100
101
 
101
- export function readClaudeCredentials() {
102
- const data = readJson(CLAUDE_CREDENTIALS_PATH, null);
102
+ function normalizeClaudeCredentials(data, source, supportsUsageApi = true) {
103
103
  if (!data) return null;
104
104
  const creds = data.claudeAiOauth || data;
105
105
  if (!creds.accessToken) return null;
@@ -107,9 +107,51 @@ export function readClaudeCredentials() {
107
107
  accessToken: creds.accessToken,
108
108
  refreshToken: creds.refreshToken,
109
109
  expiresAt: creds.expiresAt,
110
+ source,
111
+ supportsUsageApi,
110
112
  };
111
113
  }
112
114
 
115
+ function readClaudeKeychainCredentials(execFileSyncFn = execFileSync) {
116
+ try {
117
+ const raw = execFileSyncFn(
118
+ "security",
119
+ ["find-generic-password", "-s", CLAUDE_KEYCHAIN_SERVICE, "-w"],
120
+ { encoding: "utf8" },
121
+ );
122
+ return normalizeClaudeCredentials(JSON.parse(raw.trim()), "keychain");
123
+ } catch {
124
+ return null;
125
+ }
126
+ }
127
+
128
+ export function readClaudeCredentials({
129
+ readCredentialFile = () => readJson(CLAUDE_CREDENTIALS_PATH, null),
130
+ platform = process.platform,
131
+ execFileSyncFn = execFileSync,
132
+ env = process.env,
133
+ } = {}) {
134
+ const fileCreds = normalizeClaudeCredentials(readCredentialFile(), "file");
135
+ if (fileCreds) return fileCreds;
136
+
137
+ if (platform === "darwin") {
138
+ const keychainCreds = readClaudeKeychainCredentials(execFileSyncFn);
139
+ if (keychainCreds) return keychainCreds;
140
+ }
141
+
142
+ if (env.ANTHROPIC_API_KEY) {
143
+ return {
144
+ accessToken: env.ANTHROPIC_API_KEY,
145
+ refreshToken: null,
146
+ expiresAt: null,
147
+ source: "env",
148
+ supportsUsageApi: false,
149
+ };
150
+ }
151
+
152
+ return null;
153
+ }
154
+
113
155
  export function refreshClaudeAccessToken(refreshToken) {
114
156
  return new Promise((resolve) => {
115
157
  const clientId =
@@ -166,17 +208,67 @@ export function refreshClaudeAccessToken(refreshToken) {
166
208
  });
167
209
  }
168
210
 
169
- export function writeBackClaudeCredentials(creds) {
211
+ function serializeClaudeCredentials(creds) {
212
+ const oauth = {
213
+ accessToken: creds.accessToken,
214
+ };
215
+ if (creds.expiresAt != null) oauth.expiresAt = creds.expiresAt;
216
+ if (creds.refreshToken) oauth.refreshToken = creds.refreshToken;
217
+ return { claudeAiOauth: oauth };
218
+ }
219
+
220
+ function writeClaudeKeychainCredentials(creds, execFileSyncFn = execFileSync) {
221
+ execFileSyncFn(
222
+ "security",
223
+ [
224
+ "add-generic-password",
225
+ "-s",
226
+ CLAUDE_KEYCHAIN_SERVICE,
227
+ "-w",
228
+ JSON.stringify(serializeClaudeCredentials(creds)),
229
+ "-U",
230
+ ],
231
+ { stdio: "ignore" },
232
+ );
233
+ }
234
+
235
+ export function writeBackClaudeCredentials(
236
+ creds,
237
+ {
238
+ readCredentialFile = () => readJson(CLAUDE_CREDENTIALS_PATH, null),
239
+ writeCredentialFile = (data) =>
240
+ writeFileSync(CLAUDE_CREDENTIALS_PATH, JSON.stringify(data, null, 2)),
241
+ platform = process.platform,
242
+ execFileSyncFn = execFileSync,
243
+ } = {},
244
+ ) {
245
+ if (creds?.source === "env") return;
246
+
247
+ let data = null;
170
248
  try {
171
- const data = readJson(CLAUDE_CREDENTIALS_PATH, null);
172
- if (!data) return;
249
+ data = readCredentialFile();
250
+ } catch {
251
+ data = null;
252
+ }
253
+
254
+ if (data) {
173
255
  const target = data.claudeAiOauth || data;
174
256
  target.accessToken = creds.accessToken;
175
257
  if (creds.expiresAt != null) target.expiresAt = creds.expiresAt;
176
258
  if (creds.refreshToken) target.refreshToken = creds.refreshToken;
177
- writeFileSync(CLAUDE_CREDENTIALS_PATH, JSON.stringify(data, null, 2));
178
- } catch {
179
- /* 쓰기 실패 무시 */
259
+ try {
260
+ writeCredentialFile(data);
261
+ } catch {
262
+ /* 파일 쓰기 실패 무시 */
263
+ }
264
+ }
265
+
266
+ if (platform === "darwin") {
267
+ try {
268
+ writeClaudeKeychainCredentials(creds, execFileSyncFn);
269
+ } catch {
270
+ /* Keychain 쓰기 실패 무시 */
271
+ }
180
272
  }
181
273
  }
182
274
 
@@ -399,6 +491,10 @@ export async function fetchClaudeUsage(forceRefresh = false) {
399
491
  writeClaudeUsageCache(null, { type: "auth", status: 0 });
400
492
  return existingSnapshot.data || null;
401
493
  }
494
+ if (creds.supportsUsageApi === false) {
495
+ writeClaudeUsageCache(null, { type: "auth", status: 0 });
496
+ return existingSnapshot.data || null;
497
+ }
402
498
 
403
499
  // 토큰 만료 시 리프레시
404
500
  if (creds.expiresAt && creds.expiresAt <= Date.now() && creds.refreshToken) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triflux",
3
- "version": "10.18.0",
3
+ "version": "10.18.2",
4
4
  "description": "CLI-first multi-model orchestrator for Claude Code — route tasks to Codex, Gemini, and Claude",
5
5
  "type": "module",
6
6
  "bin": {
@@ -191,8 +191,13 @@ describe("release governance scripts", () => {
191
191
  });
192
192
  assert.equal(executed.skipTests, false);
193
193
 
194
+ // F3 (issue #192): test step 의 command 가 "npm test" 에서 직접 node
195
+ // test-lock.mjs 호출로 바뀜. legacy npm test 와 F3 후 node test-lock.mjs
196
+ // 둘 다 인식.
194
197
  const testCall = calls.find(
195
- (call) => call.command === "npm" && call.args.join(" ") === "test",
198
+ (call) =>
199
+ (call.command === "npm" && call.args.join(" ") === "test") ||
200
+ call.args[0]?.endsWith("test-lock.mjs"),
196
201
  );
197
202
  assert.ok(testCall);
198
203
  assert.deepEqual(testCall.options.stdio, ["ignore", "pipe", "pipe"]);
@@ -1,47 +1,47 @@
1
- #!/usr/bin/env bash
2
- # Installation: source /path/to/tfx.bash 또는 ~/.bashrc에 추가
3
-
4
- _tfx_completion() {
5
- local cur prev words cword
6
- COMPREPLY=()
7
- cur="${COMP_WORDS[COMP_CWORD]}"
8
- prev="${COMP_WORDS[COMP_CWORD-1]}"
9
- words=("${COMP_WORDS[@]}")
10
- cword=$COMP_CWORD
11
-
12
- local commands="setup doctor multi hub auto codex gemini"
13
- local multi_cmds="status stop kill attach list"
14
- local hub_cmds="start stop status restart"
15
- local flags="--thorough --quick --tmux --psmux --agents --no-attach --timeout"
16
-
17
- if [[ $cword -eq 1 ]]; then
18
- COMPREPLY=( $(compgen -W "${commands}" -- "$cur") )
19
- return 0
20
- fi
21
-
22
- local cmd="${words[1]}"
23
- case "${cmd}" in
24
- multi)
25
- if [[ $cword -eq 2 && ! "$cur" == -* ]]; then
26
- COMPREPLY=( $(compgen -W "${multi_cmds}" -- "$cur") )
27
- else
28
- COMPREPLY=( $(compgen -W "${flags}" -- "$cur") )
29
- fi
30
- ;;
31
- hub)
32
- if [[ $cword -eq 2 ]]; then
33
- COMPREPLY=( $(compgen -W "${hub_cmds}" -- "$cur") )
34
- fi
35
- ;;
36
- doctor)
37
- COMPREPLY=( $(compgen -W "--fix --reset" -- "$cur") )
38
- ;;
39
- setup|auto|codex|gemini)
40
- if [[ "$cur" == -* ]]; then
41
- COMPREPLY=( $(compgen -W "${flags}" -- "$cur") )
42
- fi
43
- ;;
44
- esac
45
- }
46
-
47
- complete -F _tfx_completion tfx
1
+ #!/usr/bin/env bash
2
+ # Installation: source /path/to/tfx.bash 또는 ~/.bashrc에 추가
3
+
4
+ _tfx_completion() {
5
+ local cur prev words cword
6
+ COMPREPLY=()
7
+ cur="${COMP_WORDS[COMP_CWORD]}"
8
+ prev="${COMP_WORDS[COMP_CWORD-1]}"
9
+ words=("${COMP_WORDS[@]}")
10
+ cword=$COMP_CWORD
11
+
12
+ local commands="setup doctor multi hub auto codex gemini"
13
+ local multi_cmds="status stop kill attach list"
14
+ local hub_cmds="start stop status restart"
15
+ local flags="--thorough --quick --tmux --psmux --agents --no-attach --timeout"
16
+
17
+ if [[ $cword -eq 1 ]]; then
18
+ COMPREPLY=( $(compgen -W "${commands}" -- "$cur") )
19
+ return 0
20
+ fi
21
+
22
+ local cmd="${words[1]}"
23
+ case "${cmd}" in
24
+ multi)
25
+ if [[ $cword -eq 2 && ! "$cur" == -* ]]; then
26
+ COMPREPLY=( $(compgen -W "${multi_cmds}" -- "$cur") )
27
+ else
28
+ COMPREPLY=( $(compgen -W "${flags}" -- "$cur") )
29
+ fi
30
+ ;;
31
+ hub)
32
+ if [[ $cword -eq 2 ]]; then
33
+ COMPREPLY=( $(compgen -W "${hub_cmds}" -- "$cur") )
34
+ fi
35
+ ;;
36
+ doctor)
37
+ COMPREPLY=( $(compgen -W "--fix --reset" -- "$cur") )
38
+ ;;
39
+ setup|auto|codex|gemini)
40
+ if [[ "$cur" == -* ]]; then
41
+ COMPREPLY=( $(compgen -W "${flags}" -- "$cur") )
42
+ fi
43
+ ;;
44
+ esac
45
+ }
46
+
47
+ complete -F _tfx_completion tfx
@@ -1,44 +1,44 @@
1
- # Installation: ~/.config/fish/completions/에 복사
2
- # e.g., cp /path/to/tfx.fish ~/.config/fish/completions/tfx.fish
3
-
4
- set -l commands setup doctor multi hub auto codex gemini
5
- set -l multi_cmds status stop kill attach list
6
- set -l hub_cmds start stop status restart
7
-
8
- complete -c tfx -f
9
-
10
- # Subcommands
11
- complete -c tfx -n "not __fish_seen_subcommand_from $commands" -a "setup" -d "Setup and sync files"
12
- complete -c tfx -n "not __fish_seen_subcommand_from $commands" -a "doctor" -d "Diagnose CLI and issues"
13
- complete -c tfx -n "not __fish_seen_subcommand_from $commands" -a "multi" -d "Multi-CLI team mode"
14
- complete -c tfx -n "not __fish_seen_subcommand_from $commands" -a "hub" -d "MCP message bus management"
15
- complete -c tfx -n "not __fish_seen_subcommand_from $commands" -a "auto" -d "Auto mode"
16
- complete -c tfx -n "not __fish_seen_subcommand_from $commands" -a "codex" -d "Codex mode"
17
- complete -c tfx -n "not __fish_seen_subcommand_from $commands" -a "gemini" -d "Gemini mode"
18
-
19
- # Doctor flags
20
- complete -c tfx -n "__fish_seen_subcommand_from doctor" -l fix -d "Auto fix issues"
21
- complete -c tfx -n "__fish_seen_subcommand_from doctor" -l reset -d "Reset all caches"
22
-
23
- # Multi subcommands
24
- complete -c tfx -n "__fish_seen_subcommand_from multi; and not __fish_seen_subcommand_from $multi_cmds" -a "status"
25
- complete -c tfx -n "__fish_seen_subcommand_from multi; and not __fish_seen_subcommand_from $multi_cmds" -a "stop"
26
- complete -c tfx -n "__fish_seen_subcommand_from multi; and not __fish_seen_subcommand_from $multi_cmds" -a "kill"
27
- complete -c tfx -n "__fish_seen_subcommand_from multi; and not __fish_seen_subcommand_from $multi_cmds" -a "attach"
28
- complete -c tfx -n "__fish_seen_subcommand_from multi; and not __fish_seen_subcommand_from $multi_cmds" -a "list"
29
-
30
- # Hub subcommands
31
- complete -c tfx -n "__fish_seen_subcommand_from hub; and not __fish_seen_subcommand_from $hub_cmds" -a "start"
32
- complete -c tfx -n "__fish_seen_subcommand_from hub; and not __fish_seen_subcommand_from $hub_cmds" -a "stop"
33
- complete -c tfx -n "__fish_seen_subcommand_from hub; and not __fish_seen_subcommand_from $hub_cmds" -a "status"
34
- complete -c tfx -n "__fish_seen_subcommand_from hub; and not __fish_seen_subcommand_from $hub_cmds" -a "restart"
35
-
36
- # Global or multi flags
37
- set -l flags_cond "__fish_seen_subcommand_from setup multi auto codex gemini"
38
- complete -c tfx -n "$flags_cond" -l thorough -d "Thorough execution"
39
- complete -c tfx -n "$flags_cond" -l quick -d "Quick execution"
40
- complete -c tfx -n "$flags_cond" -l tmux -d "Use tmux"
41
- complete -c tfx -n "$flags_cond" -l psmux -d "Use psmux"
42
- complete -c tfx -n "$flags_cond" -l agents -d "Specify agents"
43
- complete -c tfx -n "$flags_cond" -l no-attach -d "Do not attach"
44
- complete -c tfx -n "$flags_cond" -l timeout -d "Set timeout"
1
+ # Installation: ~/.config/fish/completions/에 복사
2
+ # e.g., cp /path/to/tfx.fish ~/.config/fish/completions/tfx.fish
3
+
4
+ set -l commands setup doctor multi hub auto codex gemini
5
+ set -l multi_cmds status stop kill attach list
6
+ set -l hub_cmds start stop status restart
7
+
8
+ complete -c tfx -f
9
+
10
+ # Subcommands
11
+ complete -c tfx -n "not __fish_seen_subcommand_from $commands" -a "setup" -d "Setup and sync files"
12
+ complete -c tfx -n "not __fish_seen_subcommand_from $commands" -a "doctor" -d "Diagnose CLI and issues"
13
+ complete -c tfx -n "not __fish_seen_subcommand_from $commands" -a "multi" -d "Multi-CLI team mode"
14
+ complete -c tfx -n "not __fish_seen_subcommand_from $commands" -a "hub" -d "MCP message bus management"
15
+ complete -c tfx -n "not __fish_seen_subcommand_from $commands" -a "auto" -d "Auto mode"
16
+ complete -c tfx -n "not __fish_seen_subcommand_from $commands" -a "codex" -d "Codex mode"
17
+ complete -c tfx -n "not __fish_seen_subcommand_from $commands" -a "gemini" -d "Gemini mode"
18
+
19
+ # Doctor flags
20
+ complete -c tfx -n "__fish_seen_subcommand_from doctor" -l fix -d "Auto fix issues"
21
+ complete -c tfx -n "__fish_seen_subcommand_from doctor" -l reset -d "Reset all caches"
22
+
23
+ # Multi subcommands
24
+ complete -c tfx -n "__fish_seen_subcommand_from multi; and not __fish_seen_subcommand_from $multi_cmds" -a "status"
25
+ complete -c tfx -n "__fish_seen_subcommand_from multi; and not __fish_seen_subcommand_from $multi_cmds" -a "stop"
26
+ complete -c tfx -n "__fish_seen_subcommand_from multi; and not __fish_seen_subcommand_from $multi_cmds" -a "kill"
27
+ complete -c tfx -n "__fish_seen_subcommand_from multi; and not __fish_seen_subcommand_from $multi_cmds" -a "attach"
28
+ complete -c tfx -n "__fish_seen_subcommand_from multi; and not __fish_seen_subcommand_from $multi_cmds" -a "list"
29
+
30
+ # Hub subcommands
31
+ complete -c tfx -n "__fish_seen_subcommand_from hub; and not __fish_seen_subcommand_from $hub_cmds" -a "start"
32
+ complete -c tfx -n "__fish_seen_subcommand_from hub; and not __fish_seen_subcommand_from $hub_cmds" -a "stop"
33
+ complete -c tfx -n "__fish_seen_subcommand_from hub; and not __fish_seen_subcommand_from $hub_cmds" -a "status"
34
+ complete -c tfx -n "__fish_seen_subcommand_from hub; and not __fish_seen_subcommand_from $hub_cmds" -a "restart"
35
+
36
+ # Global or multi flags
37
+ set -l flags_cond "__fish_seen_subcommand_from setup multi auto codex gemini"
38
+ complete -c tfx -n "$flags_cond" -l thorough -d "Thorough execution"
39
+ complete -c tfx -n "$flags_cond" -l quick -d "Quick execution"
40
+ complete -c tfx -n "$flags_cond" -l tmux -d "Use tmux"
41
+ complete -c tfx -n "$flags_cond" -l psmux -d "Use psmux"
42
+ complete -c tfx -n "$flags_cond" -l agents -d "Specify agents"
43
+ complete -c tfx -n "$flags_cond" -l no-attach -d "Do not attach"
44
+ complete -c tfx -n "$flags_cond" -l timeout -d "Set timeout"
@@ -1,83 +1,83 @@
1
- #compdef tfx
2
- # Installation: fpath에 추가 후 compinit
3
- # e.g., fpath=(/path/to/dir $fpath) && compinit
4
-
5
- _tfx() {
6
- local line state
7
- local -a commands multi_cmds hub_cmds flags
8
-
9
- commands=(
10
- 'setup:Setup and sync files'
11
- 'doctor:Diagnose CLI and issues'
12
- 'multi:Multi-CLI team mode'
13
- 'hub:MCP message bus management'
14
- 'auto:Auto mode'
15
- 'codex:Codex mode'
16
- 'gemini:Gemini mode'
17
- )
18
-
19
- multi_cmds=(
20
- 'status:Show status'
21
- 'stop:Stop multi'
22
- 'kill:Kill multi'
23
- 'attach:Attach to multi'
24
- 'list:List multi sessions'
25
- )
26
-
27
- hub_cmds=(
28
- 'start:Start hub'
29
- 'stop:Stop hub'
30
- 'status:Show hub status'
31
- 'restart:Restart hub'
32
- )
33
-
34
- _arguments -C \
35
- '1: :->cmds' \
36
- '*: :->args'
37
-
38
- case $state in
39
- cmds)
40
- _describe -t commands 'tfx commands' commands
41
- ;;
42
- args)
43
- case $words[2] in
44
- multi)
45
- if (( CURRENT == 3 )) && [[ $words[CURRENT] != -* ]]; then
46
- _describe -t multi_cmds 'multi commands' multi_cmds
47
- else
48
- _arguments \
49
- '--thorough[Thorough execution]' \
50
- '--quick[Quick execution]' \
51
- '--tmux[Use tmux]' \
52
- '--psmux[Use psmux]' \
53
- '--agents[Specify agents]' \
54
- '--no-attach[Do not attach]' \
55
- '--timeout[Set timeout]'
56
- fi
57
- ;;
58
- hub)
59
- if (( CURRENT == 3 )); then
60
- _describe -t hub_cmds 'hub commands' hub_cmds
61
- fi
62
- ;;
63
- doctor)
64
- _arguments \
65
- '--fix[Auto fix issues]' \
66
- '--reset[Reset all caches]'
67
- ;;
68
- *)
69
- _arguments \
70
- '--thorough[Thorough execution]' \
71
- '--quick[Quick execution]' \
72
- '--tmux[Use tmux]' \
73
- '--psmux[Use psmux]' \
74
- '--agents[Specify agents]' \
75
- '--no-attach[Do not attach]' \
76
- '--timeout[Set timeout]'
77
- ;;
78
- esac
79
- ;;
80
- esac
81
- }
82
-
83
- _tfx "$@"
1
+ #compdef tfx
2
+ # Installation: fpath에 추가 후 compinit
3
+ # e.g., fpath=(/path/to/dir $fpath) && compinit
4
+
5
+ _tfx() {
6
+ local line state
7
+ local -a commands multi_cmds hub_cmds flags
8
+
9
+ commands=(
10
+ 'setup:Setup and sync files'
11
+ 'doctor:Diagnose CLI and issues'
12
+ 'multi:Multi-CLI team mode'
13
+ 'hub:MCP message bus management'
14
+ 'auto:Auto mode'
15
+ 'codex:Codex mode'
16
+ 'gemini:Gemini mode'
17
+ )
18
+
19
+ multi_cmds=(
20
+ 'status:Show status'
21
+ 'stop:Stop multi'
22
+ 'kill:Kill multi'
23
+ 'attach:Attach to multi'
24
+ 'list:List multi sessions'
25
+ )
26
+
27
+ hub_cmds=(
28
+ 'start:Start hub'
29
+ 'stop:Stop hub'
30
+ 'status:Show hub status'
31
+ 'restart:Restart hub'
32
+ )
33
+
34
+ _arguments -C \
35
+ '1: :->cmds' \
36
+ '*: :->args'
37
+
38
+ case $state in
39
+ cmds)
40
+ _describe -t commands 'tfx commands' commands
41
+ ;;
42
+ args)
43
+ case $words[2] in
44
+ multi)
45
+ if (( CURRENT == 3 )) && [[ $words[CURRENT] != -* ]]; then
46
+ _describe -t multi_cmds 'multi commands' multi_cmds
47
+ else
48
+ _arguments \
49
+ '--thorough[Thorough execution]' \
50
+ '--quick[Quick execution]' \
51
+ '--tmux[Use tmux]' \
52
+ '--psmux[Use psmux]' \
53
+ '--agents[Specify agents]' \
54
+ '--no-attach[Do not attach]' \
55
+ '--timeout[Set timeout]'
56
+ fi
57
+ ;;
58
+ hub)
59
+ if (( CURRENT == 3 )); then
60
+ _describe -t hub_cmds 'hub commands' hub_cmds
61
+ fi
62
+ ;;
63
+ doctor)
64
+ _arguments \
65
+ '--fix[Auto fix issues]' \
66
+ '--reset[Reset all caches]'
67
+ ;;
68
+ *)
69
+ _arguments \
70
+ '--thorough[Thorough execution]' \
71
+ '--quick[Quick execution]' \
72
+ '--tmux[Use tmux]' \
73
+ '--psmux[Use psmux]' \
74
+ '--agents[Specify agents]' \
75
+ '--no-attach[Do not attach]' \
76
+ '--timeout[Set timeout]'
77
+ ;;
78
+ esac
79
+ ;;
80
+ esac
81
+ }
82
+
83
+ _tfx "$@"
@@ -0,0 +1,121 @@
1
+ import { execFileSync } from "node:child_process";
2
+
3
+ export function commandExists(name) {
4
+ try {
5
+ execFileSync("sh", ["-c", 'command -v "$1" >/dev/null 2>&1', "sh", name], {
6
+ stdio: "ignore",
7
+ timeout: 2000,
8
+ });
9
+ return true;
10
+ } catch {
11
+ return false;
12
+ }
13
+ }
14
+
15
+ export function inspectMacTimeoutDependency({
16
+ platform = process.platform,
17
+ commandExists: exists = commandExists,
18
+ } = {}) {
19
+ if (platform !== "darwin") {
20
+ return { ok: true, status: "skipped", platform };
21
+ }
22
+
23
+ if (exists("gtimeout")) {
24
+ return { ok: true, status: "ok", platform, provider: "gtimeout" };
25
+ }
26
+ if (exists("timeout")) {
27
+ return { ok: true, status: "ok", platform, provider: "timeout" };
28
+ }
29
+
30
+ return {
31
+ ok: false,
32
+ status: "missing",
33
+ platform,
34
+ fix: "brew install coreutils",
35
+ };
36
+ }
37
+
38
+ export function addPluginRootFallbackToCommand(command, pluginRoot) {
39
+ if (typeof command !== "string") return command;
40
+ const fallbackRoot = String(pluginRoot || "").replace(/\\/g, "/");
41
+ if (!fallbackRoot) return command;
42
+ return command
43
+ .replaceAll("${PLUGIN_ROOT}", `\${PLUGIN_ROOT:-${fallbackRoot}}`)
44
+ .replaceAll(
45
+ "${CLAUDE_PLUGIN_ROOT}",
46
+ `\${CLAUDE_PLUGIN_ROOT:-${fallbackRoot}}`,
47
+ )
48
+ .replace(
49
+ /(^|[\s"'])\/(hooks|scripts)\//g,
50
+ `$1\${PLUGIN_ROOT:-${fallbackRoot}}/$2/`,
51
+ );
52
+ }
53
+
54
+ function hasBarePluginRootReference(command) {
55
+ if (typeof command !== "string") return false;
56
+ return (
57
+ command.includes("${PLUGIN_ROOT}/") ||
58
+ command.includes("${CLAUDE_PLUGIN_ROOT}/")
59
+ );
60
+ }
61
+
62
+ function hasCollapsedRootHookPath(command) {
63
+ if (typeof command !== "string") return false;
64
+ return /(^|[\s"'])\/(?:hooks|scripts)\//.test(command);
65
+ }
66
+
67
+ export function findPluginRootHookIssues(settings) {
68
+ const hooksByEvent =
69
+ settings?.hooks && typeof settings.hooks === "object" ? settings.hooks : {};
70
+ const issues = [];
71
+
72
+ for (const [event, entries] of Object.entries(hooksByEvent)) {
73
+ if (!Array.isArray(entries)) continue;
74
+ for (const entry of entries) {
75
+ const hooks = Array.isArray(entry?.hooks) ? entry.hooks : [];
76
+ for (const hook of hooks) {
77
+ const command = hook?.command;
78
+ if (
79
+ !hasBarePluginRootReference(command) &&
80
+ !hasCollapsedRootHookPath(command)
81
+ ) {
82
+ continue;
83
+ }
84
+ issues.push({
85
+ event,
86
+ matcher: entry?.matcher || "*",
87
+ command,
88
+ reason: hasCollapsedRootHookPath(command)
89
+ ? "collapsed-root-path"
90
+ : "missing-plugin-root-fallback",
91
+ });
92
+ }
93
+ }
94
+ }
95
+
96
+ return issues;
97
+ }
98
+
99
+ export function applyPluginRootHookFallbacks(settings, { pluginRoot } = {}) {
100
+ if (!settings?.hooks || typeof settings.hooks !== "object") {
101
+ return { changed: false, count: 0, settings };
102
+ }
103
+
104
+ let count = 0;
105
+ for (const entries of Object.values(settings.hooks)) {
106
+ if (!Array.isArray(entries)) continue;
107
+ for (const entry of entries) {
108
+ const hooks = Array.isArray(entry?.hooks) ? entry.hooks : [];
109
+ for (const hook of hooks) {
110
+ if (typeof hook?.command !== "string") continue;
111
+ const next = addPluginRootFallbackToCommand(hook.command, pluginRoot);
112
+ if (next !== hook.command) {
113
+ hook.command = next;
114
+ count++;
115
+ }
116
+ }
117
+ }
118
+ }
119
+
120
+ return { changed: count > 0, count, settings };
121
+ }
@@ -64,23 +64,32 @@ export async function prepareRelease({
64
64
  const steps = [
65
65
  {
66
66
  name: "npm-test",
67
- command: "npm",
68
- args: ["test"],
67
+ // F3 fix (issue #192 — silent EXIT 0 회귀 우회):
68
+ // npm wrapper 경유 시 prepare.mjs process 가 native level 에서 hard kill
69
+ // 됨 (process.on("exit") 핸들러 호출 안 됨, EXIT 0 silent, 후속 lint/pack
70
+ // step 누락). 2026-04-30 진단 세션에서 5 시나리오 reproduce 결과:
71
+ // - npm wrapper 경유 + npm test 실행: silent EXIT 0 재현 (12-24s 비결정)
72
+ // - 직접 node 실행 또는 npm test 단독: 정상 진행
73
+ // - --skip-tests 우회: 정상 unhandled rejection (lint fail visible)
74
+ // 따라서 npm wrapper 한 단계 줄여 trigger 약화 (cmd.exe shim → npm-cli.js
75
+ // → spawn 3 layers 제거). package.json `scripts.test` 와 sync 유지 필수.
76
+ command: process.execPath,
77
+ args: [
78
+ "scripts/test-lock.mjs",
79
+ "--test",
80
+ "--test-force-exit",
81
+ "--test-concurrency=8",
82
+ "tests/**/*.test.mjs",
83
+ "scripts/__tests__/**/*.test.mjs",
84
+ ],
69
85
  skip: skipTests,
70
- // Windows background execution can stall when `npm test` inherits the
71
- // parent's console handles through nested shell/spawn layers. Run the
72
- // heavy test step non-interactively and fail fast if it never returns.
73
- // maxBuffer raised explicitly: 1 MiB default is too small for piped
74
- // npm test --test-concurrency=8 verbose output. This is generic
75
- // robustness, NOT a fix for the prepare-only EXIT=1 mismatch — that
76
- // root cause is the test-lock.mjs spawn `stdio: "inherit"` cascading
77
- // the parent's ignore/pipe/pipe down to grand-child `node --test`,
78
- // breaking ConPTY assumptions on Windows. See issue #192 for the
79
- // diagnosis and fix candidates (F1 = test-lock stdio split).
86
+ // maxBuffer: 1 MiB default is too small for piped node --test verbose
87
+ // output. 128 MiB matches the prior npm-test ceiling.
80
88
  options: {
81
89
  stdio: ["ignore", "pipe", "pipe"],
82
90
  timeoutMs: TEST_TIMEOUT_MS,
83
91
  maxBuffer: 128 * 1024 * 1024,
92
+ shell: false,
84
93
  },
85
94
  },
86
95
  {
package/scripts/setup.mjs CHANGED
@@ -18,13 +18,14 @@ import {
18
18
  writeFileSync,
19
19
  } from "fs";
20
20
  import { homedir } from "os";
21
- import { dirname, join, relative } from "path";
21
+ import { dirname, join, relative, resolve } from "path";
22
22
  import { fileURLToPath } from "url";
23
23
  import {
24
24
  ensureGlobalClaudeRoutingSection,
25
25
  ensureTfxSection,
26
26
  getLatestRoutingTable,
27
27
  } from "./claudemd-sync.mjs";
28
+ import { addPluginRootFallbackToCommand } from "./lib/doctor-env-checks.mjs";
28
29
  import { cleanupTmpFiles } from "./tmp-cleanup.mjs";
29
30
 
30
31
  const PLUGIN_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
@@ -141,7 +142,7 @@ function isSetupUserStateFile(fileName) {
141
142
  }
142
143
 
143
144
  /**
144
- * scripts/lib/*.mjs 자동 스캔.
145
+ * scripts/lib/*.mjs 및 *.sh 자동 스캔.
145
146
  * 수동 리스트 대신 glob으로 탐색하여 lib 파일 추가 시 sync 누락 방지.
146
147
  */
147
148
  function scanLibFiles(pluginRoot, claudeDir) {
@@ -149,7 +150,7 @@ function scanLibFiles(pluginRoot, claudeDir) {
149
150
  if (!existsSync(libDir)) return [];
150
151
  return readdirSync(libDir)
151
152
  .sort()
152
- .filter((f) => f.endsWith(".mjs"))
153
+ .filter((f) => f.endsWith(".mjs") || f.endsWith(".sh"))
153
154
  .map((f) => ({
154
155
  src: join(libDir, f),
155
156
  dst: join(claudeDir, "scripts", "lib", f),
@@ -497,10 +498,22 @@ function extractManagedHookFilename(command) {
497
498
  return match ? match[1] : null;
498
499
  }
499
500
 
501
+ function expandRequiresPath(value) {
502
+ if (typeof value !== "string" || value.trim() === "") return null;
503
+ return resolve(
504
+ value.replace(/\$\{HOME\}/g, _TFX_HOME).replace(/\$HOME\b/g, _TFX_HOME),
505
+ );
506
+ }
507
+
508
+ function isRequiredPathAvailable(value) {
509
+ const expanded = expandRequiresPath(value);
510
+ return expanded ? existsSync(expanded) : true;
511
+ }
512
+
500
513
  /**
501
514
  * hook-registry.json에서 관리 대상 훅 목록을 플랫 배열로 반환한다.
502
515
  * @param {string} registryPath - hook-registry.json 경로
503
- * @returns {Array<{ event: string, id: string, fileName: string, matcher: string, command: string, priority: number, enabled: boolean }>}
516
+ * @returns {Array<{ event: string, id: string, fileName: string, matcher: string, command: string, priority: number, enabled: boolean, requires?: string }>}
504
517
  */
505
518
  function getManagedRegistryHooks(registryPath) {
506
519
  if (!existsSync(registryPath)) return [];
@@ -512,6 +525,7 @@ function getManagedRegistryHooks(registryPath) {
512
525
  if (!Array.isArray(hooks)) continue;
513
526
  for (const hook of hooks) {
514
527
  if (!hook.enabled) continue;
528
+ if (!isRequiredPathAvailable(hook.requires)) continue;
515
529
  const fileName = extractManagedHookFilename(hook.command);
516
530
  result.push({
517
531
  event,
@@ -521,6 +535,7 @@ function getManagedRegistryHooks(registryPath) {
521
535
  command: hook.command || "",
522
536
  priority: hook.priority ?? 100,
523
537
  enabled: hook.enabled,
538
+ requires: hook.requires,
524
539
  });
525
540
  }
526
541
  }
@@ -563,7 +578,13 @@ function ensureHooksInSettings({ settingsPath, registryPath }) {
563
578
 
564
579
  entries.push({
565
580
  matcher: spec.matcher,
566
- hooks: [{ type: "command", command: spec.command, timeout: 5 }],
581
+ hooks: [
582
+ {
583
+ type: "command",
584
+ command: addPluginRootFallbackToCommand(spec.command, PLUGIN_ROOT),
585
+ timeout: 5,
586
+ },
587
+ ],
567
588
  });
568
589
  added.push(spec.id || spec.fileName);
569
590
  }
@@ -365,7 +365,8 @@ mkdir -p "$TFX_PROBE_DIR" 2>/dev/null || true
365
365
 
366
366
  estimate_expected_duration_sec() {
367
367
  local agent="${1:-}" profile="${2:-}" prompt="${3:-}"
368
- local text="${prompt,,}"
368
+ local text
369
+ text=$(printf '%s' "$prompt" | tr '[:upper:]' '[:lower:]')
369
370
  local expected=30
370
371
 
371
372
  case "$agent" in
@@ -1956,8 +1957,12 @@ _codex_config_swap() {
1956
1957
 
1957
1958
  # codex-recovery.sh 의 recover_codex_stdout 헬퍼 사용. STDOUT_LOG/STDERR_LOG env.
1958
1959
  _TFX_ROUTE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
1959
- # shellcheck source=lib/codex-recovery.sh
1960
- source "$_TFX_ROUTE_DIR/lib/codex-recovery.sh"
1960
+ if [[ -f "$_TFX_ROUTE_DIR/lib/codex-recovery.sh" ]]; then
1961
+ # shellcheck source=lib/codex-recovery.sh
1962
+ source "$_TFX_ROUTE_DIR/lib/codex-recovery.sh"
1963
+ else
1964
+ echo "[tfx-route] WARNING: optional helper missing: $_TFX_ROUTE_DIR/lib/codex-recovery.sh (run: tfx doctor --fix)" >&2
1965
+ fi
1961
1966
 
1962
1967
  run_codex_exec() {
1963
1968
  local prompt="$1"
@@ -30,7 +30,7 @@ const REPORTS_DIR = join(STATE_DIR, "reports");
30
30
  // ── 가격 모델 ($/MTok, 비캐시 기준, 보수적 추정) ──
31
31
  const PRICING = {
32
32
  claude_sonnet: { input: 3, output: 15 },
33
- claude_opus: { input: 15, output: 75 },
33
+ claude_opus: { input: 5, output: 25 },
34
34
  codex: { input: 0, output: 0 },
35
35
  gemini_flash: { input: 0.1, output: 0.4 },
36
36
  };
@@ -38,7 +38,7 @@ const PRICING = {
38
38
  // Claude 캐시 가격 ($/MTok) — 오케스트레이션 비용 정밀 계산용
39
39
  const CLAUDE_CACHE_PRICING = {
40
40
  claude_sonnet: { cache_write: 3.75, cache_read: 0.3 },
41
- claude_opus: { cache_write: 18.75, cache_read: 1.5 },
41
+ claude_opus: { cache_write: 6.25, cache_read: 0.5 },
42
42
  };
43
43
 
44
44
  // 에이전트 → Claude 대체 모델
@@ -628,11 +628,13 @@ function generateReport(sessionId) {
628
628
 
629
629
  // ── Named exports (파이프라인 벤치마크 훅용) ──
630
630
  export {
631
+ CLAUDE_CACHE_PRICING,
631
632
  computeDiff,
632
633
  DIFFS_DIR,
633
634
  estimateSavings,
634
635
  formatCost,
635
636
  formatTokenCount,
637
+ PRICING,
636
638
  STATE_DIR,
637
639
  takeSnapshot,
638
640
  };
@@ -1,46 +0,0 @@
1
- {
2
- "hosts": {
3
- "ultra4": {
4
- "description": "Windows 데스크탑 (22코어/64GB RAM)",
5
- "aliases": ["울트라", "울트라4", "데스크탑"],
6
- "default_dir": "~/Desktop/Projects/cli/triflux",
7
- "tailscale": {
8
- "ip": "100.110.136.64",
9
- "dns": "ultra-book-4-1",
10
- "ssh_mode": "ssh-over-vpn"
11
- },
12
- "ssh_user": "SSAFY",
13
- "os": "windows",
14
- "specs": {
15
- "cores": 22,
16
- "ram_gb": 64,
17
- "codex": "0.118.0",
18
- "node": "24.14.0"
19
- },
20
- "capabilities": ["codex", "claude", "high-memory"]
21
- },
22
- "m2": {
23
- "description": "MacBook Air M2 (8코어/16GB RAM)",
24
- "aliases": ["맥", "맥북", "m2"],
25
- "default_dir": "~/Desktop/Projects/triflux",
26
- "tailscale": {
27
- "ip": "100.104.61.126",
28
- "dns": "tellang의-macbook-air",
29
- "ssh_mode": "ssh-over-vpn"
30
- },
31
- "ssh_user": "tellang",
32
- "os": "darwin",
33
- "specs": {
34
- "cores": 8,
35
- "ram_gb": 16,
36
- "claude": "2.1.89",
37
- "codex": "0.114.0",
38
- "gemini": "0.33.1",
39
- "node": "25.9.0"
40
- },
41
- "capabilities": ["codex", "claude", "gemini"]
42
- }
43
- },
44
- "default_host": "m2",
45
- "triggers": ["원격에서", "다른 머신에서", "다른 컴퓨터에서"]
46
- }
@@ -1 +0,0 @@
1
- {"t":0,"agent":"adb34b0","agent_type":"general-purpose","event":"agent_stop","success":true}
@@ -1,3 +0,0 @@
1
- {
2
- "lastSentAt": "2026-03-29T03:41:07.256Z"
3
- }
@@ -1,7 +0,0 @@
1
- {
2
- "tool_name": "Bash",
3
- "tool_input_preview": "{\"command\":\"ls -la \\\"C:/Users/tellang/Desktop/Projects/triflux/skills/tfx-deslop/\\\" 2>/dev/null; echo \\\"===\\\"; ls -la \\\"C:/Users/tellang/Desktop/Projects/triflux/skills/tfx-codebase-search/\\\" 2>/dev/n...",
4
- "error": "Exit code 2\n===",
5
- "timestamp": "2026-03-29T03:40:35.913Z",
6
- "retry_count": 1
7
- }
@@ -1,7 +0,0 @@
1
- {
2
- "agents": [],
3
- "total_spawned": 0,
4
- "total_completed": 0,
5
- "total_failed": 0,
6
- "last_updated": "2026-03-29T03:41:34.338Z"
7
- }
@@ -1,41 +0,0 @@
1
- {
2
- "hosts": {
3
- "m2": {
4
- "description": "MacBook Air (tellang M2)",
5
- "aliases": ["맥북", "맥"],
6
- "default_dir": "~/projects",
7
- "os": "darwin",
8
- "ssh_user": "tellang",
9
- "tailscale": {
10
- "ip": "100.104.61.126",
11
- "dns": "m2.sole-hexatonic.ts.net",
12
- "ssh_mode": "ssh-over-vpn"
13
- },
14
- "capabilities": {
15
- "ssh_active": true,
16
- "claude": true,
17
- "node": "v25.9.0"
18
- },
19
- "last_probe": "2026-04-24T19:04:48Z"
20
- },
21
- "fold7": {
22
- "description": "Samsung Z Fold 7 (Termux)",
23
- "aliases": ["폴드", "폰", "fold"],
24
- "default_dir": "~",
25
- "os": "android",
26
- "ssh_user": "",
27
- "tailscale": {
28
- "ip": "100.107.139.115",
29
- "dns": "fold7.sole-hexatonic.ts.net",
30
- "ssh_mode": "ssh-over-vpn"
31
- },
32
- "capabilities": {
33
- "ssh_active": false,
34
- "note": "Termux sshd 미실행 — Fold7에서 'pkg install openssh && sshd -p 8022' 후 probe 재실행"
35
- },
36
- "last_probe": "2026-04-24T19:04:48Z"
37
- }
38
- },
39
- "default_host": "m2",
40
- "triggers": ["원격에서", "다른 머신에서", "다른 컴퓨터에서"]
41
- }
@@ -1,16 +0,0 @@
1
- {
2
- "hosts": {
3
- "ultra4": {
4
- "description": "Windows 데스크탑 (SSAFY)",
5
- "aliases": ["울트라", "데스크탑"],
6
- "default_dir": "~/Desktop/Projects"
7
- },
8
- "m2": {
9
- "description": "MacBook Pro",
10
- "aliases": ["맥북", "맥"],
11
- "default_dir": "~/projects"
12
- }
13
- },
14
- "default_host": "ultra4",
15
- "triggers": ["원격에서", "다른 머신에서", "다른 컴퓨터에서"]
16
- }