triflux 10.35.3 → 10.36.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/bin/triflux.mjs +14 -0
- package/package.json +5 -1
- package/scripts/lib/stealth-fetch.mjs +176 -0
- package/scripts/setup.mjs +96 -0
- package/skills/tfx-research/SKILL.md +7 -0
package/bin/triflux.mjs
CHANGED
|
@@ -75,6 +75,7 @@ import {
|
|
|
75
75
|
formatPsmuxUpdateGuidance,
|
|
76
76
|
probePsmuxSupport,
|
|
77
77
|
} from "../scripts/lib/psmux-info.mjs";
|
|
78
|
+
import { main as stealthFetchMain } from "../scripts/lib/stealth-fetch.mjs";
|
|
78
79
|
import {
|
|
79
80
|
buildWindowsHubAutostartCommand,
|
|
80
81
|
cleanupStaleSkills,
|
|
@@ -189,6 +190,11 @@ const CLI_COMMAND_SCHEMAS = Object.freeze({
|
|
|
189
190
|
},
|
|
190
191
|
],
|
|
191
192
|
},
|
|
193
|
+
"stealth-fetch": {
|
|
194
|
+
usage: "tfx stealth-fetch <url>",
|
|
195
|
+
description:
|
|
196
|
+
"cloakbrowser 기반 단일 URL fetch (http/https only, JSON stdout)",
|
|
197
|
+
},
|
|
192
198
|
doctor: {
|
|
193
199
|
usage:
|
|
194
200
|
"tfx doctor [--fix] [--reset] [--audit] [--diagnose] [--purge-logs] [--dynamic-routing] [--cleanup-stale-hubs --dry-run|--apply] [--cleanup-stale-tmux --prefix tfx-* --age-min N --dry-run|--apply] [--json]",
|
|
@@ -6327,6 +6333,7 @@ ${updateNotice}
|
|
|
6327
6333
|
${DIM} --reset${RESET} ${GRAY}캐시 전체 초기화${RESET}
|
|
6328
6334
|
${DIM} --json${RESET} ${GRAY}구조화된 진단 결과 JSON 출력${RESET}
|
|
6329
6335
|
${WHITE_BRIGHT}tfx auto${RESET} ${GRAY}tfx-auto 라우팅 결정 미리보기 (--cli codex|antigravity|claude)${RESET}
|
|
6336
|
+
${WHITE_BRIGHT}tfx stealth-fetch${RESET} ${GRAY}cloakbrowser 기반 URL fetch (JSON stdout)${RESET}
|
|
6330
6337
|
${WHITE_BRIGHT}tfx mcp${RESET} ${GRAY}MCP registry 관리 (list/sync/add/remove)${RESET}
|
|
6331
6338
|
${WHITE_BRIGHT}tfx update${RESET} ${GRAY}최신 안정 버전으로 업데이트${RESET}
|
|
6332
6339
|
${DIM} --dev / dev${RESET} ${GRAY}dev 태그로 업데이트${RESET}
|
|
@@ -7281,6 +7288,13 @@ async function main() {
|
|
|
7281
7288
|
enableHubAutostart: cmdArgs.includes("--enable-hub-autostart"),
|
|
7282
7289
|
});
|
|
7283
7290
|
return;
|
|
7291
|
+
case "stealth-fetch":
|
|
7292
|
+
if (cmdArgs.some(isHelpArg)) {
|
|
7293
|
+
printCommandHelp("stealth-fetch");
|
|
7294
|
+
return;
|
|
7295
|
+
}
|
|
7296
|
+
await stealthFetchMain([process.argv[0], "stealth-fetch", ...cmdArgs]);
|
|
7297
|
+
return;
|
|
7284
7298
|
case "doctor": {
|
|
7285
7299
|
if (cmdArgs.some(isHelpArg)) {
|
|
7286
7300
|
printCommandHelp("doctor");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "triflux",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.36.0",
|
|
4
4
|
"description": "CLI-first multi-model orchestrator for Claude Code — route tasks to Codex, Antigravity, and Claude",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -23,6 +23,10 @@
|
|
|
23
23
|
"@triflux/remote": "^10.25.0",
|
|
24
24
|
"tree-kill": "^1.2.2"
|
|
25
25
|
},
|
|
26
|
+
"optionalDependencies": {
|
|
27
|
+
"cloakbrowser": "^0.3.31",
|
|
28
|
+
"playwright-core": "^1.53.0"
|
|
29
|
+
},
|
|
26
30
|
"files": [
|
|
27
31
|
"bin",
|
|
28
32
|
"tui",
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SSRF boundary: stealthFetch only accepts http: and https: URLs. It does not
|
|
3
|
+
// guard private IPs, metadata hosts such as 169.254.x.x, or localhost; callers
|
|
4
|
+
// must treat the URL as already inside their trust boundary.
|
|
5
|
+
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
7
|
+
|
|
8
|
+
export class CloakBrowserUnavailableError extends Error {}
|
|
9
|
+
|
|
10
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
11
|
+
const DEFAULT_WAIT_UNTIL = "load";
|
|
12
|
+
const SUPPORTED_PLATFORMS = new Set([
|
|
13
|
+
"linux:x64",
|
|
14
|
+
"linux:arm64",
|
|
15
|
+
"darwin:x64",
|
|
16
|
+
"darwin:arm64",
|
|
17
|
+
"win32:x64",
|
|
18
|
+
]);
|
|
19
|
+
const EXIT_CODES = {
|
|
20
|
+
not_installed: 3,
|
|
21
|
+
unsupported_platform: 4,
|
|
22
|
+
runtime_error: 5,
|
|
23
|
+
blocked_scheme: 6,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function isSupportedPlatform(platform, arch) {
|
|
27
|
+
return SUPPORTED_PLATFORMS.has(`${platform}:${arch}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isModuleNotFound(error) {
|
|
31
|
+
return (
|
|
32
|
+
error?.code === "ERR_MODULE_NOT_FOUND" || error?.code === "MODULE_NOT_FOUND"
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function htmlToText(html) {
|
|
37
|
+
return String(html || "")
|
|
38
|
+
.replace(/<script\b[^>]*>[\s\S]*?<\/script>/giu, " ")
|
|
39
|
+
.replace(/<style\b[^>]*>[\s\S]*?<\/style>/giu, " ")
|
|
40
|
+
.replace(/<[^>]+>/gu, " ")
|
|
41
|
+
.replace(/ /giu, " ")
|
|
42
|
+
.replace(/&/giu, "&")
|
|
43
|
+
.replace(/</giu, "<")
|
|
44
|
+
.replace(/>/giu, ">")
|
|
45
|
+
.replace(/"/giu, '"')
|
|
46
|
+
.replace(/'/gu, "'")
|
|
47
|
+
.replace(/\s+/gu, " ")
|
|
48
|
+
.trim();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function responseStatus(response) {
|
|
52
|
+
if (!response) return null;
|
|
53
|
+
if (typeof response.status === "function") return response.status();
|
|
54
|
+
return response.status ?? null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function responseUrl(response, fallbackUrl) {
|
|
58
|
+
if (!response) return fallbackUrl;
|
|
59
|
+
if (typeof response.url === "function") return response.url();
|
|
60
|
+
return response.url ?? fallbackUrl;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function parseAllowedFetchUrl(url) {
|
|
64
|
+
let parsed;
|
|
65
|
+
try {
|
|
66
|
+
parsed = new URL(url);
|
|
67
|
+
} catch {
|
|
68
|
+
return { ok: false, scheme: null };
|
|
69
|
+
}
|
|
70
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
71
|
+
return { ok: false, scheme: parsed.protocol };
|
|
72
|
+
}
|
|
73
|
+
return { ok: true, href: parsed.href };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function closeBrowser(browser) {
|
|
77
|
+
if (!browser || typeof browser.close !== "function") return;
|
|
78
|
+
try {
|
|
79
|
+
await browser.close();
|
|
80
|
+
} catch {
|
|
81
|
+
// best effort: fetch callers only need the primary result signal
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function stealthFetch(url, opts = {}) {
|
|
86
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
87
|
+
const waitUntil = opts.waitUntil ?? DEFAULT_WAIT_UNTIL;
|
|
88
|
+
const loadCloakBrowser = opts._import ?? (() => import("cloakbrowser"));
|
|
89
|
+
const platform = opts._platform ?? process.platform;
|
|
90
|
+
const arch = opts._arch ?? process.arch;
|
|
91
|
+
|
|
92
|
+
if (!isSupportedPlatform(platform, arch)) {
|
|
93
|
+
return {
|
|
94
|
+
ok: false,
|
|
95
|
+
reason: "unsupported_platform",
|
|
96
|
+
platform,
|
|
97
|
+
arch,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const parsedUrl = parseAllowedFetchUrl(url);
|
|
102
|
+
if (!parsedUrl.ok) {
|
|
103
|
+
return {
|
|
104
|
+
ok: false,
|
|
105
|
+
reason: "blocked_scheme",
|
|
106
|
+
scheme: parsedUrl.scheme,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let cloakbrowser;
|
|
111
|
+
try {
|
|
112
|
+
cloakbrowser = await loadCloakBrowser();
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (isModuleNotFound(error)) return { ok: false, reason: "not_installed" };
|
|
115
|
+
return { ok: false, reason: "runtime_error", error: String(error) };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let browser;
|
|
119
|
+
try {
|
|
120
|
+
const launch = cloakbrowser?.launch;
|
|
121
|
+
if (typeof launch !== "function") {
|
|
122
|
+
throw new CloakBrowserUnavailableError("cloakbrowser launch unavailable");
|
|
123
|
+
}
|
|
124
|
+
browser = await launch();
|
|
125
|
+
const page = await browser.newPage();
|
|
126
|
+
const response = await page.goto(parsedUrl.href, {
|
|
127
|
+
waitUntil,
|
|
128
|
+
timeout: timeoutMs,
|
|
129
|
+
});
|
|
130
|
+
const html = await page.content();
|
|
131
|
+
return {
|
|
132
|
+
ok: true,
|
|
133
|
+
engine: "cloakbrowser",
|
|
134
|
+
url: parsedUrl.href,
|
|
135
|
+
finalUrl: responseUrl(response, parsedUrl.href),
|
|
136
|
+
status: responseStatus(response),
|
|
137
|
+
html,
|
|
138
|
+
text: htmlToText(html),
|
|
139
|
+
};
|
|
140
|
+
} catch (error) {
|
|
141
|
+
return { ok: false, reason: "runtime_error", error: String(error) };
|
|
142
|
+
} finally {
|
|
143
|
+
await closeBrowser(browser);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function exitCodeFor(result) {
|
|
148
|
+
return EXIT_CODES[result?.reason] ?? 5;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export async function main(argv = process.argv) {
|
|
152
|
+
const url = argv[2];
|
|
153
|
+
if (!url) {
|
|
154
|
+
console.error("usage: stealth-fetch <url>");
|
|
155
|
+
process.exitCode = 2;
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const result = await stealthFetch(url);
|
|
160
|
+
if (result.ok) {
|
|
161
|
+
console.log(JSON.stringify(result));
|
|
162
|
+
process.exitCode = 0;
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
console.error(`[stealth-fetch] 폴백: ${result.reason}`);
|
|
167
|
+
console.log(JSON.stringify(result));
|
|
168
|
+
process.exitCode = exitCodeFor(result);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (
|
|
172
|
+
process.argv[1] &&
|
|
173
|
+
import.meta.url === pathToFileURL(process.argv[1]).href
|
|
174
|
+
) {
|
|
175
|
+
await main();
|
|
176
|
+
}
|
package/scripts/setup.mjs
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
unlinkSync,
|
|
19
19
|
writeFileSync,
|
|
20
20
|
} from "fs";
|
|
21
|
+
import { createRequire } from "module";
|
|
21
22
|
import { homedir } from "os";
|
|
22
23
|
import { dirname, join, relative, resolve } from "path";
|
|
23
24
|
import { fileURLToPath } from "url";
|
|
@@ -32,6 +33,7 @@ import { addPluginRootFallbackToCommand } from "./lib/doctor-env-checks.mjs";
|
|
|
32
33
|
import { cleanupTmpFiles } from "./tmp-cleanup.mjs";
|
|
33
34
|
|
|
34
35
|
const PLUGIN_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
36
|
+
const require = createRequire(import.meta.url);
|
|
35
37
|
// Windows 에서 os.homedir() 가 USERPROFILE 만 보고 process.env.HOME swap 을
|
|
36
38
|
// 무시하기 때문에, integration test 가 fixture 격리한 spawn child 에서도
|
|
37
39
|
// production ~/.codex/config.toml 을 건드릴 수 있다. (#193 회귀)
|
|
@@ -698,6 +700,90 @@ function isProtectedCodexConfigMutationEnv(env = process.env) {
|
|
|
698
700
|
);
|
|
699
701
|
}
|
|
700
702
|
|
|
703
|
+
function isProtectedCloakBrowserInstallEnv(env = process.env) {
|
|
704
|
+
return (
|
|
705
|
+
env.NODE_ENV === "test" ||
|
|
706
|
+
env.CI === "true" ||
|
|
707
|
+
env.TFX_TEST === "1" ||
|
|
708
|
+
Boolean(env.TRIFLUX_TEST_HOME) ||
|
|
709
|
+
Boolean(env.TEST_LOCK_PID) ||
|
|
710
|
+
Boolean(env.NODE_TEST_CONTEXT) ||
|
|
711
|
+
Boolean(env.NODE_TEST_WORKER_ID) ||
|
|
712
|
+
env.TFX_SKIP_CLOAKBROWSER_SETUP === "1"
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function isCloakBrowserSupportedPlatform(platform, arch) {
|
|
717
|
+
return (
|
|
718
|
+
(platform === "linux" && (arch === "x64" || arch === "arm64")) ||
|
|
719
|
+
(platform === "darwin" && (arch === "x64" || arch === "arm64")) ||
|
|
720
|
+
(platform === "win32" && arch === "x64")
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function ensureCloakBrowser({
|
|
725
|
+
env = process.env,
|
|
726
|
+
platform = process.platform,
|
|
727
|
+
arch = process.arch,
|
|
728
|
+
requireResolve = require.resolve,
|
|
729
|
+
execFileSyncFn = execFileSync,
|
|
730
|
+
warn = (message) => console.warn(message),
|
|
731
|
+
} = {}) {
|
|
732
|
+
if (isProtectedCloakBrowserInstallEnv(env)) {
|
|
733
|
+
return {
|
|
734
|
+
ok: true,
|
|
735
|
+
installed: false,
|
|
736
|
+
skipped: true,
|
|
737
|
+
reason: "protected-env",
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
if (!isCloakBrowserSupportedPlatform(platform, arch)) {
|
|
742
|
+
warn(
|
|
743
|
+
`[setup] cloakbrowser optional setup skipped: unsupported_platform (${platform}/${arch})`,
|
|
744
|
+
);
|
|
745
|
+
return {
|
|
746
|
+
ok: true,
|
|
747
|
+
installed: false,
|
|
748
|
+
skipped: true,
|
|
749
|
+
reason: "unsupported_platform",
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
try {
|
|
754
|
+
requireResolve("cloakbrowser", { paths: [PLUGIN_ROOT] });
|
|
755
|
+
return { ok: true, installed: true, skipped: false, reason: "installed" };
|
|
756
|
+
} catch {
|
|
757
|
+
// CloakBrowser downloads its browser binary lazily on first launch; there is
|
|
758
|
+
// no separate browser install command to run here.
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
try {
|
|
762
|
+
execFileSyncFn(
|
|
763
|
+
"npm",
|
|
764
|
+
["install", "--no-save", "cloakbrowser", "playwright-core"],
|
|
765
|
+
{
|
|
766
|
+
cwd: PLUGIN_ROOT,
|
|
767
|
+
stdio: "ignore",
|
|
768
|
+
env,
|
|
769
|
+
timeout: 120_000,
|
|
770
|
+
},
|
|
771
|
+
);
|
|
772
|
+
return { ok: true, installed: true, skipped: false, reason: "installed" };
|
|
773
|
+
} catch (error) {
|
|
774
|
+
warn(
|
|
775
|
+
`[setup] cloakbrowser optional setup failed: ${_normalizeErrorMessage(error)}`,
|
|
776
|
+
);
|
|
777
|
+
return {
|
|
778
|
+
ok: false,
|
|
779
|
+
installed: false,
|
|
780
|
+
skipped: false,
|
|
781
|
+
reason: "install_failed",
|
|
782
|
+
error: _normalizeErrorMessage(error),
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
701
787
|
/**
|
|
702
788
|
* Codex config.json에 tfx-hub MCP 서버 엔트리를 보장한다.
|
|
703
789
|
* @param {{ mcpUrl: string, createIfMissing?: boolean, enabled?: boolean }} opts
|
|
@@ -1404,6 +1490,7 @@ export {
|
|
|
1404
1490
|
DEPRECATED_SKILLS,
|
|
1405
1491
|
detectDevMode,
|
|
1406
1492
|
ensureAgyHooks,
|
|
1493
|
+
ensureCloakBrowser,
|
|
1407
1494
|
ensureCodexHooks,
|
|
1408
1495
|
ensureCodexHubServerConfig,
|
|
1409
1496
|
ensureCodexProfiles,
|
|
@@ -1490,6 +1577,15 @@ export async function runDeferred(stdinData) {
|
|
|
1490
1577
|
result.action === "updated" ||
|
|
1491
1578
|
result.action === "removed",
|
|
1492
1579
|
).length;
|
|
1580
|
+
const cloakBrowserResult = ensureCloakBrowser({
|
|
1581
|
+
warn: (message) => io.log(` \x1b[33m⚠\x1b[0m ${message}`),
|
|
1582
|
+
});
|
|
1583
|
+
if (
|
|
1584
|
+
cloakBrowserResult.reason === "installed" &&
|
|
1585
|
+
cloakBrowserResult.installed
|
|
1586
|
+
) {
|
|
1587
|
+
io.log(" \x1b[32m✓\x1b[0m cloakbrowser optional backend ready");
|
|
1588
|
+
}
|
|
1493
1589
|
if (pkgVersion && marker?.version === pkgVersion && !isForce) {
|
|
1494
1590
|
if (claudeRoutingChangedCount > 0) {
|
|
1495
1591
|
io.log(
|
|
@@ -123,6 +123,13 @@ AskUserQuestion:
|
|
|
123
123
|
5. URL 콘텐츠 추출
|
|
124
124
|
```
|
|
125
125
|
|
|
126
|
+
## URL 콘텐츠 수집 (stealth-first)
|
|
127
|
+
|
|
128
|
+
Quick 모드 Step 1.5 에서 `5. URL 콘텐츠 추출` 을 선택했거나 Deep 모드에서 소스 URL 본문이 필요하면:
|
|
129
|
+
1. 기본 경로로 먼저 `node scripts/lib/stealth-fetch.mjs <url>` 를 실행한다.
|
|
130
|
+
2. exit 0 이면 stdout JSON 의 `html` 또는 `text` 를 URL 본문 근거로 사용한다.
|
|
131
|
+
3. exit 3/4/5 이면 stderr/stdout 의 reason 을 확인하고 `stealth-fetch 폴백: <이유>` 1줄 경고 후 기존 폴백을 사용한다: WebFetch → Tavily extract → Exa fetch.
|
|
132
|
+
|
|
126
133
|
### Step 2: Antigravity Google Search 위임
|
|
127
134
|
|
|
128
135
|
```
|