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.
- package/bin/triflux.mjs +1296 -1055
- package/hooks/hooks.json +17 -0
- package/hooks/keyword-rules.json +20 -4
- package/hooks/pipeline-stop.mjs +54 -0
- package/hub/bridge.mjs +517 -318
- package/hub/hitl.mjs +45 -31
- package/hub/pipe.mjs +457 -0
- package/hub/pipeline/index.mjs +121 -0
- package/hub/pipeline/state.mjs +164 -0
- package/hub/pipeline/transitions.mjs +114 -0
- package/hub/router.mjs +422 -161
- package/hub/schema.sql +14 -0
- package/hub/server.mjs +499 -424
- package/hub/store.mjs +388 -314
- package/hub/team/cli-team-common.mjs +348 -0
- package/hub/team/cli-team-control.mjs +393 -0
- package/hub/team/cli-team-start.mjs +516 -0
- package/hub/team/cli-team-status.mjs +269 -0
- package/hub/team/cli.mjs +75 -1475
- package/hub/team/dashboard.mjs +1 -9
- package/hub/team/native.mjs +190 -130
- package/hub/team/nativeProxy.mjs +165 -78
- package/hub/team/orchestrator.mjs +15 -20
- package/hub/team/pane.mjs +137 -103
- package/hub/team/psmux.mjs +506 -0
- package/hub/team/session.mjs +393 -330
- package/hub/team/shared.mjs +13 -0
- package/hub/team/staleState.mjs +299 -0
- package/hub/tools.mjs +105 -31
- package/hub/workers/claude-worker.mjs +446 -0
- package/hub/workers/codex-mcp.mjs +414 -0
- package/hub/workers/factory.mjs +18 -0
- package/hub/workers/gemini-worker.mjs +349 -0
- package/hub/workers/interface.mjs +41 -0
- package/hud/hud-qos-status.mjs +1790 -1788
- package/package.json +4 -1
- package/scripts/__tests__/keyword-detector.test.mjs +8 -8
- package/scripts/keyword-detector.mjs +15 -0
- package/scripts/lib/keyword-rules.mjs +4 -1
- package/scripts/preflight-cache.mjs +72 -0
- package/scripts/psmux-steering-prototype.sh +368 -0
- package/scripts/setup.mjs +136 -71
- package/scripts/tfx-route-worker.mjs +161 -0
- package/scripts/tfx-route.sh +485 -91
- package/skills/tfx-auto/SKILL.md +90 -564
- package/skills/tfx-auto-codex/SKILL.md +1 -3
- package/skills/tfx-codex/SKILL.md +1 -4
- package/skills/tfx-doctor/SKILL.md +1 -0
- package/skills/tfx-gemini/SKILL.md +1 -4
- package/skills/tfx-multi/SKILL.md +378 -0
- package/skills/tfx-setup/SKILL.md +1 -4
- 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.
|
|
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
|
+
}
|