triflux 7.3.2 → 7.5.0

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 (38) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.ko.md +145 -145
  3. package/README.md +145 -145
  4. package/hub/pipeline/index.mjs +318 -318
  5. package/hub/schema.sql +146 -146
  6. package/hub/team/agent-map.json +2 -1
  7. package/hub/team/backend.mjs +3 -3
  8. package/hub/team/cli/commands/kill.mjs +37 -37
  9. package/hub/team/cli/commands/start/parse-args.mjs +4 -2
  10. package/hub/team/cli/commands/stop.mjs +31 -31
  11. package/hub/team/cli/commands/task.mjs +30 -30
  12. package/hub/team/cli/services/hub-client.mjs +208 -208
  13. package/hub/team/cli/services/native-control.mjs +4 -1
  14. package/hub/team/cli/services/runtime-mode.mjs +62 -62
  15. package/hub/team/cli/services/state-store.mjs +48 -48
  16. package/hub/team/codex-compat.mjs +78 -0
  17. package/hub/team/dashboard.mjs +274 -274
  18. package/hub/team/native.mjs +649 -649
  19. package/hub/team/pane.mjs +154 -150
  20. package/hub/team/psmux.mjs +1041 -1023
  21. package/hub/team/tui-viewer.mjs +2 -2
  22. package/hub/team/tui.mjs +12 -1
  23. package/hub/tools.mjs +554 -554
  24. package/hud/constants.mjs +3 -0
  25. package/package.json +1 -1
  26. package/scripts/claude-logged.ps1 +54 -0
  27. package/scripts/headless-guard.mjs +94 -7
  28. package/scripts/lib/mcp-filter.mjs +720 -720
  29. package/scripts/preflight-cache.mjs +137 -137
  30. package/scripts/remote-spawn.mjs +222 -0
  31. package/scripts/setup.mjs +84 -1
  32. package/scripts/tfx-gate-activate.mjs +89 -0
  33. package/scripts/tfx-route-post.mjs +17 -13
  34. package/scripts/tfx-route.sh +118 -46
  35. package/scripts/token-snapshot.mjs +575 -575
  36. package/skills/remote-spawn/SKILL.md +63 -0
  37. package/skills/tfx-auto/SKILL.md +1 -1
  38. package/skills/tfx-multi/SKILL.md +1 -1
@@ -1,137 +1,137 @@
1
- #!/usr/bin/env node
2
- // scripts/preflight-cache.mjs — 세션 시작 시 preflight 점검 캐싱
3
-
4
- import { writeFileSync, mkdirSync, existsSync, readFileSync } from "node:fs";
5
- import { join, dirname } from "node:path";
6
- import { homedir } from "node:os";
7
- import { execSync, spawn } from "node:child_process";
8
- import { fileURLToPath } from "node:url";
9
-
10
- const __filename = fileURLToPath(import.meta.url);
11
- const PKG_ROOT = join(dirname(__filename), "..");
12
-
13
- // 동기 대기 (Atomics.wait — Node.js main thread에서 사용 가능)
14
- const _sab = new Int32Array(new SharedArrayBuffer(4));
15
- function sleepSync(ms) { Atomics.wait(_sab, 0, 0, ms); }
16
-
17
- const CACHE_DIR = join(homedir(), ".claude", "cache");
18
- const CACHE_FILE = join(CACHE_DIR, "tfx-preflight.json");
19
- const CACHE_TTL_MS = 30_000; // 30초
20
-
21
- function checkHub() {
22
- // 1차 시도
23
- try {
24
- const res = execSync("curl -sf http://127.0.0.1:27888/status", { timeout: 3000, encoding: "utf8", windowsHide: true });
25
- const data = JSON.parse(res);
26
- return { ok: true, state: data?.hub?.state || "unknown", pid: data?.pid };
27
- } catch {}
28
-
29
- // Hub 미응답 → 자동 재시작 시도 (bridge.mjs tryRestartHub 동기 버전)
30
- const serverPath = join(PKG_ROOT, "hub", "server.mjs");
31
- if (!existsSync(serverPath)) return { ok: false, state: "unreachable", restart: "no_server" };
32
-
33
- try {
34
- const child = spawn(process.execPath, [serverPath], {
35
- detached: true,
36
- stdio: "ignore",
37
- windowsHide: true,
38
- });
39
- child.unref();
40
- } catch {
41
- return { ok: false, state: "unreachable", restart: "spawn_failed" };
42
- }
43
-
44
- // 최대 4초 폴링 (500ms × 8)
45
- for (let i = 0; i < 8; i++) {
46
- sleepSync(500);
47
- try {
48
- const res = execSync("curl -sf http://127.0.0.1:27888/status", { timeout: 1000, encoding: "utf8", windowsHide: true });
49
- const data = JSON.parse(res);
50
- if (data?.hub?.state === "healthy") {
51
- return { ok: true, state: "healthy", pid: data?.pid, restarted: true };
52
- }
53
- } catch {}
54
- }
55
-
56
- return { ok: false, state: "unreachable", restart: "timeout" };
57
- }
58
-
59
- function checkRoute() {
60
- const routePath = join(homedir(), ".claude", "scripts", "tfx-route.sh");
61
- return { ok: existsSync(routePath), path: routePath };
62
- }
63
-
64
- function checkCli(name) {
65
- try {
66
- const path = execSync(`which ${name} 2>/dev/null || where ${name} 2>nul`, { encoding: "utf8", timeout: 2000, windowsHide: true }).trim();
67
- return { ok: !!path, path };
68
- } catch {
69
- return { ok: false };
70
- }
71
- }
72
-
73
- /** Codex auth.json의 JWT에서 chatgpt_plan_type 추출 (pro/plus/free) */
74
- function detectCodexPlan() {
75
- try {
76
- const authPath = join(homedir(), ".codex", "auth.json");
77
- if (!existsSync(authPath)) return { plan: "unknown", source: "no_auth" };
78
- const auth = JSON.parse(readFileSync(authPath, "utf8"));
79
- if (auth.auth_mode !== "chatgpt") return { plan: "api", source: "api_key" };
80
- const token = auth.tokens?.id_token || auth.tokens?.access_token;
81
- if (!token) return { plan: "unknown", source: "no_token" };
82
- // JWT payload = 2번째 파트, base64url 디코딩
83
- const payload = JSON.parse(Buffer.from(token.split(".")[1], "base64url").toString());
84
- const plan = payload?.["https://api.openai.com/auth"]?.chatgpt_plan_type || "unknown";
85
- return { plan, source: "jwt" };
86
- } catch {
87
- return { plan: "unknown", source: "error" };
88
- }
89
- }
90
-
91
- function runPreflight() {
92
- const result = {
93
- timestamp: Date.now(),
94
- hub: checkHub(),
95
- route: checkRoute(),
96
- codex: checkCli("codex"),
97
- gemini: checkCli("gemini"),
98
- codex_plan: detectCodexPlan(),
99
- ok: false,
100
- };
101
- result.ok = result.hub.ok && result.route.ok;
102
-
103
- // CLI 가용성 → available_agents (triage에서 참조)
104
- const agents = [];
105
- if (result.codex.ok) agents.push("codex");
106
- if (result.gemini.ok) agents.push("gemini");
107
- agents.push("claude"); // claude는 항상 가용
108
- result.available_agents = agents;
109
-
110
- return result;
111
- }
112
-
113
- // 캐시 읽기 (TTL 검증 포함)
114
- export function readPreflightCache() {
115
- try {
116
- const data = JSON.parse(readFileSync(CACHE_FILE, "utf8"));
117
- if (Date.now() - data.timestamp < CACHE_TTL_MS) return data;
118
- } catch {}
119
- return null;
120
- }
121
-
122
- // 메인 실행
123
- if (process.argv[1]?.endsWith("preflight-cache.mjs")) {
124
- const result = runPreflight();
125
- mkdirSync(CACHE_DIR, { recursive: true });
126
- writeFileSync(CACHE_FILE, JSON.stringify(result, null, 2));
127
- // 간결 출력 (hook stdout)
128
- const summary = result.ok ? "preflight: ok" : "preflight: FAIL";
129
- const details = [];
130
- if (!result.hub.ok) details.push("hub:" + result.hub.state);
131
- else if (result.hub.restarted) details.push("hub:restarted");
132
- if (!result.route.ok) details.push("route:missing");
133
- if (result.available_agents.length === 1) details.push("agents:claude-only");
134
- console.log(details.length ? `${summary} (${details.join(", ")})` : summary);
135
- }
136
-
137
- export { runPreflight, CACHE_FILE, CACHE_TTL_MS };
1
+ #!/usr/bin/env node
2
+ // scripts/preflight-cache.mjs — 세션 시작 시 preflight 점검 캐싱
3
+
4
+ import { writeFileSync, mkdirSync, existsSync, readFileSync } from "node:fs";
5
+ import { join, dirname } from "node:path";
6
+ import { homedir } from "node:os";
7
+ import { execSync, spawn } from "node:child_process";
8
+ import { fileURLToPath } from "node:url";
9
+
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const PKG_ROOT = join(dirname(__filename), "..");
12
+
13
+ // 동기 대기 (Atomics.wait — Node.js main thread에서 사용 가능)
14
+ const _sab = new Int32Array(new SharedArrayBuffer(4));
15
+ function sleepSync(ms) { Atomics.wait(_sab, 0, 0, ms); }
16
+
17
+ const CACHE_DIR = join(homedir(), ".claude", "cache");
18
+ const CACHE_FILE = join(CACHE_DIR, "tfx-preflight.json");
19
+ const CACHE_TTL_MS = 30_000; // 30초
20
+
21
+ function checkHub() {
22
+ // 1차 시도
23
+ try {
24
+ const res = execSync("curl -sf http://127.0.0.1:27888/status", { timeout: 3000, encoding: "utf8", windowsHide: true });
25
+ const data = JSON.parse(res);
26
+ return { ok: true, state: data?.hub?.state || "unknown", pid: data?.pid };
27
+ } catch {}
28
+
29
+ // Hub 미응답 → 자동 재시작 시도 (bridge.mjs tryRestartHub 동기 버전)
30
+ const serverPath = join(PKG_ROOT, "hub", "server.mjs");
31
+ if (!existsSync(serverPath)) return { ok: false, state: "unreachable", restart: "no_server" };
32
+
33
+ try {
34
+ const child = spawn(process.execPath, [serverPath], {
35
+ detached: true,
36
+ stdio: "ignore",
37
+ windowsHide: true,
38
+ });
39
+ child.unref();
40
+ } catch {
41
+ return { ok: false, state: "unreachable", restart: "spawn_failed" };
42
+ }
43
+
44
+ // 최대 4초 폴링 (500ms × 8)
45
+ for (let i = 0; i < 8; i++) {
46
+ sleepSync(500);
47
+ try {
48
+ const res = execSync("curl -sf http://127.0.0.1:27888/status", { timeout: 1000, encoding: "utf8", windowsHide: true });
49
+ const data = JSON.parse(res);
50
+ if (data?.hub?.state === "healthy") {
51
+ return { ok: true, state: "healthy", pid: data?.pid, restarted: true };
52
+ }
53
+ } catch {}
54
+ }
55
+
56
+ return { ok: false, state: "unreachable", restart: "timeout" };
57
+ }
58
+
59
+ function checkRoute() {
60
+ const routePath = join(homedir(), ".claude", "scripts", "tfx-route.sh");
61
+ return { ok: existsSync(routePath), path: routePath };
62
+ }
63
+
64
+ function checkCli(name) {
65
+ try {
66
+ const path = execSync(`which ${name} 2>/dev/null || where ${name} 2>nul`, { encoding: "utf8", timeout: 2000, windowsHide: true }).trim();
67
+ return { ok: !!path, path };
68
+ } catch {
69
+ return { ok: false };
70
+ }
71
+ }
72
+
73
+ /** Codex auth.json의 JWT에서 chatgpt_plan_type 추출 (pro/plus/free) */
74
+ function detectCodexPlan() {
75
+ try {
76
+ const authPath = join(homedir(), ".codex", "auth.json");
77
+ if (!existsSync(authPath)) return { plan: "unknown", source: "no_auth" };
78
+ const auth = JSON.parse(readFileSync(authPath, "utf8"));
79
+ if (auth.auth_mode !== "chatgpt") return { plan: "api", source: "api_key" };
80
+ const token = auth.tokens?.id_token || auth.tokens?.access_token;
81
+ if (!token) return { plan: "unknown", source: "no_token" };
82
+ // JWT payload = 2번째 파트, base64url 디코딩
83
+ const payload = JSON.parse(Buffer.from(token.split(".")[1], "base64url").toString());
84
+ const plan = payload?.["https://api.openai.com/auth"]?.chatgpt_plan_type || "unknown";
85
+ return { plan, source: "jwt" };
86
+ } catch {
87
+ return { plan: "unknown", source: "error" };
88
+ }
89
+ }
90
+
91
+ function runPreflight() {
92
+ const result = {
93
+ timestamp: Date.now(),
94
+ hub: checkHub(),
95
+ route: checkRoute(),
96
+ codex: checkCli("codex"),
97
+ gemini: checkCli("gemini"),
98
+ codex_plan: detectCodexPlan(),
99
+ ok: false,
100
+ };
101
+ result.ok = result.hub.ok && result.route.ok;
102
+
103
+ // CLI 가용성 → available_agents (triage에서 참조)
104
+ const agents = [];
105
+ if (result.codex.ok) agents.push("codex");
106
+ if (result.gemini.ok) agents.push("gemini");
107
+ agents.push("claude"); // claude는 항상 가용
108
+ result.available_agents = agents;
109
+
110
+ return result;
111
+ }
112
+
113
+ // 캐시 읽기 (TTL 검증 포함)
114
+ export function readPreflightCache() {
115
+ try {
116
+ const data = JSON.parse(readFileSync(CACHE_FILE, "utf8"));
117
+ if (Date.now() - data.timestamp < CACHE_TTL_MS) return data;
118
+ } catch {}
119
+ return null;
120
+ }
121
+
122
+ // 메인 실행
123
+ if (process.argv[1]?.endsWith("preflight-cache.mjs")) {
124
+ const result = runPreflight();
125
+ mkdirSync(CACHE_DIR, { recursive: true });
126
+ writeFileSync(CACHE_FILE, JSON.stringify(result, null, 2));
127
+ // 간결 출력 (hook stdout)
128
+ const summary = result.ok ? "preflight: ok" : "preflight: FAIL";
129
+ const details = [];
130
+ if (!result.hub.ok) details.push("hub:" + result.hub.state);
131
+ else if (result.hub.restarted) details.push("hub:restarted");
132
+ if (!result.route.ok) details.push("route:missing");
133
+ if (result.available_agents.length === 1) details.push("agents:claude-only");
134
+ console.log(details.length ? `${summary} (${details.join(", ")})` : summary);
135
+ }
136
+
137
+ export { runPreflight, CACHE_FILE, CACHE_TTL_MS };
@@ -0,0 +1,222 @@
1
+ #!/usr/bin/env node
2
+ // remote-spawn.mjs — 로컬/원격 Claude 세션 실행 유틸리티
3
+ //
4
+ // Usage:
5
+ // node remote-spawn.mjs --local [--dir <path>] [--prompt "..."] [--handoff <file>]
6
+ // node remote-spawn.mjs --host <ssh-host> [--dir <path>] [--prompt "..."] [--handoff <file>]
7
+
8
+ import { execFileSync, spawn } from "child_process";
9
+ import { readFileSync, existsSync, statSync } from "fs";
10
+ import { resolve, join } from "path";
11
+ import { homedir, platform } from "os";
12
+
13
+ const MAX_HANDOFF_BYTES = 1 * 1024 * 1024; // 1 MB
14
+
15
+ // ── 입력 검증 ──
16
+
17
+ const SAFE_HOST_RE = /^[a-zA-Z0-9._-]+$/;
18
+ const SAFE_DIR_RE = /^[a-zA-Z0-9_.~\/:\\-]+$/;
19
+
20
+ function validateHost(host) {
21
+ if (!SAFE_HOST_RE.test(host)) {
22
+ console.error(`invalid host name: ${host}`);
23
+ process.exit(1);
24
+ }
25
+ return host;
26
+ }
27
+
28
+ function validateDir(dir) {
29
+ if (!SAFE_DIR_RE.test(dir)) {
30
+ console.error(`invalid directory path: ${dir}`);
31
+ process.exit(1);
32
+ }
33
+ return dir;
34
+ }
35
+
36
+ function shellQuote(s) {
37
+ return "'" + s.replace(/'/g, "'\\''") + "'";
38
+ }
39
+
40
+ // ── CLI 파싱 ──
41
+
42
+ function parseArgs(argv) {
43
+ const args = { host: null, dir: null, prompt: null, handoff: null, local: false };
44
+ for (let i = 2; i < argv.length; i++) {
45
+ const a = argv[i];
46
+ if (a === "--local") { args.local = true; continue; }
47
+ if (a === "--host" && argv[i + 1]) { args.host = validateHost(argv[++i]); continue; }
48
+ if (a === "--dir" && argv[i + 1]) { args.dir = validateDir(argv[++i]); continue; }
49
+ if (a === "--prompt" && argv[i + 1]) { args.prompt = argv[++i]; continue; }
50
+ if (a === "--handoff" && argv[i + 1]) { args.handoff = argv[++i]; continue; }
51
+ // 미지정 인자는 prompt로 처리
52
+ if (!args.prompt) args.prompt = a;
53
+ }
54
+ return args;
55
+ }
56
+
57
+ // ── Claude 실행 경로 감지 ──
58
+
59
+ function detectClaudePath() {
60
+ // 1. 환경변수 오버라이드
61
+ if (process.env.CLAUDE_BIN_PATH) return process.env.CLAUDE_BIN_PATH;
62
+
63
+ // 2. WinGet Links
64
+ const wingetPath = join(homedir(), "AppData", "Local", "Microsoft", "WinGet", "Links", "claude.exe");
65
+ if (existsSync(wingetPath)) return wingetPath;
66
+
67
+ // 3. npm global
68
+ const npmPath = join(process.env.APPDATA || "", "npm", "claude.cmd");
69
+ if (existsSync(npmPath)) return npmPath;
70
+
71
+ // 3. PATH에서 찾기
72
+ try {
73
+ const cmd = platform() === "win32" ? "where" : "which";
74
+ const result = execFileSync(cmd, ["claude"], { encoding: "utf8", timeout: 5000 }).trim();
75
+ if (result) return result.split("\n")[0].trim();
76
+ } catch { /* not found */ }
77
+
78
+ return "claude"; // fallback — PATH에 있다고 가정
79
+ }
80
+
81
+ // ── 권한 플래그 ──
82
+
83
+ function getPermissionFlag() {
84
+ if (process.env.TFX_CLAUDE_SAFE_MODE === "1") return [];
85
+ return ["--PLACEHOLDER_PERM_FLAG"];
86
+ }
87
+
88
+ // ── 핸드오프 컨텐츠 생성 ──
89
+
90
+ function buildPrompt(args) {
91
+ let content = "";
92
+
93
+ if (args.handoff) {
94
+ const handoffPath = resolve(args.handoff);
95
+ if (!existsSync(handoffPath)) {
96
+ console.error(`handoff file not found: ${handoffPath}`);
97
+ process.exit(1);
98
+ }
99
+ const size = statSync(handoffPath).size;
100
+ if (size > MAX_HANDOFF_BYTES) {
101
+ console.error(`handoff file too large: ${size} bytes (max ${MAX_HANDOFF_BYTES})`);
102
+ process.exit(1);
103
+ }
104
+ content = readFileSync(handoffPath, "utf8").trim();
105
+ }
106
+
107
+ if (args.prompt) {
108
+ content = content ? `${content}\n\n---\n\n${args.prompt}` : args.prompt;
109
+ }
110
+
111
+ return content;
112
+ }
113
+
114
+ // ── 로컬 Spawn (WT 탭) ──
115
+
116
+ function spawnLocal(args, claudePath, prompt) {
117
+ const dir = args.dir ? resolve(args.dir) : process.cwd();
118
+
119
+ if (platform() !== "win32") {
120
+ // Linux/macOS: 직접 실행
121
+ const cliArgs = [...getPermissionFlag()];
122
+ if (prompt) cliArgs.push(prompt);
123
+
124
+ const child = spawn(claudePath, cliArgs, {
125
+ cwd: dir,
126
+ stdio: "inherit",
127
+ });
128
+ child.on("exit", (code) => process.exit(code || 0));
129
+ return;
130
+ }
131
+
132
+ // Windows: wt.exe new-tab
133
+ const wtArgs = ["new-tab", "-d", dir, "--"];
134
+ const claudeForward = claudePath.replace(/\\/g, "/");
135
+
136
+ if (prompt) {
137
+ // pwsh single-quote: 내부 ' → '' 이스케이프
138
+ const psQuoted = "'" + prompt.replace(/'/g, "''") + "'";
139
+ wtArgs.push(
140
+ "pwsh", "-NoProfile", "-Command",
141
+ `& '${claudeForward}' ${getPermissionFlag().join(" ")} ${psQuoted}`,
142
+ );
143
+ } else {
144
+ wtArgs.push(claudeForward, ...getPermissionFlag());
145
+ }
146
+
147
+ try {
148
+ spawn("wt.exe", wtArgs, { detached: true, stdio: "ignore", windowsHide: false }).unref();
149
+ console.log(`spawned local Claude in WT tab → ${dir}`);
150
+ } catch (err) {
151
+ console.error("wt.exe spawn failed:", err.message);
152
+ process.exit(1);
153
+ }
154
+ }
155
+
156
+ // ── 원격 Spawn (SSH) ──
157
+
158
+ function spawnRemote(args, prompt) {
159
+ const { host } = args;
160
+ if (!host) {
161
+ console.error("--host required for remote spawn");
162
+ process.exit(1);
163
+ }
164
+
165
+ const dir = args.dir || "~";
166
+ const quotedDir = shellQuote(dir);
167
+ const permFlag = getPermissionFlag().join(" ");
168
+ const remoteCmd = prompt
169
+ ? `cd ${quotedDir} && claude ${permFlag} ${shellQuote(prompt)}`
170
+ : `cd ${quotedDir} && claude ${permFlag}`;
171
+
172
+ if (platform() === "win32") {
173
+ // WT 탭에서 SSH 세션 열기
174
+ const wtArgs = [
175
+ "new-tab", "--title", `Claude@${host}`, "--",
176
+ "ssh", "-t", "--", host, remoteCmd,
177
+ ];
178
+
179
+ try {
180
+ spawn("wt.exe", wtArgs, { detached: true, stdio: "ignore", windowsHide: false }).unref();
181
+ console.log(`spawned remote Claude → ${host}:${dir}`);
182
+ } catch (err) {
183
+ console.error("wt.exe spawn failed:", err.message);
184
+ process.exit(1);
185
+ }
186
+ } else {
187
+ // Linux/macOS: 직접 SSH
188
+ const child = spawn("ssh", ["-t", "--", host, remoteCmd], { stdio: "inherit" });
189
+ child.on("exit", (code) => process.exit(code || 0));
190
+ }
191
+ }
192
+
193
+ // ── main ──
194
+
195
+ function main() {
196
+ const args = parseArgs(process.argv);
197
+
198
+ if (!args.local && !args.host) {
199
+ console.log(`Usage:
200
+ remote-spawn --local [--dir <path>] [--prompt "task"] [--handoff <file>]
201
+ remote-spawn --host <ssh-host> [--dir <path>] [--prompt "task"] [--handoff <file>]
202
+
203
+ Options:
204
+ --local 로컬 WT 탭에서 Claude 실행
205
+ --host <name> SSH 호스트로 원격 Claude 실행
206
+ --dir <path> 작업 디렉토리 (기본: 현재 디렉토리 / ~)
207
+ --prompt "..." Claude에 전달할 첫 메시지
208
+ --handoff <file> 핸드오프 파일 경로 (prompt와 결합 가능)`);
209
+ process.exit(0);
210
+ }
211
+
212
+ const prompt = buildPrompt(args);
213
+ const claudePath = detectClaudePath();
214
+
215
+ if (args.local) {
216
+ spawnLocal(args, claudePath, prompt);
217
+ } else {
218
+ spawnRemote(args, prompt);
219
+ }
220
+ }
221
+
222
+ main();
package/scripts/setup.mjs CHANGED
@@ -30,6 +30,20 @@ function detectDevMode(root = PLUGIN_ROOT) {
30
30
  const BREADCRUMB_PATH = join(CLAUDE_DIR, "scripts", ".tfx-pkg-root");
31
31
 
32
32
  const REQUIRED_CODEX_PROFILES = [
33
+ {
34
+ name: "fast",
35
+ lines: [
36
+ 'model = "gpt-5.3-codex"',
37
+ 'model_reasoning_effort = "low"',
38
+ ],
39
+ },
40
+ {
41
+ name: "normal",
42
+ lines: [
43
+ 'model = "gpt-5.3-codex"',
44
+ 'model_reasoning_effort = "medium"',
45
+ ],
46
+ },
33
47
  {
34
48
  name: "high",
35
49
  lines: [
@@ -37,6 +51,14 @@ const REQUIRED_CODEX_PROFILES = [
37
51
  'model_reasoning_effort = "high"',
38
52
  ],
39
53
  },
54
+ {
55
+ name: "thorough",
56
+ lines: [
57
+ 'model = "gpt-5.3-codex"',
58
+ 'model_reasoning_effort = "high"',
59
+ 'model_temperature = 0.2',
60
+ ],
61
+ },
40
62
  {
41
63
  name: "xhigh",
42
64
  lines: [
@@ -51,6 +73,13 @@ const REQUIRED_CODEX_PROFILES = [
51
73
  'model_reasoning_effort = "low"',
52
74
  ],
53
75
  },
76
+ {
77
+ name: "spark_balanced",
78
+ lines: [
79
+ 'model = "gpt-5.1-codex-mini"',
80
+ 'model_reasoning_effort = "medium"',
81
+ ],
82
+ },
54
83
  ];
55
84
 
56
85
  // ── 파일 동기화 ──
@@ -186,6 +215,16 @@ const SYNC_MAP = [
186
215
  dst: join(CLAUDE_DIR, "scripts", "headless-guard-fast.sh"),
187
216
  label: "headless-guard-fast.sh",
188
217
  },
218
+ {
219
+ src: join(PLUGIN_ROOT, "scripts", "tfx-gate-activate.mjs"),
220
+ dst: join(CLAUDE_DIR, "scripts", "tfx-gate-activate.mjs"),
221
+ label: "tfx-gate-activate.mjs",
222
+ },
223
+ {
224
+ src: join(PLUGIN_ROOT, "scripts", "remote-spawn.mjs"),
225
+ dst: join(CLAUDE_DIR, "scripts", "remote-spawn.mjs"),
226
+ label: "remote-spawn.mjs",
227
+ },
189
228
  ];
190
229
 
191
230
  function getVersion(filePath) {
@@ -454,6 +493,19 @@ function applyAgentTeams(s) {
454
493
  return changed;
455
494
  }
456
495
 
496
+ /**
497
+ * Remote Control 자동 활성화.
498
+ * 모든 세션에서 remote control URL을 자동 발급하도록 설정.
499
+ * @param {object} s - settings 객체 (직접 변경)
500
+ * @returns {boolean} 변경 여부
501
+ */
502
+ function applyRemoteControl(s) {
503
+ if (s.remoteControlAtStartup === true) return false;
504
+ if (process.env.TFX_REMOTE_CONTROL !== "1" && !detectDevMode()) return false;
505
+ s.remoteControlAtStartup = true;
506
+ return true;
507
+ }
508
+
457
509
  /**
458
510
  * SessionStart + PreToolUse 훅 섹션 적용.
459
511
  * @param {object} s - settings 객체 (직접 변경)
@@ -533,6 +585,37 @@ function applyHooks(s) {
533
585
  }
534
586
  }
535
587
 
588
+ // ── PreToolUse 훅: tfx-gate-activate (Skill 감지 → A+B gate) ──
589
+ const gateScriptPath = join(CLAUDE_DIR, "scripts", "tfx-gate-activate.mjs").replace(/\\/g, "/");
590
+ const hasGateHook = s.hooks.PreToolUse.some((entry) =>
591
+ Array.isArray(entry.hooks) &&
592
+ entry.hooks.some((h) => typeof h.command === "string" && h.command.includes("tfx-gate-activate")),
593
+ );
594
+
595
+ if (!hasGateHook && existsSync(gateScriptPath.replace(/\//g, "\\"))) {
596
+ s.hooks.PreToolUse.push({
597
+ matcher: "Skill",
598
+ hooks: [
599
+ {
600
+ type: "command",
601
+ command: `node "${gateScriptPath}"`,
602
+ timeout: 2,
603
+ },
604
+ ],
605
+ });
606
+ changed = true;
607
+ } else if (hasGateHook) {
608
+ for (const entry of s.hooks.PreToolUse) {
609
+ if (!Array.isArray(entry.hooks)) continue;
610
+ for (const h of entry.hooks) {
611
+ if (typeof h.command === "string" && h.command.includes("tfx-gate-activate") && !h.command.includes(gateScriptPath)) {
612
+ h.command = `node "${gateScriptPath}"`;
613
+ changed = true;
614
+ }
615
+ }
616
+ }
617
+ }
618
+
536
619
  return changed;
537
620
  }
538
621
 
@@ -546,6 +629,7 @@ if (existsSync(settingsPath)) {
546
629
  let settingsChanged = false;
547
630
  try { if (applyStatusLine(settings)) { settingsChanged = true; synced++; } } catch {}
548
631
  try { if (applyAgentTeams(settings)) { settingsChanged = true; synced++; } } catch {}
632
+ try { if (applyRemoteControl(settings)) { settingsChanged = true; synced++; } } catch {}
549
633
  try { if (applyHooks(settings)) { settingsChanged = true; synced++; } } catch {}
550
634
 
551
635
  // 1회 쓰기
@@ -722,4 +806,3 @@ process.exit(0);
722
806
  if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
723
807
  main();
724
808
  }
725
-