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.
- package/README.ko.md +108 -199
- package/README.md +108 -199
- package/bin/triflux.mjs +2415 -1762
- package/hooks/keyword-rules.json +361 -354
- package/hooks/pipeline-stop.mjs +5 -2
- package/hub/assign-callbacks.mjs +136 -136
- package/hub/bridge.mjs +734 -708
- package/hub/delegator/contracts.mjs +38 -0
- package/hub/delegator/index.mjs +14 -0
- package/hub/delegator/schema/delegator-tools.schema.json +250 -0
- package/hub/delegator/service.mjs +302 -0
- package/hub/delegator/tool-definitions.mjs +35 -0
- package/hub/hitl.mjs +67 -67
- package/hub/paths.mjs +28 -0
- package/hub/pipe.mjs +589 -561
- package/hub/pipeline/state.mjs +23 -0
- package/hub/public/dashboard.html +349 -0
- package/hub/public/tray-icon.ico +0 -0
- package/hub/public/tray-icon.png +0 -0
- package/hub/router.mjs +782 -782
- package/hub/schema.sql +40 -40
- package/hub/server.mjs +810 -637
- package/hub/store.mjs +706 -706
- package/hub/team/cli/commands/attach.mjs +37 -0
- package/hub/team/cli/commands/control.mjs +43 -0
- package/hub/team/cli/commands/debug.mjs +74 -0
- package/hub/team/cli/commands/focus.mjs +53 -0
- package/hub/team/cli/commands/interrupt.mjs +36 -0
- package/hub/team/cli/commands/kill.mjs +37 -0
- package/hub/team/cli/commands/list.mjs +24 -0
- package/hub/team/cli/commands/send.mjs +37 -0
- package/hub/team/cli/commands/start/index.mjs +87 -0
- package/hub/team/cli/commands/start/parse-args.mjs +32 -0
- package/hub/team/cli/commands/start/start-in-process.mjs +40 -0
- package/hub/team/cli/commands/start/start-mux.mjs +73 -0
- package/hub/team/cli/commands/start/start-wt.mjs +69 -0
- package/hub/team/cli/commands/status.mjs +87 -0
- package/hub/team/cli/commands/stop.mjs +31 -0
- package/hub/team/cli/commands/task.mjs +30 -0
- package/hub/team/cli/commands/tasks.mjs +13 -0
- package/hub/team/{cli.mjs → cli/help.mjs} +38 -99
- package/hub/team/cli/index.mjs +39 -0
- package/hub/team/cli/manifest.mjs +28 -0
- package/hub/team/cli/render.mjs +30 -0
- package/hub/team/cli/services/attach-fallback.mjs +54 -0
- package/hub/team/cli/services/hub-client.mjs +171 -0
- package/hub/team/cli/services/member-selector.mjs +30 -0
- package/hub/team/cli/services/native-control.mjs +115 -0
- package/hub/team/cli/services/runtime-mode.mjs +60 -0
- package/hub/team/cli/services/state-store.mjs +34 -0
- package/hub/team/cli/services/task-model.mjs +30 -0
- package/hub/team/native-supervisor.mjs +69 -63
- package/hub/team/native.mjs +367 -266
- package/hub/team/nativeProxy.mjs +217 -173
- package/hub/team/pane.mjs +149 -149
- package/hub/team/psmux.mjs +946 -946
- package/hub/team/session.mjs +608 -608
- package/hub/team/staleState.mjs +369 -299
- package/hub/tools.mjs +107 -107
- package/hub/tray.mjs +332 -0
- package/hub/workers/claude-worker.mjs +446 -446
- package/hub/workers/codex-mcp.mjs +414 -414
- package/hub/workers/delegator-mcp.mjs +1045 -1045
- package/hub/workers/factory.mjs +21 -21
- package/hub/workers/gemini-worker.mjs +349 -349
- package/hub/workers/interface.mjs +41 -41
- package/package.json +61 -60
- package/scripts/__tests__/keyword-detector.test.mjs +234 -234
- package/scripts/hub-ensure.mjs +102 -101
- package/scripts/keyword-detector.mjs +272 -272
- package/scripts/keyword-rules-expander.mjs +521 -521
- package/scripts/lib/keyword-rules.mjs +168 -168
- package/scripts/lib/mcp-filter.mjs +642 -642
- package/scripts/lib/mcp-server-catalog.mjs +118 -118
- package/scripts/mcp-check.mjs +126 -126
- package/scripts/preflight-cache.mjs +19 -0
- package/scripts/run.cjs +62 -62
- package/scripts/setup.mjs +68 -31
- package/scripts/test-tfx-route-no-claude-native.mjs +57 -57
- package/scripts/tfx-route-worker.mjs +161 -161
- package/scripts/tfx-route.sh +1360 -1326
- package/skills/tfx-auto/SKILL.md +196 -196
- package/skills/tfx-auto-codex/SKILL.md +77 -77
- package/skills/tfx-multi/SKILL.md +378 -378
- package/hub/team/cli-team-common.mjs +0 -348
- package/hub/team/cli-team-control.mjs +0 -393
- package/hub/team/cli-team-start.mjs +0 -516
- package/hub/team/cli-team-status.mjs +0 -283
- package/skills/auto-verify/SKILL.md +0 -145
- package/skills/manage-skills/SKILL.md +0 -192
- 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
|
-
// ──
|
|
385
|
-
//
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
+
}
|