triflux 3.3.0-dev.7 → 4.0.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 (91) hide show
  1. package/README.ko.md +108 -199
  2. package/README.md +108 -199
  3. package/bin/triflux.mjs +2415 -1762
  4. package/hooks/keyword-rules.json +361 -354
  5. package/hooks/pipeline-stop.mjs +5 -2
  6. package/hub/assign-callbacks.mjs +136 -136
  7. package/hub/bridge.mjs +734 -708
  8. package/hub/delegator/contracts.mjs +38 -0
  9. package/hub/delegator/index.mjs +14 -0
  10. package/hub/delegator/schema/delegator-tools.schema.json +250 -0
  11. package/hub/delegator/service.mjs +302 -0
  12. package/hub/delegator/tool-definitions.mjs +35 -0
  13. package/hub/hitl.mjs +67 -67
  14. package/hub/paths.mjs +28 -0
  15. package/hub/pipe.mjs +589 -561
  16. package/hub/pipeline/state.mjs +23 -0
  17. package/hub/public/dashboard.html +349 -0
  18. package/hub/public/tray-icon.ico +0 -0
  19. package/hub/public/tray-icon.png +0 -0
  20. package/hub/router.mjs +782 -782
  21. package/hub/schema.sql +40 -40
  22. package/hub/server.mjs +810 -637
  23. package/hub/store.mjs +706 -706
  24. package/hub/team/cli/commands/attach.mjs +37 -0
  25. package/hub/team/cli/commands/control.mjs +43 -0
  26. package/hub/team/cli/commands/debug.mjs +74 -0
  27. package/hub/team/cli/commands/focus.mjs +53 -0
  28. package/hub/team/cli/commands/interrupt.mjs +36 -0
  29. package/hub/team/cli/commands/kill.mjs +37 -0
  30. package/hub/team/cli/commands/list.mjs +24 -0
  31. package/hub/team/cli/commands/send.mjs +37 -0
  32. package/hub/team/cli/commands/start/index.mjs +87 -0
  33. package/hub/team/cli/commands/start/parse-args.mjs +32 -0
  34. package/hub/team/cli/commands/start/start-in-process.mjs +40 -0
  35. package/hub/team/cli/commands/start/start-mux.mjs +73 -0
  36. package/hub/team/cli/commands/start/start-wt.mjs +69 -0
  37. package/hub/team/cli/commands/status.mjs +87 -0
  38. package/hub/team/cli/commands/stop.mjs +31 -0
  39. package/hub/team/cli/commands/task.mjs +30 -0
  40. package/hub/team/cli/commands/tasks.mjs +13 -0
  41. package/hub/team/{cli.mjs → cli/help.mjs} +38 -99
  42. package/hub/team/cli/index.mjs +39 -0
  43. package/hub/team/cli/manifest.mjs +28 -0
  44. package/hub/team/cli/render.mjs +30 -0
  45. package/hub/team/cli/services/attach-fallback.mjs +54 -0
  46. package/hub/team/cli/services/hub-client.mjs +171 -0
  47. package/hub/team/cli/services/member-selector.mjs +30 -0
  48. package/hub/team/cli/services/native-control.mjs +115 -0
  49. package/hub/team/cli/services/runtime-mode.mjs +60 -0
  50. package/hub/team/cli/services/state-store.mjs +34 -0
  51. package/hub/team/cli/services/task-model.mjs +30 -0
  52. package/hub/team/native-supervisor.mjs +69 -63
  53. package/hub/team/native.mjs +367 -266
  54. package/hub/team/nativeProxy.mjs +217 -173
  55. package/hub/team/pane.mjs +149 -149
  56. package/hub/team/psmux.mjs +946 -946
  57. package/hub/team/session.mjs +608 -608
  58. package/hub/team/staleState.mjs +369 -299
  59. package/hub/tools.mjs +107 -107
  60. package/hub/tray.mjs +332 -0
  61. package/hub/workers/claude-worker.mjs +446 -446
  62. package/hub/workers/codex-mcp.mjs +414 -414
  63. package/hub/workers/delegator-mcp.mjs +1045 -1045
  64. package/hub/workers/factory.mjs +21 -21
  65. package/hub/workers/gemini-worker.mjs +349 -349
  66. package/hub/workers/interface.mjs +41 -41
  67. package/package.json +61 -60
  68. package/scripts/__tests__/keyword-detector.test.mjs +234 -234
  69. package/scripts/hub-ensure.mjs +102 -101
  70. package/scripts/keyword-detector.mjs +272 -272
  71. package/scripts/keyword-rules-expander.mjs +521 -521
  72. package/scripts/lib/keyword-rules.mjs +168 -168
  73. package/scripts/lib/mcp-filter.mjs +642 -642
  74. package/scripts/lib/mcp-server-catalog.mjs +118 -118
  75. package/scripts/mcp-check.mjs +126 -126
  76. package/scripts/preflight-cache.mjs +19 -0
  77. package/scripts/run.cjs +62 -62
  78. package/scripts/setup.mjs +68 -31
  79. package/scripts/test-tfx-route-no-claude-native.mjs +57 -57
  80. package/scripts/tfx-route-worker.mjs +161 -161
  81. package/scripts/tfx-route.sh +1360 -1326
  82. package/skills/tfx-auto/SKILL.md +196 -196
  83. package/skills/tfx-auto-codex/SKILL.md +77 -77
  84. package/skills/tfx-multi/SKILL.md +378 -378
  85. package/hub/team/cli-team-common.mjs +0 -348
  86. package/hub/team/cli-team-control.mjs +0 -393
  87. package/hub/team/cli-team-start.mjs +0 -516
  88. package/hub/team/cli-team-status.mjs +0 -283
  89. package/skills/auto-verify/SKILL.md +0 -145
  90. package/skills/manage-skills/SKILL.md +0 -192
  91. package/skills/verify-implementation/SKILL.md +0 -138
package/scripts/setup.mjs CHANGED
@@ -57,21 +57,21 @@ const SYNC_MAP = [
57
57
  dst: join(CLAUDE_DIR, "scripts", "tfx-route-worker.mjs"),
58
58
  label: "tfx-route-worker.mjs",
59
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", "delegator-mcp.mjs"),
67
- dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "delegator-mcp.mjs"),
68
- label: "hub/workers/delegator-mcp.mjs",
69
- },
70
- {
71
- src: join(PLUGIN_ROOT, "hub", "workers", "interface.mjs"),
72
- dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "interface.mjs"),
73
- label: "hub/workers/interface.mjs",
74
- },
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", "delegator-mcp.mjs"),
67
+ dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "delegator-mcp.mjs"),
68
+ label: "hub/workers/delegator-mcp.mjs",
69
+ },
70
+ {
71
+ src: join(PLUGIN_ROOT, "hub", "workers", "interface.mjs"),
72
+ dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "interface.mjs"),
73
+ label: "hub/workers/interface.mjs",
74
+ },
75
75
  {
76
76
  src: join(PLUGIN_ROOT, "hub", "workers", "gemini-worker.mjs"),
77
77
  dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "gemini-worker.mjs"),
@@ -377,28 +377,65 @@ if (existsSync(mcpCheck)) {
377
377
  const child = spawn(process.execPath, [mcpCheck], {
378
378
  detached: true,
379
379
  stdio: "ignore",
380
+ windowsHide: true,
380
381
  });
381
382
  child.unref(); // 부모 프로세스와 분리 — 비동기 실행
382
383
  }
383
384
 
384
- // ── Hub 헬스체크 + 자동 기동 (세션 시작 백그라운드) ──
385
- // setup 훅이 포그라운드 지연을 만들지 않도록 별도 detached 프로세스로 처리한다.
386
- const hubEnsure = join(PLUGIN_ROOT, "scripts", "hub-ensure.mjs");
387
- const isPostinstall = process.env.npm_lifecycle_event === "postinstall";
388
- const isCi = /^(1|true)$/i.test(process.env.CI || "");
389
- const disableHubAutostart = process.env.TFX_DISABLE_HUB_AUTOSTART === "1";
385
+ // ── SessionStart 자동 등록 (settings.json) ──
386
+ // .claude-plugin/ 개발 플러그인의 SessionStart 훅은 플러그인 로드 시점 문제로
387
+ // 실행되지 않을 있으므로, settings.json에 직접 등록한다.
388
+ // hub-ensure.mjs는 settings.json 훅으로만 실행 (이중 spawn 방지).
390
389
 
391
- if (!isPostinstall && !isCi && !disableHubAutostart && existsSync(hubEnsure)) {
392
- try {
393
- const child = spawn(process.execPath, [hubEnsure], {
394
- env: process.env,
395
- detached: true,
396
- stdio: "ignore",
397
- });
398
- child.unref();
399
- } catch {
400
- // best effort: 실패해도 setup 흐름은 지속
390
+ try {
391
+ let hookSettings = {};
392
+ if (existsSync(settingsPath)) {
393
+ hookSettings = JSON.parse(readFileSync(settingsPath, "utf8"));
394
+ }
395
+
396
+ if (!hookSettings.hooks) hookSettings.hooks = {};
397
+ if (!Array.isArray(hookSettings.hooks.SessionStart)) {
398
+ hookSettings.hooks.SessionStart = [];
399
+ }
400
+
401
+ const existingHooks = hookSettings.hooks.SessionStart;
402
+ const hasTrifluxHooks = existingHooks.some((entry) =>
403
+ Array.isArray(entry.hooks) &&
404
+ entry.hooks.some((h) => typeof h.command === "string" && h.command.includes("triflux")),
405
+ );
406
+
407
+ if (!hasTrifluxHooks) {
408
+ const nodePath = process.execPath.replace(/\\/g, "/");
409
+ const nodeRef = nodePath.includes(" ") ? `"${nodePath}"` : nodePath;
410
+ const pluginRoot = PLUGIN_ROOT.replace(/\\/g, "/");
411
+
412
+ const trifluxHookEntry = {
413
+ matcher: "*",
414
+ hooks: [
415
+ {
416
+ type: "command",
417
+ command: `${nodeRef} "${pluginRoot}/scripts/setup.mjs"`,
418
+ timeout: 10,
419
+ },
420
+ {
421
+ type: "command",
422
+ command: `${nodeRef} "${pluginRoot}/scripts/hub-ensure.mjs"`,
423
+ timeout: 8,
424
+ },
425
+ {
426
+ type: "command",
427
+ command: `${nodeRef} "${pluginRoot}/scripts/preflight-cache.mjs"`,
428
+ timeout: 5,
429
+ },
430
+ ],
431
+ };
432
+
433
+ hookSettings.hooks.SessionStart.push(trifluxHookEntry);
434
+ writeFileSync(settingsPath, JSON.stringify(hookSettings, null, 2) + "\n", "utf8");
435
+ synced++;
401
436
  }
437
+ } catch {
438
+ // settings.json 파싱 실패 시 무시 — 기존 설정 보존
402
439
  }
403
440
 
404
441
  // ── postinstall 배너 (npm install 시에만 출력) ──
@@ -1,57 +1,57 @@
1
- #!/usr/bin/env node
2
- import test from "node:test";
3
- import assert from "node:assert/strict";
4
- import { spawnSync } from "node:child_process";
5
- import { dirname, resolve } from "node:path";
6
- import { fileURLToPath } from "node:url";
7
-
8
- const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
9
- const PROJECT_ROOT = resolve(SCRIPT_DIR, "..");
10
-
11
- function runBash(command) {
12
- return spawnSync("bash", ["-lc", command], {
13
- cwd: PROJECT_ROOT,
14
- encoding: "utf8",
15
- env: {
16
- ...process.env,
17
- TFX_TEAM_NAME: '',
18
- TFX_TEAM_TASK_ID: '',
19
- TFX_TEAM_AGENT_NAME: '',
20
- TFX_TEAM_LEAD_NAME: '',
21
- TFX_HUB_URL: '',
22
- TMUX: '',
23
- }
24
- });
25
- }
26
-
27
- function out(result) {
28
- return `${result.stdout || ""}\n${result.stderr || ""}`;
29
- }
30
-
31
- test("gemini 모드에서는 no-claude-native 강제 치환이 적용되지 않는다", () => {
32
- const result = runBash(
33
- "TFX_CLI_MODE=gemini TFX_NO_CLAUDE_NATIVE=1 bash scripts/tfx-route.sh explore 'test-case'"
34
- );
35
-
36
- assert.equal(result.status, 0, out(result));
37
- assert.match(out(result), /ROUTE_TYPE=claude-native/, out(result));
38
- });
39
-
40
- test("auto 모드 + no-claude-native=1이면 explore가 codex로 치환된다", () => {
41
- const result = runBash(
42
- "TFX_CLI_MODE=auto TFX_NO_CLAUDE_NATIVE=1 CODEX_BIN=true bash scripts/tfx-route.sh explore 'test-case' minimal 5"
43
- );
44
-
45
- assert.equal(result.status, 0, out(result));
46
- assert.match(out(result), /TFX_NO_CLAUDE_NATIVE=1: explore -> codex/, out(result));
47
- assert.match(out(result), /type=codex|cli:\\s*codex/i, out(result));
48
- });
49
-
50
- test("TFX_NO_CLAUDE_NATIVE는 0/1 값만 허용한다", () => {
51
- const result = runBash(
52
- "TFX_NO_CLAUDE_NATIVE=2 bash scripts/tfx-route.sh explore 'test-case'"
53
- );
54
-
55
- assert.notEqual(result.status, 0, out(result));
56
- assert.match(out(result), /0 또는 1/, out(result));
57
- });
1
+ #!/usr/bin/env node
2
+ import test from "node:test";
3
+ import assert from "node:assert/strict";
4
+ import { spawnSync } from "node:child_process";
5
+ import { dirname, resolve } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
9
+ const PROJECT_ROOT = resolve(SCRIPT_DIR, "..");
10
+
11
+ function runBash(command) {
12
+ return spawnSync("bash", ["-lc", command], {
13
+ cwd: PROJECT_ROOT,
14
+ encoding: "utf8",
15
+ env: {
16
+ ...process.env,
17
+ TFX_TEAM_NAME: '',
18
+ TFX_TEAM_TASK_ID: '',
19
+ TFX_TEAM_AGENT_NAME: '',
20
+ TFX_TEAM_LEAD_NAME: '',
21
+ TFX_HUB_URL: '',
22
+ TMUX: '',
23
+ }
24
+ });
25
+ }
26
+
27
+ function out(result) {
28
+ return `${result.stdout || ""}\n${result.stderr || ""}`;
29
+ }
30
+
31
+ test("gemini 모드에서는 no-claude-native 강제 치환이 적용되지 않는다", () => {
32
+ const result = runBash(
33
+ "TFX_CLI_MODE=gemini TFX_NO_CLAUDE_NATIVE=1 bash scripts/tfx-route.sh explore 'test-case'"
34
+ );
35
+
36
+ assert.equal(result.status, 0, out(result));
37
+ assert.match(out(result), /ROUTE_TYPE=claude-native/, out(result));
38
+ });
39
+
40
+ test("auto 모드 + no-claude-native=1이면 explore가 codex로 치환된다", () => {
41
+ const result = runBash(
42
+ "TFX_CLI_MODE=auto TFX_NO_CLAUDE_NATIVE=1 CODEX_BIN=true bash scripts/tfx-route.sh explore 'test-case' minimal 5"
43
+ );
44
+
45
+ assert.equal(result.status, 0, out(result));
46
+ assert.match(out(result), /TFX_NO_CLAUDE_NATIVE=1: explore -> codex/, out(result));
47
+ assert.match(out(result), /type=codex|cli:\\s*codex/i, out(result));
48
+ });
49
+
50
+ test("TFX_NO_CLAUDE_NATIVE는 0/1 값만 허용한다", () => {
51
+ const result = runBash(
52
+ "TFX_NO_CLAUDE_NATIVE=2 bash scripts/tfx-route.sh explore 'test-case'"
53
+ );
54
+
55
+ assert.notEqual(result.status, 0, out(result));
56
+ assert.match(out(result), /0 또는 1/, out(result));
57
+ });
@@ -1,161 +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
- }
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
+ }