@triflux/remote 10.50.2 → 10.52.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/cto/brief.mjs +0 -20
- package/cto/collect.mjs +1 -200
- package/cto/current.schema.json +2 -23
- package/cto/hygiene.mjs +2 -61
- package/cto/lake-root.mjs +0 -56
- package/cto/status.mjs +5 -187
- package/hub/index.mjs +0 -17
- package/hub/team/agent-map.json +0 -1
- package/hub/team/agents-row-attach.py +120 -0
- package/hub/team/agents-row.mjs +200 -0
- package/hub/team/ansi.mjs +1 -35
- package/hub/team/backend.mjs +9 -145
- package/hub/team/claude-daemon-control.mjs +1 -2
- package/hub/team/claude-session-projection.mjs +5 -73
- package/hub/team/cli/commands/attach.mjs +1 -17
- package/hub/team/cli/commands/control.mjs +12 -38
- package/hub/team/cli/commands/debug.mjs +2 -39
- package/hub/team/cli/commands/focus.mjs +5 -31
- package/hub/team/cli/commands/interrupt.mjs +6 -25
- package/hub/team/cli/commands/kill.mjs +1 -30
- package/hub/team/cli/commands/list.mjs +0 -16
- package/hub/team/cli/commands/send.mjs +6 -26
- package/hub/team/cli/commands/start/index.mjs +42 -133
- package/hub/team/cli/commands/start/parse-args.mjs +15 -40
- package/hub/team/cli/commands/start/start-headless.mjs +138 -105
- package/hub/team/cli/commands/start/start-mux.mjs +2 -7
- package/hub/team/cli/commands/status.mjs +2 -57
- package/hub/team/cli/commands/stop.mjs +14 -19
- package/hub/team/cli/help.mjs +4 -5
- package/hub/team/cli/manifest.mjs +5 -1
- package/hub/team/cli/render.mjs +0 -12
- package/hub/team/cli/services/runtime-mode.mjs +27 -98
- package/hub/team/cli/services/state-store.mjs +5 -20
- package/hub/team/handoff.mjs +3 -34
- package/hub/team/headless.mjs +108 -1296
- package/hub/team/orchestrator.mjs +4 -30
- package/hub/team/pane.mjs +6 -28
- package/hub/team/psmux.mjs +3 -34
- package/hub/team/remote-session.mjs +1 -2
- package/hub/team/session-context.mjs +1 -1
- package/hub/team/session.mjs +7 -91
- package/hub/team/tui-core.mjs +1 -1
- package/hub/team/tui-viewer.mjs +11 -58
- package/hub/team/tui.mjs +8 -306
- package/hub/team/uds-orchestrator.mjs +2 -580
- package/hub/team/wt-manager.mjs +2 -2
- package/hub/workers/factory.mjs +6 -84
- package/hub/workers/interface.mjs +1 -7
- package/hub/workers/lib/jsonrpc-core.mjs +11 -0
- package/hub/workers/lib/jsonrpc-ws-uds.mjs +5 -8
- package/package.json +3 -8
- package/scripts/lib/agent-route-policy.mjs +143 -62
- package/scripts/lib/cli-claude.mjs +0 -14
- package/scripts/lib/env-probe.mjs +2 -131
- package/scripts/lib/gemini-profiles.mjs +2 -69
- package/scripts/lib/legacy-mcp-cleanup.mjs +283 -0
- package/scripts/lib/machine-profile.mjs +3 -17
- package/scripts/lib/mcp-filter.mjs +109 -53
- package/scripts/lib/mcp-guard-engine.mjs +40 -98
- package/scripts/lib/mcp-health.mjs +2 -2
- package/scripts/lib/mcp-server-catalog.mjs +2 -2
- package/scripts/lib/psmux-info.mjs +11 -59
- package/scripts/lib/skill-template.mjs +0 -245
- package/hub/hub-lifecycle.mjs +0 -248
- package/hub/pipe.mjs +0 -825
- package/hub/promote-penalties.mjs +0 -138
- package/hub/public/dashboard.html +0 -416
- package/hub/schema.sql +0 -209
- package/hub/server.mjs +0 -3048
- package/hub/store-adapter.mjs +0 -182
- package/hub/store.mjs +0 -1736
- package/hub/team/build-worker-prompt.mjs +0 -92
- package/hub/team/check-mcp-hub.mjs +0 -49
- package/hub/team/claude-native-bridge.mjs +0 -987
- package/hub/team/cli/commands/start/start-in-process.mjs +0 -50
- package/hub/team/cli/commands/start/start-wt.mjs +0 -86
- package/hub/team/cli/services/hub-client.mjs +0 -264
- package/hub/team/cli/services/native-control.mjs +0 -145
- package/hub/team/codex-review.mjs +0 -520
- package/hub/team/conductor-mesh-bridge.mjs +0 -122
- package/hub/team/conductor-registry.mjs +0 -121
- package/hub/team/conductor.mjs +0 -1529
- package/hub/team/consensus-meta.mjs +0 -176
- package/hub/team/cto-auto-collect.mjs +0 -115
- package/hub/team/cto-pull.mjs +0 -31
- package/hub/team/daemon-pty-tmux-bridge.mjs +0 -146
- package/hub/team/dashboard.mjs +0 -322
- package/hub/team/event-log.mjs +0 -84
- package/hub/team/execution-mode.mjs +0 -324
- package/hub/team/extract-completion-payload.mjs +0 -110
- package/hub/team/headless-bridge-session.mjs +0 -17
- package/hub/team/health-probe.mjs +0 -435
- package/hub/team/interactive-native-launcher.mjs +0 -119
- package/hub/team/interactive-native-worker.mjs +0 -80
- package/hub/team/interactive-tui-transport.mjs +0 -479
- package/hub/team/launcher-template.mjs +0 -144
- package/hub/team/lead-control.mjs +0 -191
- package/hub/team/mcp-selector.mjs +0 -145
- package/hub/team/native-supervisor.mjs +0 -460
- package/hub/team/native.mjs +0 -727
- package/hub/team/recovery-store.mjs +0 -149
- package/hub/team/remote-probe.mjs +0 -292
- package/hub/team/remote-watcher.mjs +0 -506
- package/hub/team/runtime-strategy.mjs +0 -200
- package/hub/team/schemas/worker-completion.json +0 -39
- package/hub/team/sentinel-capture.mjs +0 -127
- package/hub/team/session-sync.mjs +0 -316
- package/hub/team/swarm-cli.mjs +0 -390
- package/hub/team/swarm-hypervisor.mjs +0 -2206
- package/hub/team/swarm-intent.mjs +0 -216
- package/hub/team/swarm-locks.mjs +0 -335
- package/hub/team/swarm-planner.mjs +0 -371
- package/hub/team/swarm-preflight.mjs +0 -366
- package/hub/team/swarm-reconciler.mjs +0 -242
- package/hub/team/synapse-cli.mjs +0 -458
- package/hub/team/synapse-http.mjs +0 -311
- package/hub/team/synapse-registry.mjs +0 -618
- package/hub/team/tui-remote-adapter.mjs +0 -377
- package/hub/team/tui-synapse.mjs +0 -223
- package/hub/team/worker-completion-validator.mjs +0 -79
- package/hub/team/worker-sandbox.mjs +0 -125
- package/hub/team/worktree-lifecycle.mjs +0 -639
- package/hub/team/wt-templates.json +0 -43
- package/hub/tools.mjs +0 -900
- package/hub/workers/antigravity-route-worker.mjs +0 -179
- package/hub/workers/codex-app-server-worker.mjs +0 -1067
- package/hub/workers/delegator-mcp.mjs +0 -1098
- package/hub/workers/lib/jsonrpc-stdio.mjs +0 -204
- package/scripts/lib/claudemd-scanner.mjs +0 -223
- package/scripts/lib/context.mjs +0 -67
- package/scripts/lib/dynamic-route-cli.mjs +0 -107
- package/scripts/lib/handoff.mjs +0 -187
- package/scripts/lib/logger.mjs +0 -122
- package/scripts/lib/stealth-fetch.mjs +0 -176
package/scripts/lib/handoff.mjs
DELETED
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
import { execSync } from "node:child_process";
|
|
2
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
-
import { basename, resolve } from "node:path";
|
|
4
|
-
import { findAllClaudeMdPaths } from "./claudemd-scanner.mjs";
|
|
5
|
-
|
|
6
|
-
function runCommand(command, cwd, executor = execSync) {
|
|
7
|
-
try {
|
|
8
|
-
const output = executor(command, {
|
|
9
|
-
cwd,
|
|
10
|
-
encoding: "utf8",
|
|
11
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
12
|
-
windowsHide: true,
|
|
13
|
-
});
|
|
14
|
-
return String(output || "").trimEnd();
|
|
15
|
-
} catch {
|
|
16
|
-
return "";
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function parseStatusLines(statusText) {
|
|
21
|
-
const lines = String(statusText || "")
|
|
22
|
-
.split(/\r?\n/u)
|
|
23
|
-
.filter(Boolean);
|
|
24
|
-
const changedFiles = [];
|
|
25
|
-
const status = [];
|
|
26
|
-
|
|
27
|
-
for (const line of lines) {
|
|
28
|
-
const normalized = line.trimEnd();
|
|
29
|
-
if (!normalized) continue;
|
|
30
|
-
|
|
31
|
-
const rawStatus = normalized.slice(0, 2).trim() || "??";
|
|
32
|
-
const rawPath = normalized.slice(3).trim();
|
|
33
|
-
const filePath = rawPath.includes(" -> ")
|
|
34
|
-
? rawPath.split(" -> ").at(-1)?.trim()
|
|
35
|
-
: rawPath;
|
|
36
|
-
if (!filePath) continue;
|
|
37
|
-
|
|
38
|
-
changedFiles.push(filePath);
|
|
39
|
-
status.push({ path: filePath, status: rawStatus });
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return {
|
|
43
|
-
changedFiles: Array.from(new Set(changedFiles)),
|
|
44
|
-
status,
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function normalizeDecisionLine(line) {
|
|
49
|
-
return String(line || "")
|
|
50
|
-
.trim()
|
|
51
|
-
.replace(/^[-*]\s+/u, "")
|
|
52
|
-
.trim();
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function parseDecisionFile(decisionFile) {
|
|
56
|
-
if (!decisionFile) return [];
|
|
57
|
-
const absolute = resolve(decisionFile);
|
|
58
|
-
if (!existsSync(absolute)) return [];
|
|
59
|
-
|
|
60
|
-
const raw = readFileSync(absolute, "utf8");
|
|
61
|
-
return raw
|
|
62
|
-
.split(/\r?\n/u)
|
|
63
|
-
.map(normalizeDecisionLine)
|
|
64
|
-
.filter((line) => line.length > 0);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function normalizeDecisions(decisions, decisionFile) {
|
|
68
|
-
const merged = [
|
|
69
|
-
...(Array.isArray(decisions) ? decisions : []),
|
|
70
|
-
...parseDecisionFile(decisionFile),
|
|
71
|
-
]
|
|
72
|
-
.map(normalizeDecisionLine)
|
|
73
|
-
.filter(Boolean);
|
|
74
|
-
|
|
75
|
-
return Array.from(new Set(merged));
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function formatAheadBehind(value) {
|
|
79
|
-
if (!value) return null;
|
|
80
|
-
const [behindRaw, aheadRaw] = value.split(/\s+/u);
|
|
81
|
-
const behind = Number.parseInt(behindRaw, 10);
|
|
82
|
-
const ahead = Number.parseInt(aheadRaw, 10);
|
|
83
|
-
if (!Number.isFinite(behind) || !Number.isFinite(ahead)) return null;
|
|
84
|
-
return { ahead, behind };
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export function collectHandoffContext(options = {}) {
|
|
88
|
-
const cwd = resolve(options.cwd || process.cwd());
|
|
89
|
-
const executor =
|
|
90
|
-
typeof options.commandRunner === "function"
|
|
91
|
-
? options.commandRunner
|
|
92
|
-
: execSync;
|
|
93
|
-
const target = options.target === "local" ? "local" : "remote";
|
|
94
|
-
const decisions = normalizeDecisions(options.decisions, options.decisionFile);
|
|
95
|
-
const generatedAt = options.generatedAt || new Date().toISOString();
|
|
96
|
-
const claudeMdPaths = Array.isArray(options.claudeMdPaths)
|
|
97
|
-
? options.claudeMdPaths
|
|
98
|
-
: findAllClaudeMdPaths({
|
|
99
|
-
cwd,
|
|
100
|
-
homeDir: options.homeDir,
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
const gitRoot = runCommand("git rev-parse --show-toplevel", cwd, executor);
|
|
104
|
-
const branch =
|
|
105
|
-
runCommand("git rev-parse --abbrev-ref HEAD", cwd, executor) || null;
|
|
106
|
-
const shortStatus = runCommand("git status --short", cwd, executor);
|
|
107
|
-
const diffStat = runCommand("git diff --stat --no-color", cwd, executor);
|
|
108
|
-
const upstreamRaw = runCommand(
|
|
109
|
-
"git rev-list --left-right --count @{upstream}...HEAD",
|
|
110
|
-
cwd,
|
|
111
|
-
executor,
|
|
112
|
-
);
|
|
113
|
-
const parsedStatus = parseStatusLines(shortStatus);
|
|
114
|
-
|
|
115
|
-
return {
|
|
116
|
-
generatedAt,
|
|
117
|
-
target,
|
|
118
|
-
cwd,
|
|
119
|
-
gitRoot: gitRoot || null,
|
|
120
|
-
repository: gitRoot ? basename(gitRoot) : basename(cwd),
|
|
121
|
-
branch,
|
|
122
|
-
upstream: formatAheadBehind(upstreamRaw),
|
|
123
|
-
changedFiles: parsedStatus.changedFiles,
|
|
124
|
-
fileStatus: parsedStatus.status,
|
|
125
|
-
diffStat,
|
|
126
|
-
decisions,
|
|
127
|
-
claudeMdPaths,
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export function buildHandoffPrompt(context) {
|
|
132
|
-
const safeContext = context || collectHandoffContext();
|
|
133
|
-
const branch = safeContext.branch || "unknown";
|
|
134
|
-
const upstream = safeContext.upstream
|
|
135
|
-
? `ahead ${safeContext.upstream.ahead}, behind ${safeContext.upstream.behind}`
|
|
136
|
-
: "unknown";
|
|
137
|
-
const changedFiles =
|
|
138
|
-
safeContext.changedFiles.length > 0
|
|
139
|
-
? safeContext.changedFiles.map((file) => `- ${file}`).join("\n")
|
|
140
|
-
: "- 변경 파일 없음";
|
|
141
|
-
const decisions =
|
|
142
|
-
safeContext.decisions.length > 0
|
|
143
|
-
? safeContext.decisions.map((decision) => `- ${decision}`).join("\n")
|
|
144
|
-
: "- 명시된 결정사항 없음";
|
|
145
|
-
const claudeMdList =
|
|
146
|
-
Array.isArray(safeContext.claudeMdPaths) &&
|
|
147
|
-
safeContext.claudeMdPaths.length > 0
|
|
148
|
-
? safeContext.claudeMdPaths.map((path) => `- ${path}`).join("\n")
|
|
149
|
-
: "- 자동 탐지된 CLAUDE.md 없음";
|
|
150
|
-
const diffStat = safeContext.diffStat || "(diff stat 없음)";
|
|
151
|
-
|
|
152
|
-
return [
|
|
153
|
-
"## TFX Remote Handoff",
|
|
154
|
-
`- generated_at: ${safeContext.generatedAt}`,
|
|
155
|
-
`- target: ${safeContext.target}`,
|
|
156
|
-
`- repository: ${safeContext.repository}`,
|
|
157
|
-
`- branch: ${branch} (${upstream})`,
|
|
158
|
-
`- cwd: ${safeContext.cwd}`,
|
|
159
|
-
"",
|
|
160
|
-
"### 변경 파일",
|
|
161
|
-
changedFiles,
|
|
162
|
-
"",
|
|
163
|
-
"### 변경 요약 (git diff --stat)",
|
|
164
|
-
"```",
|
|
165
|
-
diffStat,
|
|
166
|
-
"```",
|
|
167
|
-
"",
|
|
168
|
-
"### 결정사항",
|
|
169
|
-
decisions,
|
|
170
|
-
"",
|
|
171
|
-
"### CLAUDE.md 참조",
|
|
172
|
-
claudeMdList,
|
|
173
|
-
"",
|
|
174
|
-
"### 다음 세션 지시",
|
|
175
|
-
"- 위 변경사항을 먼저 검토하고 누락된 테스트를 확인하세요.",
|
|
176
|
-
"- 필요 시 CLAUDE.md 지침을 재확인한 뒤 작업을 이어가세요.",
|
|
177
|
-
"- 작업 완료 후 변경 파일과 검증 결과를 요약하세요.",
|
|
178
|
-
].join("\n");
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
export function serializeHandoff(options = {}) {
|
|
182
|
-
const context = collectHandoffContext(options);
|
|
183
|
-
return {
|
|
184
|
-
...context,
|
|
185
|
-
prompt: buildHandoffPrompt(context),
|
|
186
|
-
};
|
|
187
|
-
}
|
package/scripts/lib/logger.mjs
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Logify — triflux 구조화 로깅 설정
|
|
3
|
-
*
|
|
4
|
-
* 사용법:
|
|
5
|
-
* import { logger, createModuleLogger } from './lib/logger.mjs';
|
|
6
|
-
*
|
|
7
|
-
* // 기본 로거
|
|
8
|
-
* logger.info({ taskId: 'abc' }, 'task.started');
|
|
9
|
-
*
|
|
10
|
-
* // 모듈별 로거
|
|
11
|
-
* const log = createModuleLogger('hub');
|
|
12
|
-
* log.info({ port: 27888 }, 'server.started');
|
|
13
|
-
* log.error({ err }, 'server.error');
|
|
14
|
-
*
|
|
15
|
-
* 이벤트 네이밍: {도메인}.{액션} 형식
|
|
16
|
-
* hub.started, hub.stopped, route.started, route.completed,
|
|
17
|
-
* worker.spawned, worker.completed, worker.timeout,
|
|
18
|
-
* mcp.connected, mcp.disconnected, mcp.error,
|
|
19
|
-
* team.created, team.deleted, task.claimed, task.completed,
|
|
20
|
-
* pipe.connected, pipe.message, pipe.error,
|
|
21
|
-
* http.request, http.response, http.error
|
|
22
|
-
*
|
|
23
|
-
* 로그 레벨 가이드:
|
|
24
|
-
* debug — 개발/트러블슈팅용 (변수 값, MCP 메시지, 캐시 키)
|
|
25
|
-
* info — 정상 흐름 상태 변경 (서버 시작, 워커 완료, 팀 생성)
|
|
26
|
-
* warn — 위험 신호 (재시도 발생, 쿼타 임박, 느린 워커)
|
|
27
|
-
* error — 작업 실패 (CLI 실행 실패, MCP 연결 끊김)
|
|
28
|
-
* fatal — 프로세스 위협 (DB 연결 불가, 포트 충돌)
|
|
29
|
-
*/
|
|
30
|
-
import pino from "pino";
|
|
31
|
-
|
|
32
|
-
const isDev = process.env.NODE_ENV !== "production";
|
|
33
|
-
|
|
34
|
-
// 로그는 항상 stderr(fd 2)로 보낸다.
|
|
35
|
-
//
|
|
36
|
-
// Claude Code 훅은 stdout 을 훅의 JSON 페이로드로 파싱한다. 로그가 stdout 으로
|
|
37
|
-
// 나가면 페이로드와 한 스트림에 섞여, 훅이 돌려준 hookSpecificOutput 이 JSON 으로
|
|
38
|
-
// 해석되지 않고 그냥 본문 텍스트가 된다. Claude Code 2.1.257 부터는 stdout 이
|
|
39
|
-
// "{" 로 시작하는데 유효한 JSON 이 아니면 훅 오류로 보고하므로, 로그가 먼저 나가느냐
|
|
40
|
-
// 페이로드가 먼저 나가느냐에 따라 훅 전체가 실패로 뒤집힌다.
|
|
41
|
-
//
|
|
42
|
-
// stderr 로 보내면 Claude Code 가 verbose/transcript 에서 그대로 보여주고 stdout 은
|
|
43
|
-
// 페이로드만 남는다. CLI 관례상으로도 구조화 로그는 stderr 가 맞다.
|
|
44
|
-
const LOG_FD = 2;
|
|
45
|
-
|
|
46
|
-
export const logger = pino(
|
|
47
|
-
{
|
|
48
|
-
level: process.env.LOG_LEVEL || (isDev ? "debug" : "info"),
|
|
49
|
-
|
|
50
|
-
// 모든 로그에 포함되는 기본 필드
|
|
51
|
-
base: {
|
|
52
|
-
service: process.env.SERVICE_NAME || "triflux",
|
|
53
|
-
env: process.env.NODE_ENV || "development",
|
|
54
|
-
},
|
|
55
|
-
|
|
56
|
-
// 레벨을 대문자로 출력 (AI 파싱 용이)
|
|
57
|
-
formatters: {
|
|
58
|
-
level: (label) => ({ level: label.toUpperCase() }),
|
|
59
|
-
},
|
|
60
|
-
|
|
61
|
-
// ISO 8601 타임스탬프
|
|
62
|
-
timestamp: pino.stdTimeFunctions.isoTime,
|
|
63
|
-
|
|
64
|
-
// 민감정보 자동 필터링
|
|
65
|
-
redact: {
|
|
66
|
-
paths: [
|
|
67
|
-
"password",
|
|
68
|
-
"token",
|
|
69
|
-
"apiKey",
|
|
70
|
-
"secret",
|
|
71
|
-
"authorization",
|
|
72
|
-
"*.password",
|
|
73
|
-
"*.token",
|
|
74
|
-
"*.apiKey",
|
|
75
|
-
"*.secret",
|
|
76
|
-
"req.headers.authorization",
|
|
77
|
-
"req.headers.cookie",
|
|
78
|
-
"hubToken",
|
|
79
|
-
],
|
|
80
|
-
remove: true,
|
|
81
|
-
},
|
|
82
|
-
|
|
83
|
-
// 개발 환경: 컬러 콘솔 출력 (stderr)
|
|
84
|
-
transport: isDev
|
|
85
|
-
? {
|
|
86
|
-
target: "pino-pretty",
|
|
87
|
-
options: {
|
|
88
|
-
colorize: true,
|
|
89
|
-
translateTime: "yyyy-mm-dd HH:MM:ss",
|
|
90
|
-
ignore: "pid,hostname",
|
|
91
|
-
destination: LOG_FD,
|
|
92
|
-
},
|
|
93
|
-
}
|
|
94
|
-
: undefined,
|
|
95
|
-
},
|
|
96
|
-
// 운영 환경(transport 미사용)에서도 stdout 이 아니라 stderr 로 쓴다.
|
|
97
|
-
isDev ? undefined : pino.destination({ dest: LOG_FD, sync: false }),
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* 모듈별 Child Logger 생성.
|
|
102
|
-
* 모듈 이름이 모든 로그에 자동 포함된다.
|
|
103
|
-
*
|
|
104
|
-
* @param {string} module — 모듈 이름 (hub, route, worker, mcp, team 등)
|
|
105
|
-
* @returns {import('pino').Logger}
|
|
106
|
-
*/
|
|
107
|
-
export function createModuleLogger(module) {
|
|
108
|
-
return logger.child({ module });
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// 정상 종료 시 버퍼 flush 보장
|
|
112
|
-
process.on("uncaughtException", (err) => {
|
|
113
|
-
logger.fatal({ err }, "process.uncaught_exception");
|
|
114
|
-
logger.flush();
|
|
115
|
-
process.exit(1);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
process.on("unhandledRejection", (reason) => {
|
|
119
|
-
logger.fatal({ reason: String(reason) }, "process.unhandled_rejection");
|
|
120
|
-
logger.flush();
|
|
121
|
-
process.exit(1);
|
|
122
|
-
});
|
|
@@ -1,176 +0,0 @@
|
|
|
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
|
-
}
|