triflux 9.8.2 → 9.8.3
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 +5 -0
- package/hooks/safety-guard.mjs +1 -1
- package/hub/assign-callbacks.mjs +13 -16
- package/hub/hitl.mjs +6 -3
- package/hub/intent.mjs +2 -7
- package/hub/lib/process-utils.mjs +5 -4
- package/hub/pipe.mjs +4 -7
- package/hub/platform.mjs +186 -0
- package/hub/router.mjs +791 -791
- package/hub/server.mjs +1112 -1000
- package/hub/state.mjs +245 -0
- package/hub/store-adapter.mjs +614 -0
- package/hub/store.mjs +820 -807
- package/hub/team/headless.mjs +298 -66
- package/hub/team/nativeProxy.mjs +2 -1
- package/hub/team/psmux.mjs +3 -3
- package/hub/tray.mjs +8 -7
- package/hub/workers/claude-worker.mjs +89 -37
- package/hub/workers/codex-mcp.mjs +123 -29
- package/hub/workers/gemini-worker.mjs +81 -137
- package/hub/workers/interface.mjs +12 -0
- package/hub/workers/worker-utils.mjs +78 -0
- package/package.json +7 -1
- package/scripts/headless-guard.mjs +7 -1
- package/scripts/setup.mjs +56 -745
- package/scripts/tfx-route.sh +2 -2
- package/scripts/tmp-cleanup.mjs +34 -5
|
@@ -18,6 +18,16 @@
|
|
|
18
18
|
* @property {number} [timeoutMs] - MCP 요청 타임아웃(ms)
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* 워커 구조화 오류 메타데이터
|
|
23
|
+
* @typedef {object} WorkerErrorInfo
|
|
24
|
+
* @property {string} code - 오류 코드
|
|
25
|
+
* @property {boolean} retryable - 재시도 대상 여부
|
|
26
|
+
* @property {number} attempts - 실행 시도 횟수
|
|
27
|
+
* @property {'transient'|'auth'|'config'|'input'} category - 오류 분류
|
|
28
|
+
* @property {string} recovery - 권장 복구 가이드
|
|
29
|
+
*/
|
|
30
|
+
|
|
21
31
|
/**
|
|
22
32
|
* 워커 실행 결과
|
|
23
33
|
* @typedef {object} WorkerResult
|
|
@@ -25,6 +35,7 @@
|
|
|
25
35
|
* @property {number} exitCode - 종료 코드(0=성공)
|
|
26
36
|
* @property {string | null} [threadId] - Codex 세션 threadId
|
|
27
37
|
* @property {string | null} [sessionKey] - 내부 세션 키
|
|
38
|
+
* @property {WorkerErrorInfo} [error] - 구조화 오류 메타데이터
|
|
28
39
|
* @property {unknown} [raw] - 원본 tool call 결과
|
|
29
40
|
*/
|
|
30
41
|
|
|
@@ -38,3 +49,4 @@
|
|
|
38
49
|
* @property {string} type - 'codex' | 'gemini' | 'claude' | 'delegator'
|
|
39
50
|
*/
|
|
40
51
|
|
|
52
|
+
export const WORKER_TYPES = Object.freeze(['codex', 'gemini', 'claude', 'delegator']);
|
|
@@ -24,3 +24,81 @@ export function createWorkerError(message, details = {}) {
|
|
|
24
24
|
Object.assign(error, details);
|
|
25
25
|
return error;
|
|
26
26
|
}
|
|
27
|
+
|
|
28
|
+
function sleep(delayMs) {
|
|
29
|
+
return new Promise((resolve) => {
|
|
30
|
+
const timer = setTimeout(resolve, Math.max(0, delayMs));
|
|
31
|
+
timer.unref?.();
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function withRetry(fn, opts = {}) {
|
|
36
|
+
const {
|
|
37
|
+
maxAttempts = 3,
|
|
38
|
+
baseDelayMs = 1000,
|
|
39
|
+
maxDelayMs = 15000,
|
|
40
|
+
shouldRetry = () => true,
|
|
41
|
+
} = opts;
|
|
42
|
+
|
|
43
|
+
let lastError;
|
|
44
|
+
|
|
45
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
46
|
+
try {
|
|
47
|
+
return await fn();
|
|
48
|
+
} catch (error) {
|
|
49
|
+
lastError = error;
|
|
50
|
+
if (attempt >= maxAttempts || !shouldRetry(error, attempt)) {
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const delay = Math.min(baseDelayMs * 2 ** (attempt - 1), maxDelayMs)
|
|
55
|
+
* (0.5 + Math.random() * 0.5);
|
|
56
|
+
await sleep(delay);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
throw lastError;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function appendTextFragments(value, parts) {
|
|
64
|
+
if (value == null) return;
|
|
65
|
+
|
|
66
|
+
if (typeof value === 'string') {
|
|
67
|
+
const trimmed = value.trim();
|
|
68
|
+
if (trimmed) parts.push(trimmed);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (Array.isArray(value)) {
|
|
73
|
+
for (const item of value) appendTextFragments(item, parts);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (typeof value !== 'object') return;
|
|
78
|
+
|
|
79
|
+
if (typeof value.text === 'string') appendTextFragments(value.text, parts);
|
|
80
|
+
if (typeof value.response === 'string') appendTextFragments(value.response, parts);
|
|
81
|
+
if (typeof value.result === 'string') appendTextFragments(value.result, parts);
|
|
82
|
+
if (value.content != null) appendTextFragments(value.content, parts);
|
|
83
|
+
if (value.message != null) appendTextFragments(value.message, parts);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function extractText(value) {
|
|
87
|
+
const parts = [];
|
|
88
|
+
appendTextFragments(value, parts);
|
|
89
|
+
return parts.join('\n').trim();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function terminateChild(child, killGraceMs) {
|
|
93
|
+
if (!child || child.exitCode !== null || child.killed) return;
|
|
94
|
+
|
|
95
|
+
try { child.stdin.end(); } catch {}
|
|
96
|
+
try { child.kill(); } catch {}
|
|
97
|
+
|
|
98
|
+
const timer = setTimeout(() => {
|
|
99
|
+
if (child.exitCode === null) {
|
|
100
|
+
try { child.kill('SIGKILL'); } catch {}
|
|
101
|
+
}
|
|
102
|
+
}, killGraceMs);
|
|
103
|
+
timer.unref?.();
|
|
104
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "triflux",
|
|
3
|
-
"version": "9.8.
|
|
3
|
+
"version": "9.8.3",
|
|
4
4
|
"description": "CLI-first multi-model orchestrator for Claude Code — route tasks to Codex, Gemini, and Claude",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -32,6 +32,9 @@
|
|
|
32
32
|
"setup": "node scripts/setup.mjs",
|
|
33
33
|
"preinstall": "node scripts/preinstall.mjs",
|
|
34
34
|
"postinstall": "node scripts/setup.mjs",
|
|
35
|
+
"lint": "biome check .",
|
|
36
|
+
"lint:fix": "biome check --fix .",
|
|
37
|
+
"health": "npm test && npm run lint",
|
|
35
38
|
"test": "node --test --test-force-exit --test-concurrency=1 \"tests/**/*.test.mjs\" \"scripts/__tests__/**/*.test.mjs\"",
|
|
36
39
|
"test:unit": "node --test --test-force-exit --test-concurrency=1 tests/unit/**/*.test.mjs",
|
|
37
40
|
"test:integration": "node --test --test-force-exit --test-concurrency=1 tests/integration/**/*.test.mjs",
|
|
@@ -55,6 +58,9 @@
|
|
|
55
58
|
"systray2": "^2.1.4",
|
|
56
59
|
"zod": "^4.0.0"
|
|
57
60
|
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@biomejs/biome": "^2.0.0"
|
|
63
|
+
},
|
|
58
64
|
"keywords": [
|
|
59
65
|
"claude-code",
|
|
60
66
|
"plugin",
|
|
@@ -190,7 +190,13 @@ async function main() {
|
|
|
190
190
|
}
|
|
191
191
|
|
|
192
192
|
// codex/gemini 직접 CLI 호출 → deny (인라인 TFX_ALLOW_DIRECT_CLI=1 우회 허용)
|
|
193
|
-
|
|
193
|
+
// 복합 명령(&&, ||, ;) 분리 후 각 세그먼트의 커맨드 위치만 검사 (args/quotes 안의 codex는 무시)
|
|
194
|
+
const cmdParts = cmd.split(/\s*(?:&&|\|\||;)\s*/);
|
|
195
|
+
const hasDirectCli = cmdParts.some(part => {
|
|
196
|
+
const stripped = part.replace(/^\s*(?:[\w_]+=\S+\s+)*/, "");
|
|
197
|
+
return /^\s*codex\b.*\bexec\b/i.test(stripped) || /^\s*gemini\s+(-p|--prompt)\b/i.test(stripped);
|
|
198
|
+
});
|
|
199
|
+
if (hasDirectCli) {
|
|
194
200
|
if (/\bTFX_ALLOW_DIRECT_CLI=1\b/.test(cmd)) {
|
|
195
201
|
nudge("[headless-guard] direct CLI mode (inline TFX_ALLOW_DIRECT_CLI=1)");
|
|
196
202
|
}
|