triflux 3.2.0-dev.8 → 3.3.0-dev.1

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 (52) hide show
  1. package/bin/triflux.mjs +1296 -1055
  2. package/hooks/hooks.json +17 -0
  3. package/hooks/keyword-rules.json +20 -4
  4. package/hooks/pipeline-stop.mjs +54 -0
  5. package/hub/bridge.mjs +517 -318
  6. package/hub/hitl.mjs +45 -31
  7. package/hub/pipe.mjs +457 -0
  8. package/hub/pipeline/index.mjs +121 -0
  9. package/hub/pipeline/state.mjs +164 -0
  10. package/hub/pipeline/transitions.mjs +114 -0
  11. package/hub/router.mjs +422 -161
  12. package/hub/schema.sql +14 -0
  13. package/hub/server.mjs +499 -424
  14. package/hub/store.mjs +388 -314
  15. package/hub/team/cli-team-common.mjs +348 -0
  16. package/hub/team/cli-team-control.mjs +393 -0
  17. package/hub/team/cli-team-start.mjs +516 -0
  18. package/hub/team/cli-team-status.mjs +269 -0
  19. package/hub/team/cli.mjs +75 -1475
  20. package/hub/team/dashboard.mjs +1 -9
  21. package/hub/team/native.mjs +190 -130
  22. package/hub/team/nativeProxy.mjs +165 -78
  23. package/hub/team/orchestrator.mjs +15 -20
  24. package/hub/team/pane.mjs +137 -103
  25. package/hub/team/psmux.mjs +506 -0
  26. package/hub/team/session.mjs +393 -330
  27. package/hub/team/shared.mjs +13 -0
  28. package/hub/team/staleState.mjs +299 -0
  29. package/hub/tools.mjs +105 -31
  30. package/hub/workers/claude-worker.mjs +446 -0
  31. package/hub/workers/codex-mcp.mjs +414 -0
  32. package/hub/workers/factory.mjs +18 -0
  33. package/hub/workers/gemini-worker.mjs +349 -0
  34. package/hub/workers/interface.mjs +41 -0
  35. package/hud/hud-qos-status.mjs +1790 -1788
  36. package/package.json +4 -1
  37. package/scripts/__tests__/keyword-detector.test.mjs +8 -8
  38. package/scripts/keyword-detector.mjs +15 -0
  39. package/scripts/lib/keyword-rules.mjs +4 -1
  40. package/scripts/preflight-cache.mjs +72 -0
  41. package/scripts/psmux-steering-prototype.sh +368 -0
  42. package/scripts/setup.mjs +136 -71
  43. package/scripts/tfx-route-worker.mjs +161 -0
  44. package/scripts/tfx-route.sh +485 -91
  45. package/skills/tfx-auto/SKILL.md +90 -564
  46. package/skills/tfx-auto-codex/SKILL.md +1 -3
  47. package/skills/tfx-codex/SKILL.md +1 -4
  48. package/skills/tfx-doctor/SKILL.md +1 -0
  49. package/skills/tfx-gemini/SKILL.md +1 -4
  50. package/skills/tfx-multi/SKILL.md +378 -0
  51. package/skills/tfx-setup/SKILL.md +1 -4
  52. package/skills/tfx-team/SKILL.md +0 -304
package/scripts/setup.mjs CHANGED
@@ -4,22 +4,29 @@
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");
17
17
 
18
18
  const REQUIRED_CODEX_PROFILES = [
19
+ {
20
+ name: "high",
21
+ lines: [
22
+ 'model = "gpt-5.4"',
23
+ 'model_reasoning_effort = "high"',
24
+ ],
25
+ },
19
26
  {
20
27
  name: "xhigh",
21
28
  lines: [
22
- 'model = "gpt-5.3-codex"',
29
+ 'model = "gpt-5.4"',
23
30
  'model_reasoning_effort = "xhigh"',
24
31
  ],
25
32
  },
@@ -45,6 +52,36 @@ const SYNC_MAP = [
45
52
  dst: join(CLAUDE_DIR, "scripts", "tfx-route-post.mjs"),
46
53
  label: "tfx-route-post.mjs",
47
54
  },
55
+ {
56
+ src: join(PLUGIN_ROOT, "scripts", "tfx-route-worker.mjs"),
57
+ dst: join(CLAUDE_DIR, "scripts", "tfx-route-worker.mjs"),
58
+ label: "tfx-route-worker.mjs",
59
+ },
60
+ {
61
+ src: join(PLUGIN_ROOT, "hub", "workers", "codex-mcp.mjs"),
62
+ dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "codex-mcp.mjs"),
63
+ label: "hub/workers/codex-mcp.mjs",
64
+ },
65
+ {
66
+ src: join(PLUGIN_ROOT, "hub", "workers", "interface.mjs"),
67
+ dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "interface.mjs"),
68
+ label: "hub/workers/interface.mjs",
69
+ },
70
+ {
71
+ src: join(PLUGIN_ROOT, "hub", "workers", "gemini-worker.mjs"),
72
+ dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "gemini-worker.mjs"),
73
+ label: "hub/workers/gemini-worker.mjs",
74
+ },
75
+ {
76
+ src: join(PLUGIN_ROOT, "hub", "workers", "claude-worker.mjs"),
77
+ dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "claude-worker.mjs"),
78
+ label: "hub/workers/claude-worker.mjs",
79
+ },
80
+ {
81
+ src: join(PLUGIN_ROOT, "hub", "workers", "factory.mjs"),
82
+ dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "factory.mjs"),
83
+ label: "hub/workers/factory.mjs",
84
+ },
48
85
  {
49
86
  src: join(PLUGIN_ROOT, "hud", "hud-qos-status.mjs"),
50
87
  dst: join(CLAUDE_DIR, "hud", "hud-qos-status.mjs"),
@@ -52,24 +89,24 @@ const SYNC_MAP = [
52
89
  },
53
90
  ];
54
91
 
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
- }
92
+ function getVersion(filePath) {
93
+ try {
94
+ const content = readFileSync(filePath, "utf8");
95
+ const match = content.match(/VERSION\s*=\s*"([^"]+)"/);
96
+ return match ? match[1] : null;
97
+ } catch {
98
+ return null;
99
+ }
100
+ }
101
+
102
+ function shouldSyncTextFile(src, dst) {
103
+ if (!existsSync(dst)) return true;
104
+ try {
105
+ return readFileSync(src, "utf8") !== readFileSync(dst, "utf8");
106
+ } catch {
107
+ return true;
108
+ }
109
+ }
73
110
 
74
111
  function escapeRegExp(value) {
75
112
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -124,13 +161,41 @@ for (const { src, dst, label } of SYNC_MAP) {
124
161
  copyFileSync(src, dst);
125
162
  try { chmodSync(dst, 0o755); } catch {}
126
163
  synced++;
127
- } else {
128
- if (shouldSyncTextFile(src, dst)) {
129
- copyFileSync(src, dst);
130
- try { chmodSync(dst, 0o755); } catch {}
131
- synced++;
132
- }
133
- }
164
+ } else {
165
+ if (shouldSyncTextFile(src, dst)) {
166
+ copyFileSync(src, dst);
167
+ try { chmodSync(dst, 0o755); } catch {}
168
+ synced++;
169
+ }
170
+ }
171
+ }
172
+
173
+ // ── Worker 의존성 동기화 (MCP SDK + transitive deps) ──
174
+
175
+ const workerNodeModules = join(CLAUDE_DIR, "scripts", "node_modules");
176
+ const mcpSdkPath = join(workerNodeModules, "@modelcontextprotocol", "sdk");
177
+ const srcNodeModules = join(PLUGIN_ROOT, "node_modules");
178
+
179
+ // native 모듈은 제외 (플랫폼 의존적, worker에서 불필요)
180
+ const SKIP_PACKAGES = new Set(["better-sqlite3", "prebuild-install", "node-abi", "node-addon-api"]);
181
+
182
+ if (!existsSync(mcpSdkPath) && existsSync(srcNodeModules)) {
183
+ try {
184
+ const { cpSync } = await import("fs");
185
+ for (const entry of readdirSync(srcNodeModules)) {
186
+ if (SKIP_PACKAGES.has(entry)) continue;
187
+
188
+ const src = join(srcNodeModules, entry);
189
+ const dst = join(workerNodeModules, entry);
190
+ if (existsSync(dst)) continue;
191
+
192
+ mkdirSync(dirname(dst), { recursive: true });
193
+ cpSync(src, dst, { recursive: true });
194
+ }
195
+ synced++;
196
+ } catch {
197
+ // best effort: 의존성 복사 실패 시 exec fallback으로 동작
198
+ }
134
199
  }
135
200
 
136
201
  // ── 스킬 동기화 ──
@@ -300,36 +365,36 @@ if (codexProfilesAdded > 0) {
300
365
  synced++;
301
366
  }
302
367
 
303
- // ── MCP 인벤토리 백그라운드 갱신 ──
304
-
305
- const mcpCheck = join(PLUGIN_ROOT, "scripts", "mcp-check.mjs");
306
- if (existsSync(mcpCheck)) {
307
- const child = spawn(process.execPath, [mcpCheck], {
368
+ // ── MCP 인벤토리 백그라운드 갱신 ──
369
+
370
+ const mcpCheck = join(PLUGIN_ROOT, "scripts", "mcp-check.mjs");
371
+ if (existsSync(mcpCheck)) {
372
+ const child = spawn(process.execPath, [mcpCheck], {
308
373
  detached: true,
309
374
  stdio: "ignore",
310
375
  });
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
- }
376
+ child.unref(); // 부모 프로세스와 분리 — 비동기 실행
377
+ }
378
+
379
+ // ── Hub 헬스체크 + 자동 기동 (세션 시작 백그라운드) ──
380
+ // setup 훅이 포그라운드 지연을 만들지 않도록 별도 detached 프로세스로 처리한다.
381
+ const hubEnsure = join(PLUGIN_ROOT, "scripts", "hub-ensure.mjs");
382
+ const isPostinstall = process.env.npm_lifecycle_event === "postinstall";
383
+ const isCi = /^(1|true)$/i.test(process.env.CI || "");
384
+ const disableHubAutostart = process.env.TFX_DISABLE_HUB_AUTOSTART === "1";
385
+
386
+ if (!isPostinstall && !isCi && !disableHubAutostart && existsSync(hubEnsure)) {
387
+ try {
388
+ const child = spawn(process.execPath, [hubEnsure], {
389
+ env: process.env,
390
+ detached: true,
391
+ stdio: "ignore",
392
+ });
393
+ child.unref();
394
+ } catch {
395
+ // best effort: 실패해도 setup 흐름은 지속
396
+ }
397
+ }
333
398
 
334
399
  // ── postinstall 배너 (npm install 시에만 출력) ──
335
400
 
@@ -357,23 +422,23 @@ ${B}╚════════════════════════
357
422
  ${G}✓${R} ${synced > 0 ? synced + " files synced" : "all files up to date"}
358
423
  ${G}✓${R} HUD statusLine → settings.json
359
424
 
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})
425
+ ${B}Commands:${R}
426
+ ${C}triflux${R} setup 파일 동기화 + HUD 설정
427
+ ${C}triflux${R} doctor CLI 진단 (Codex/Gemini 확인)
428
+ ${C}triflux${R} list 설치된 스킬 목록
429
+ ${C}triflux${R} update 최신 안정 버전으로 업데이트
430
+ ${C}triflux${R} update --dev dev 채널로 업데이트 (${D}dev 별칭 지원${R})
366
431
 
367
432
  ${B}Shortcuts:${R}
368
433
  ${C}tfx${R} triflux 축약
369
434
  ${C}tfx-setup${R} triflux setup
370
435
  ${C}tfx-doctor${R} triflux doctor
371
436
 
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 전용 모드
437
+ ${B}Skills (Claude Code):${R}
438
+ ${C}/tfx-auto${R} "작업" 자동 분류 + 병렬 실행
439
+ ${C}/tfx-auto-codex${R} "작업" Codex 리드 + Gemini 유지
440
+ ${C}/tfx-codex${R} "작업" Codex 전용 모드
441
+ ${C}/tfx-gemini${R} "작업" Gemini 전용 모드
377
442
  ${C}/tfx-setup${R} HUD 설정 + 진단
378
443
 
379
444
  ${Y}!${R} 세션 재시작 후 스킬이 활성화됩니다
@@ -0,0 +1,161 @@
1
+ #!/usr/bin/env node
2
+ // tfx-route-worker.mjs — tfx-route.sh용 subprocess worker 러너
3
+
4
+ import { readFileSync, existsSync } from 'node:fs';
5
+ import { dirname, resolve } from 'node:path';
6
+ import { fileURLToPath, pathToFileURL } from 'node:url';
7
+
8
+ const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
9
+ const FACTORY_CANDIDATES = [
10
+ resolve(SCRIPT_DIR, '../hub/workers/factory.mjs'),
11
+ resolve(SCRIPT_DIR, './hub/workers/factory.mjs'),
12
+ ];
13
+
14
+ // MCP transport 실패 시 tfx-route.sh가 exec fallback을 수행할 수 있도록
15
+ // CODEX_MCP_TRANSPORT_EXIT_CODE(70)으로 종료한다.
16
+ const MCP_TRANSPORT_EXIT_CODE = 70;
17
+
18
+ let createWorker = null;
19
+
20
+ for (const candidate of FACTORY_CANDIDATES) {
21
+ if (!existsSync(candidate)) continue;
22
+ try {
23
+ ({ createWorker } = await import(pathToFileURL(candidate).href));
24
+ } catch (err) {
25
+ // 의존성 누락 (예: @modelcontextprotocol/sdk) → fallback 가능하도록 exit 70
26
+ if (err.code === 'ERR_MODULE_NOT_FOUND') {
27
+ process.stderr.write(`[tfx-route-worker] 모듈 로드 실패: ${err.message}\n`);
28
+ process.exit(MCP_TRANSPORT_EXIT_CODE);
29
+ }
30
+ throw err;
31
+ }
32
+ break;
33
+ }
34
+
35
+ if (!createWorker) {
36
+ process.stderr.write('[tfx-route-worker] worker factory를 찾지 못했습니다.\n');
37
+ process.exit(MCP_TRANSPORT_EXIT_CODE);
38
+ }
39
+
40
+ function parseArgs(argv) {
41
+ const args = {
42
+ allowedMcpServerNames: [],
43
+ mcpConfig: [],
44
+ };
45
+
46
+ for (let index = 0; index < argv.length; index += 1) {
47
+ const token = argv[index];
48
+ const next = argv[index + 1];
49
+
50
+ switch (token) {
51
+ case '--type':
52
+ args.type = next;
53
+ index += 1;
54
+ break;
55
+ case '--command':
56
+ args.command = next;
57
+ index += 1;
58
+ break;
59
+ case '--command-args-json':
60
+ args.commandArgsJson = next;
61
+ index += 1;
62
+ break;
63
+ case '--model':
64
+ args.model = next;
65
+ index += 1;
66
+ break;
67
+ case '--timeout-ms':
68
+ args.timeoutMs = Number(next);
69
+ index += 1;
70
+ break;
71
+ case '--approval-mode':
72
+ args.approvalMode = next;
73
+ index += 1;
74
+ break;
75
+ case '--permission-mode':
76
+ args.permissionMode = next;
77
+ index += 1;
78
+ break;
79
+ case '--allow-dangerously-skip-permissions':
80
+ args.allowDangerouslySkipPermissions = true;
81
+ break;
82
+ case '--allowed-mcp-server-name':
83
+ args.allowedMcpServerNames.push(next);
84
+ index += 1;
85
+ break;
86
+ case '--mcp-config':
87
+ args.mcpConfig.push(next);
88
+ index += 1;
89
+ break;
90
+ case '--cwd':
91
+ args.cwd = next;
92
+ index += 1;
93
+ break;
94
+ default:
95
+ throw new Error(`Unknown argument: ${token}`);
96
+ }
97
+ }
98
+
99
+ if (!args.type) {
100
+ throw new Error('--type is required');
101
+ }
102
+
103
+ return args;
104
+ }
105
+
106
+ function parseJsonArray(raw, label) {
107
+ if (!raw) return [];
108
+ try {
109
+ const parsed = JSON.parse(raw);
110
+ if (!Array.isArray(parsed)) {
111
+ throw new Error(`${label} must be a JSON array`);
112
+ }
113
+ return parsed.map((item) => String(item));
114
+ } catch (error) {
115
+ throw new Error(`${label} parse failed: ${error.message}`);
116
+ }
117
+ }
118
+
119
+ function readPromptFromStdin() {
120
+ return readFileSync(0, 'utf8');
121
+ }
122
+
123
+ function resolveDefaultMcpConfig(cwd) {
124
+ const candidate = resolve(cwd, '.mcp.json');
125
+ return existsSync(candidate) ? [candidate] : [];
126
+ }
127
+
128
+ const args = parseArgs(process.argv.slice(2));
129
+ const prompt = readPromptFromStdin();
130
+
131
+ const worker = createWorker(args.type, {
132
+ command: args.command,
133
+ commandArgs: parseJsonArray(args.commandArgsJson, '--command-args-json'),
134
+ model: args.model,
135
+ timeoutMs: args.timeoutMs,
136
+ approvalMode: args.approvalMode,
137
+ permissionMode: args.permissionMode,
138
+ allowDangerouslySkipPermissions: args.allowDangerouslySkipPermissions,
139
+ allowedMcpServerNames: args.allowedMcpServerNames,
140
+ mcpConfig: args.type === 'claude' && args.mcpConfig.length === 0
141
+ ? resolveDefaultMcpConfig(args.cwd || process.cwd())
142
+ : args.mcpConfig,
143
+ cwd: args.cwd || process.cwd(),
144
+ });
145
+
146
+ try {
147
+ const result = await worker.run(prompt);
148
+ if (result.response) {
149
+ process.stdout.write(result.response);
150
+ if (!result.response.endsWith('\n')) process.stdout.write('\n');
151
+ }
152
+ } catch (error) {
153
+ if (error.stderr) {
154
+ process.stderr.write(String(error.stderr));
155
+ if (!String(error.stderr).endsWith('\n')) process.stderr.write('\n');
156
+ }
157
+ process.stderr.write(`${error.message}\n`);
158
+ process.exitCode = error.code === 'ETIMEDOUT' ? 124 : 1;
159
+ } finally {
160
+ try { await worker.stop(); } catch {}
161
+ }