@triflux/core 10.0.0-alpha.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/hooks/agent-route-guard.mjs +109 -0
- package/hooks/cross-review-tracker.mjs +122 -0
- package/hooks/error-context.mjs +148 -0
- package/hooks/hook-manager.mjs +352 -0
- package/hooks/hook-orchestrator.mjs +312 -0
- package/hooks/hook-registry.json +213 -0
- package/hooks/hooks.json +89 -0
- package/hooks/keyword-rules.json +581 -0
- package/hooks/lib/resolve-root.mjs +59 -0
- package/hooks/mcp-config-watcher.mjs +85 -0
- package/hooks/pipeline-stop.mjs +76 -0
- package/hooks/safety-guard.mjs +106 -0
- package/hooks/subagent-verifier.mjs +80 -0
- package/hub/assign-callbacks.mjs +133 -0
- package/hub/bridge.mjs +799 -0
- package/hub/cli-adapter-base.mjs +192 -0
- package/hub/codex-adapter.mjs +190 -0
- package/hub/codex-compat.mjs +78 -0
- package/hub/codex-preflight.mjs +147 -0
- package/hub/delegator/contracts.mjs +37 -0
- package/hub/delegator/index.mjs +14 -0
- package/hub/delegator/schema/delegator-tools.schema.json +250 -0
- package/hub/delegator/service.mjs +307 -0
- package/hub/delegator/tool-definitions.mjs +35 -0
- package/hub/fullcycle.mjs +96 -0
- package/hub/gemini-adapter.mjs +179 -0
- package/hub/hitl.mjs +143 -0
- package/hub/intent.mjs +193 -0
- package/hub/lib/process-utils.mjs +361 -0
- package/hub/middleware/request-logger.mjs +81 -0
- package/hub/paths.mjs +30 -0
- package/hub/pipeline/gates/confidence.mjs +56 -0
- package/hub/pipeline/gates/consensus.mjs +94 -0
- package/hub/pipeline/gates/index.mjs +5 -0
- package/hub/pipeline/gates/selfcheck.mjs +82 -0
- package/hub/pipeline/index.mjs +318 -0
- package/hub/pipeline/state.mjs +191 -0
- package/hub/pipeline/transitions.mjs +124 -0
- package/hub/platform.mjs +225 -0
- package/hub/quality/deslop.mjs +253 -0
- package/hub/reflexion.mjs +372 -0
- package/hub/research.mjs +146 -0
- package/hub/router.mjs +791 -0
- package/hub/routing/complexity.mjs +166 -0
- package/hub/routing/index.mjs +117 -0
- package/hub/routing/q-learning.mjs +336 -0
- package/hub/session-fingerprint.mjs +352 -0
- package/hub/state.mjs +245 -0
- package/hub/team-bridge.mjs +25 -0
- package/hub/token-mode.mjs +224 -0
- package/hub/workers/worker-utils.mjs +104 -0
- package/hud/colors.mjs +88 -0
- package/hud/constants.mjs +81 -0
- package/hud/hud-qos-status.mjs +206 -0
- package/hud/providers/claude.mjs +309 -0
- package/hud/providers/codex.mjs +151 -0
- package/hud/providers/gemini.mjs +320 -0
- package/hud/renderers.mjs +424 -0
- package/hud/terminal.mjs +140 -0
- package/hud/utils.mjs +287 -0
- package/package.json +31 -0
- package/scripts/lib/claudemd-manager.mjs +325 -0
- package/scripts/lib/context.mjs +67 -0
- package/scripts/lib/cross-review-utils.mjs +51 -0
- package/scripts/lib/env-probe.mjs +241 -0
- package/scripts/lib/gemini-profiles.mjs +85 -0
- package/scripts/lib/hook-utils.mjs +14 -0
- package/scripts/lib/keyword-rules.mjs +166 -0
- package/scripts/lib/logger.mjs +105 -0
- package/scripts/lib/mcp-filter.mjs +739 -0
- package/scripts/lib/mcp-guard-engine.mjs +940 -0
- package/scripts/lib/mcp-manifest.mjs +79 -0
- package/scripts/lib/mcp-server-catalog.mjs +118 -0
- package/scripts/lib/psmux-info.mjs +119 -0
- package/scripts/lib/remote-spawn-transfer.mjs +196 -0
|
@@ -0,0 +1,739 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/lib/mcp-filter.mjs
|
|
3
|
+
// 역할/컨텍스트 기반 MCP 도구 노출 정책의 단일 소스.
|
|
4
|
+
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
import process from 'node:process';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
DOMAIN_TAG_KEYWORDS,
|
|
11
|
+
MCP_SERVER_TOOL_CATALOG,
|
|
12
|
+
SEARCH_SERVER_ORDER,
|
|
13
|
+
SERVER_EXPLICIT_KEYWORDS,
|
|
14
|
+
normalizeServerMetadata,
|
|
15
|
+
uniqueStrings,
|
|
16
|
+
} from './mcp-server-catalog.mjs';
|
|
17
|
+
import { readManifest, CORE_SERVERS } from './mcp-manifest.mjs';
|
|
18
|
+
|
|
19
|
+
export const KNOWN_MCP_SERVERS = Object.freeze(Object.keys(MCP_SERVER_TOOL_CATALOG));
|
|
20
|
+
|
|
21
|
+
const SEARCH_INTENT_PATTERNS = Object.freeze([
|
|
22
|
+
/\b(search|web|browse|look ?up|find|latest|recent|news|current|today|release(?: note)?s?|changelog|announcement|pricing|status|verify|fact[- ]?check)\b/i,
|
|
23
|
+
/(검색|웹|브라우즈|찾아|조회|최신|최근|뉴스|현재|오늘|릴리즈|배포|변경사항|공지|가격|상태|검증)/u,
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
const PROFILE_DEFINITIONS = Object.freeze({
|
|
27
|
+
default: Object.freeze({
|
|
28
|
+
description: '보수적 기본 프로필. 문서 조회 + 최소 검색만 허용',
|
|
29
|
+
allowedServers: Object.freeze(['context7', 'brave-search']),
|
|
30
|
+
alwaysOnServers: Object.freeze(['context7']),
|
|
31
|
+
maxSearchServers: 1,
|
|
32
|
+
allowedToolsByServer: Object.freeze({
|
|
33
|
+
context7: Object.freeze(['resolve-library-id', 'query-docs']),
|
|
34
|
+
'brave-search': Object.freeze(['brave_web_search', 'brave_news_search']),
|
|
35
|
+
}),
|
|
36
|
+
}),
|
|
37
|
+
executor: Object.freeze({
|
|
38
|
+
description: '구현 워커용. 문서/검색/브라우징 보조 MCP 허용',
|
|
39
|
+
allowedServers: Object.freeze(['context7', 'playwright', 'brave-search', 'tavily', 'exa']),
|
|
40
|
+
alwaysOnServers: Object.freeze(['context7']),
|
|
41
|
+
maxSearchServers: 2,
|
|
42
|
+
allowedToolsByServer: Object.freeze({
|
|
43
|
+
context7: Object.freeze(['resolve-library-id', 'query-docs']),
|
|
44
|
+
'brave-search': Object.freeze(['brave_web_search', 'brave_news_search']),
|
|
45
|
+
exa: Object.freeze(['web_search_exa', 'get_code_context_exa']),
|
|
46
|
+
tavily: Object.freeze(['tavily_search', 'tavily_extract']),
|
|
47
|
+
playwright: Object.freeze([
|
|
48
|
+
'browser_navigate',
|
|
49
|
+
'browser_navigate_back',
|
|
50
|
+
'browser_snapshot',
|
|
51
|
+
'browser_take_screenshot',
|
|
52
|
+
'browser_wait_for',
|
|
53
|
+
]),
|
|
54
|
+
}),
|
|
55
|
+
}),
|
|
56
|
+
designer: Object.freeze({
|
|
57
|
+
description: '디자인/UI 워커용. 브라우저 관찰 + 문서 조회 중심 MCP 허용',
|
|
58
|
+
allowedServers: Object.freeze(['context7', 'playwright', 'tavily', 'exa', 'brave-search']),
|
|
59
|
+
alwaysOnServers: Object.freeze(['context7']),
|
|
60
|
+
maxSearchServers: 2,
|
|
61
|
+
allowedToolsByServer: Object.freeze({
|
|
62
|
+
context7: Object.freeze(['resolve-library-id', 'query-docs']),
|
|
63
|
+
'brave-search': Object.freeze(['brave_web_search', 'brave_news_search']),
|
|
64
|
+
exa: Object.freeze(['web_search_exa', 'get_code_context_exa']),
|
|
65
|
+
tavily: Object.freeze(['tavily_search', 'tavily_extract']),
|
|
66
|
+
playwright: Object.freeze([
|
|
67
|
+
'browser_navigate',
|
|
68
|
+
'browser_navigate_back',
|
|
69
|
+
'browser_snapshot',
|
|
70
|
+
'browser_take_screenshot',
|
|
71
|
+
'browser_wait_for',
|
|
72
|
+
]),
|
|
73
|
+
}),
|
|
74
|
+
}),
|
|
75
|
+
analyze: Object.freeze({
|
|
76
|
+
description: '분석/설계 워커용. 추론 + 검색 MCP 허용',
|
|
77
|
+
allowedServers: Object.freeze(['context7', 'brave-search', 'tavily', 'exa', 'sequential-thinking']),
|
|
78
|
+
alwaysOnServers: Object.freeze(['context7', 'sequential-thinking']),
|
|
79
|
+
maxSearchServers: 2,
|
|
80
|
+
allowedToolsByServer: Object.freeze({
|
|
81
|
+
context7: Object.freeze(['resolve-library-id', 'query-docs']),
|
|
82
|
+
'brave-search': Object.freeze(['brave_web_search', 'brave_news_search']),
|
|
83
|
+
exa: Object.freeze(['web_search_exa', 'get_code_context_exa']),
|
|
84
|
+
tavily: Object.freeze(['tavily_search', 'tavily_extract']),
|
|
85
|
+
'sequential-thinking': Object.freeze(['sequentialthinking']),
|
|
86
|
+
}),
|
|
87
|
+
}),
|
|
88
|
+
explore: Object.freeze({
|
|
89
|
+
description: '탐색/리서치 워커용. 읽기/검색 중심 MCP만 허용',
|
|
90
|
+
allowedServers: Object.freeze(['context7', 'brave-search', 'tavily', 'exa']),
|
|
91
|
+
alwaysOnServers: Object.freeze(['context7']),
|
|
92
|
+
maxSearchServers: 2,
|
|
93
|
+
allowedToolsByServer: Object.freeze({
|
|
94
|
+
context7: Object.freeze(['resolve-library-id', 'query-docs']),
|
|
95
|
+
'brave-search': Object.freeze(['brave_web_search', 'brave_news_search']),
|
|
96
|
+
exa: Object.freeze(['web_search_exa', 'get_code_context_exa']),
|
|
97
|
+
tavily: Object.freeze(['tavily_search', 'tavily_extract']),
|
|
98
|
+
}),
|
|
99
|
+
}),
|
|
100
|
+
reviewer: Object.freeze({
|
|
101
|
+
description: '리뷰 워커용. 문서 조회 + 분석 전용 MCP만 허용',
|
|
102
|
+
allowedServers: Object.freeze(['context7', 'brave-search', 'sequential-thinking']),
|
|
103
|
+
alwaysOnServers: Object.freeze(['context7', 'sequential-thinking']),
|
|
104
|
+
maxSearchServers: 1,
|
|
105
|
+
allowedToolsByServer: Object.freeze({
|
|
106
|
+
context7: Object.freeze(['resolve-library-id', 'query-docs']),
|
|
107
|
+
'brave-search': Object.freeze(['brave_web_search']),
|
|
108
|
+
'sequential-thinking': Object.freeze(['sequentialthinking']),
|
|
109
|
+
}),
|
|
110
|
+
}),
|
|
111
|
+
writer: Object.freeze({
|
|
112
|
+
description: '문서/작성 워커용. 공식 문서와 최소 검색 MCP만 허용',
|
|
113
|
+
allowedServers: Object.freeze(['context7', 'brave-search', 'exa']),
|
|
114
|
+
alwaysOnServers: Object.freeze(['context7']),
|
|
115
|
+
maxSearchServers: 2,
|
|
116
|
+
allowedToolsByServer: Object.freeze({
|
|
117
|
+
context7: Object.freeze(['resolve-library-id', 'query-docs']),
|
|
118
|
+
'brave-search': Object.freeze(['brave_web_search', 'brave_news_search']),
|
|
119
|
+
exa: Object.freeze(['web_search_exa']),
|
|
120
|
+
}),
|
|
121
|
+
}),
|
|
122
|
+
none: Object.freeze({
|
|
123
|
+
description: '모든 선택적 MCP 서버 비활성화',
|
|
124
|
+
allowedServers: Object.freeze([]),
|
|
125
|
+
alwaysOnServers: Object.freeze([]),
|
|
126
|
+
maxSearchServers: 0,
|
|
127
|
+
allowedToolsByServer: Object.freeze({}),
|
|
128
|
+
}),
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 파이프라인 단계별 MCP 서버/도구 제한 (post-filter).
|
|
133
|
+
* role-based 프로필 위에 추가 적용. 빈 배열 = 전체 차단, 미정의 = 제한 없음.
|
|
134
|
+
*/
|
|
135
|
+
export const PHASE_OVERRIDES = Object.freeze({
|
|
136
|
+
plan: Object.freeze({
|
|
137
|
+
description: '계획 단계: 읽기 전용 탐색만 허용',
|
|
138
|
+
allowedServers: Object.freeze(['context7']),
|
|
139
|
+
blockedServers: Object.freeze(['playwright', 'tavily', 'exa']),
|
|
140
|
+
}),
|
|
141
|
+
prd: Object.freeze({
|
|
142
|
+
description: 'PRD 단계: 읽기 전용 탐색 + 문서 조회',
|
|
143
|
+
allowedServers: Object.freeze(['context7', 'brave-search']),
|
|
144
|
+
blockedServers: Object.freeze(['playwright']),
|
|
145
|
+
}),
|
|
146
|
+
exec: Object.freeze({
|
|
147
|
+
description: '실행 단계: 프로필 기반 전체 허용 (제한 없음)',
|
|
148
|
+
}),
|
|
149
|
+
verify: Object.freeze({
|
|
150
|
+
description: '검증 단계: 읽기 전용 + 분석 도구',
|
|
151
|
+
allowedServers: Object.freeze(['context7', 'brave-search', 'exa']),
|
|
152
|
+
blockedServers: Object.freeze(['playwright']),
|
|
153
|
+
}),
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
export const LEGACY_PROFILE_ALIASES = Object.freeze({
|
|
157
|
+
implement: 'executor',
|
|
158
|
+
analyze: 'analyze',
|
|
159
|
+
review: 'reviewer',
|
|
160
|
+
docs: 'writer',
|
|
161
|
+
minimal: 'default',
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
export const SUPPORTED_MCP_PROFILES = Object.freeze([
|
|
165
|
+
'auto',
|
|
166
|
+
...Object.keys(PROFILE_DEFINITIONS),
|
|
167
|
+
...Object.keys(LEGACY_PROFILE_ALIASES),
|
|
168
|
+
]);
|
|
169
|
+
|
|
170
|
+
function normalizeTaskText(taskText = '') {
|
|
171
|
+
if (typeof taskText !== 'string') return '';
|
|
172
|
+
return taskText.replace(/\s+/g, ' ').trim();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function normalizeProfileName(profile) {
|
|
176
|
+
const raw = typeof profile === 'string' && profile.trim() ? profile.trim() : 'auto';
|
|
177
|
+
if (raw === 'auto') return raw;
|
|
178
|
+
if (PROFILE_DEFINITIONS[raw]) return raw;
|
|
179
|
+
if (LEGACY_PROFILE_ALIASES[raw]) return LEGACY_PROFILE_ALIASES[raw];
|
|
180
|
+
// graceful fallback: --flag나 잘못된 프로필 → 'auto'로 폴백 (hard crash 방지)
|
|
181
|
+
if (raw.startsWith('-') || raw.startsWith('/')) return 'auto';
|
|
182
|
+
console.error(`[mcp-filter] 경고: 알 수 없는 프로필 '${raw}', 'auto'로 폴백`);
|
|
183
|
+
return 'auto';
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function resolveAutoProfile(agentType = '') {
|
|
187
|
+
switch (agentType) {
|
|
188
|
+
case 'executor':
|
|
189
|
+
case 'build-fixer':
|
|
190
|
+
case 'debugger':
|
|
191
|
+
case 'deep-executor':
|
|
192
|
+
return 'executor';
|
|
193
|
+
case 'test-engineer':
|
|
194
|
+
case 'qa-tester':
|
|
195
|
+
return 'none';
|
|
196
|
+
case 'designer':
|
|
197
|
+
return 'designer';
|
|
198
|
+
case 'architect':
|
|
199
|
+
case 'planner':
|
|
200
|
+
case 'critic':
|
|
201
|
+
case 'analyst':
|
|
202
|
+
return 'analyze';
|
|
203
|
+
case 'scientist':
|
|
204
|
+
case 'scientist-deep':
|
|
205
|
+
case 'document-specialist':
|
|
206
|
+
case 'explore':
|
|
207
|
+
return 'explore';
|
|
208
|
+
case 'code-reviewer':
|
|
209
|
+
case 'security-reviewer':
|
|
210
|
+
case 'quality-reviewer':
|
|
211
|
+
case 'verifier':
|
|
212
|
+
return 'reviewer';
|
|
213
|
+
case 'writer':
|
|
214
|
+
return 'writer';
|
|
215
|
+
default:
|
|
216
|
+
return 'default';
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function escapeRegExp(value) {
|
|
221
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function countKeywordMatches(text, keywords = []) {
|
|
225
|
+
let matches = 0;
|
|
226
|
+
for (const keyword of keywords) {
|
|
227
|
+
const source = String(keyword || '').trim();
|
|
228
|
+
if (!source) continue;
|
|
229
|
+
const pattern = /^[a-z0-9- ]+$/i.test(source)
|
|
230
|
+
? new RegExp(`\\b${escapeRegExp(source)}\\b`, 'i')
|
|
231
|
+
: new RegExp(escapeRegExp(source), 'iu');
|
|
232
|
+
if (pattern.test(text)) matches += 1;
|
|
233
|
+
}
|
|
234
|
+
return matches;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function loadInventory(inventoryFile = '') {
|
|
238
|
+
if (typeof inventoryFile !== 'string' || !inventoryFile.trim()) return null;
|
|
239
|
+
try {
|
|
240
|
+
return JSON.parse(readFileSync(inventoryFile, 'utf8'));
|
|
241
|
+
} catch {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function buildInventoryIndex(inventory = null) {
|
|
247
|
+
const index = new Map();
|
|
248
|
+
if (!inventory || typeof inventory !== 'object') return index;
|
|
249
|
+
|
|
250
|
+
for (const client of ['codex', 'gemini']) {
|
|
251
|
+
const servers = Array.isArray(inventory[client]?.servers) ? inventory[client].servers : [];
|
|
252
|
+
for (const server of servers) {
|
|
253
|
+
if (!server || typeof server.name !== 'string' || !server.name.trim()) continue;
|
|
254
|
+
const name = server.name.trim();
|
|
255
|
+
const previous = index.get(name) || {};
|
|
256
|
+
index.set(name, {
|
|
257
|
+
...previous,
|
|
258
|
+
tool_count: Number.isFinite(server.tool_count)
|
|
259
|
+
? Math.max(previous.tool_count ?? 0, Math.trunc(server.tool_count))
|
|
260
|
+
: previous.tool_count,
|
|
261
|
+
domain_tags: uniqueStrings([
|
|
262
|
+
...(Array.isArray(previous.domain_tags) ? previous.domain_tags : []),
|
|
263
|
+
...(Array.isArray(server.domain_tags) ? server.domain_tags : []),
|
|
264
|
+
]),
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return index;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function getServerMetadata(server, inventoryIndex) {
|
|
273
|
+
const inventoryMetadata = inventoryIndex.get(server) || {};
|
|
274
|
+
return normalizeServerMetadata(server, {
|
|
275
|
+
// Inventory tool_count is useful for tie-breaks, but dynamic domain tags
|
|
276
|
+
// can over-broaden role policies compared to the static catalog.
|
|
277
|
+
tool_count: inventoryMetadata.tool_count,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function scoreServer(server, taskText = '', inventoryIndex = new Map()) {
|
|
282
|
+
const normalized = normalizeTaskText(taskText);
|
|
283
|
+
const metadata = getServerMetadata(server, inventoryIndex);
|
|
284
|
+
if (!normalized) {
|
|
285
|
+
return {
|
|
286
|
+
server,
|
|
287
|
+
score: 0,
|
|
288
|
+
toolCount: metadata.tool_count,
|
|
289
|
+
matchedTags: [],
|
|
290
|
+
explicitMatch: false,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
let score = 0;
|
|
295
|
+
const matchedTags = [];
|
|
296
|
+
for (const tag of metadata.domain_tags) {
|
|
297
|
+
const matches = countKeywordMatches(normalized, DOMAIN_TAG_KEYWORDS[tag] || []);
|
|
298
|
+
if (matches > 0) {
|
|
299
|
+
matchedTags.push(tag);
|
|
300
|
+
score += matches * 2;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const explicitMatches = countKeywordMatches(normalized, SERVER_EXPLICIT_KEYWORDS[server] || []);
|
|
305
|
+
if (explicitMatches > 0) {
|
|
306
|
+
score += explicitMatches * 4;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const toolKeywords = (MCP_SERVER_TOOL_CATALOG[server] || [])
|
|
310
|
+
.flatMap((toolName) => String(toolName).split(/[_-]+/))
|
|
311
|
+
.filter((token) => token.length >= 4);
|
|
312
|
+
score += countKeywordMatches(normalized, toolKeywords);
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
server,
|
|
316
|
+
score,
|
|
317
|
+
toolCount: metadata.tool_count,
|
|
318
|
+
matchedTags,
|
|
319
|
+
explicitMatch: explicitMatches > 0,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function compareRankedServers(left, right, workerIndex, availableOrder = []) {
|
|
324
|
+
if (right.explicitMatch !== left.explicitMatch) {
|
|
325
|
+
return Number(right.explicitMatch) - Number(left.explicitMatch);
|
|
326
|
+
}
|
|
327
|
+
if (right.score !== left.score) return right.score - left.score;
|
|
328
|
+
if (left.toolCount !== right.toolCount) return left.toolCount - right.toolCount;
|
|
329
|
+
|
|
330
|
+
if (Number.isInteger(workerIndex) && workerIndex > 0 && availableOrder.length > 1) {
|
|
331
|
+
const offset = (workerIndex - 1) % availableOrder.length;
|
|
332
|
+
const rotated = availableOrder.slice(offset).concat(availableOrder.slice(0, offset));
|
|
333
|
+
return rotated.indexOf(left.server) - rotated.indexOf(right.server);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return availableOrder.indexOf(left.server) - availableOrder.indexOf(right.server);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function rankServers(servers = [], options = {}) {
|
|
340
|
+
const inventoryIndex = options.inventoryIndex instanceof Map
|
|
341
|
+
? options.inventoryIndex
|
|
342
|
+
: buildInventoryIndex(options.inventory);
|
|
343
|
+
return servers
|
|
344
|
+
.map((server) => scoreServer(server, options.taskText, inventoryIndex))
|
|
345
|
+
.sort((left, right) => compareRankedServers(left, right, options.workerIndex, servers));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function hasContextSignals(servers = [], options = {}) {
|
|
349
|
+
return rankServers(servers, options).some((server) => server.score > 0);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function inferPreferredSearchTool(taskText = '', inventoryIndex = new Map(), allowedServers = SEARCH_SERVER_ORDER) {
|
|
353
|
+
const ranked = rankServers(
|
|
354
|
+
SEARCH_SERVER_ORDER.filter((server) => allowedServers.includes(server)),
|
|
355
|
+
{ taskText, inventoryIndex },
|
|
356
|
+
);
|
|
357
|
+
return ranked.find((server) => server.score > 0)?.server || '';
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function selectContextualServers(baseServers, profile, options = {}) {
|
|
361
|
+
const taskText = normalizeTaskText(options.taskText);
|
|
362
|
+
if (!taskText || !baseServers.length) return [...baseServers];
|
|
363
|
+
|
|
364
|
+
const inventoryIndex = options.inventoryIndex instanceof Map
|
|
365
|
+
? options.inventoryIndex
|
|
366
|
+
: buildInventoryIndex(options.inventory);
|
|
367
|
+
if (!hasContextSignals(baseServers, { ...options, inventoryIndex })) return [...baseServers];
|
|
368
|
+
|
|
369
|
+
const selected = new Set(
|
|
370
|
+
(profile.alwaysOnServers || []).filter((server) => baseServers.includes(server)),
|
|
371
|
+
);
|
|
372
|
+
const wantsBrowserObservation = (
|
|
373
|
+
baseServers.includes('playwright')
|
|
374
|
+
&& /(?:browser|screenshot|layout|responsive|visual|screen|page|ui|ux|regression|캡처|스크린샷|레이아웃|반응형|화면|브라우저)/iu.test(taskText)
|
|
375
|
+
);
|
|
376
|
+
if (wantsBrowserObservation) {
|
|
377
|
+
selected.add('playwright');
|
|
378
|
+
}
|
|
379
|
+
const requestedSearchTool = typeof options.searchTool === 'string' ? options.searchTool : '';
|
|
380
|
+
|
|
381
|
+
const rankedServers = rankServers(
|
|
382
|
+
baseServers.filter((server) => !SEARCH_SERVER_ORDER.includes(server) && !selected.has(server)),
|
|
383
|
+
{ ...options, inventoryIndex },
|
|
384
|
+
);
|
|
385
|
+
for (const ranked of rankedServers) {
|
|
386
|
+
if (ranked.score > 0 || ranked.explicitMatch) {
|
|
387
|
+
selected.add(ranked.server);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const wantsSearchFallback = SEARCH_INTENT_PATTERNS.some((pattern) => {
|
|
392
|
+
pattern.lastIndex = 0;
|
|
393
|
+
return pattern.test(taskText);
|
|
394
|
+
});
|
|
395
|
+
const orderedSearchServers = resolveSearchToolOrder(
|
|
396
|
+
requestedSearchTool,
|
|
397
|
+
Number.isInteger(options.workerIndex) ? options.workerIndex : undefined,
|
|
398
|
+
baseServers.filter((server) => SEARCH_SERVER_ORDER.includes(server)),
|
|
399
|
+
taskText,
|
|
400
|
+
{ inventoryIndex },
|
|
401
|
+
);
|
|
402
|
+
const rankedSearchSet = new Set(orderedSearchServers.filter((server) => {
|
|
403
|
+
if (requestedSearchTool === server) return true;
|
|
404
|
+
const ranked = scoreServer(server, taskText, inventoryIndex);
|
|
405
|
+
return ranked.score > 0 || ranked.explicitMatch;
|
|
406
|
+
}));
|
|
407
|
+
const maxSearchServers = Number.isInteger(profile.maxSearchServers)
|
|
408
|
+
? profile.maxSearchServers
|
|
409
|
+
: orderedSearchServers.length;
|
|
410
|
+
const chosenSearchServers = (
|
|
411
|
+
rankedSearchSet.size
|
|
412
|
+
? orderedSearchServers.filter((server) => rankedSearchSet.has(server))
|
|
413
|
+
: wantsSearchFallback
|
|
414
|
+
? orderedSearchServers.slice(0, 1)
|
|
415
|
+
: []
|
|
416
|
+
).slice(0, maxSearchServers);
|
|
417
|
+
|
|
418
|
+
for (const server of chosenSearchServers) {
|
|
419
|
+
selected.add(server);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const alwaysOnServers = baseServers.filter((server) => selected.has(server) && (profile.alwaysOnServers || []).includes(server));
|
|
423
|
+
const contextualNonSearch = rankServers(
|
|
424
|
+
baseServers.filter((server) => selected.has(server) && !SEARCH_SERVER_ORDER.includes(server) && !alwaysOnServers.includes(server)),
|
|
425
|
+
{ ...options, inventoryIndex },
|
|
426
|
+
).map((entry) => entry.server);
|
|
427
|
+
const contextualSearch = orderedSearchServers.filter((server) => selected.has(server));
|
|
428
|
+
const contextualServers = uniqueStrings([
|
|
429
|
+
...alwaysOnServers,
|
|
430
|
+
...contextualNonSearch,
|
|
431
|
+
...contextualSearch,
|
|
432
|
+
]);
|
|
433
|
+
return contextualServers.length ? contextualServers : [...baseServers];
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export function resolveMcpProfile(agentType = '', requestedProfile = 'auto') {
|
|
437
|
+
const normalized = normalizeProfileName(requestedProfile);
|
|
438
|
+
return normalized === 'auto' ? resolveAutoProfile(agentType) : normalized;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export function parseAvailableServers(rawAvailableServers = '') {
|
|
442
|
+
if (Array.isArray(rawAvailableServers)) return uniqueStrings(rawAvailableServers);
|
|
443
|
+
if (typeof rawAvailableServers !== 'string' || !rawAvailableServers.trim()) return [];
|
|
444
|
+
return uniqueStrings(rawAvailableServers.split(/[,\s]+/));
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
export function resolveSearchToolOrder(searchTool = '', workerIndex, allowedServers = SEARCH_SERVER_ORDER, taskText = '', options = {}) {
|
|
448
|
+
const available = SEARCH_SERVER_ORDER.filter((tool) => allowedServers.includes(tool));
|
|
449
|
+
if (!available.length) return [];
|
|
450
|
+
|
|
451
|
+
const inventoryIndex = options.inventoryIndex instanceof Map
|
|
452
|
+
? options.inventoryIndex
|
|
453
|
+
: buildInventoryIndex(options.inventory);
|
|
454
|
+
const preferredSearchTool = searchTool && available.includes(searchTool)
|
|
455
|
+
? searchTool
|
|
456
|
+
: inferPreferredSearchTool(taskText, inventoryIndex, available);
|
|
457
|
+
|
|
458
|
+
const ranked = rankServers(available, { taskText, workerIndex, inventoryIndex }).map((entry) => entry.server);
|
|
459
|
+
if (!preferredSearchTool || !available.includes(preferredSearchTool)) {
|
|
460
|
+
return ranked;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return [preferredSearchTool, ...ranked.filter((tool) => tool !== preferredSearchTool)];
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function getProfileDefinition(resolvedProfile) {
|
|
467
|
+
return PROFILE_DEFINITIONS[resolvedProfile] || PROFILE_DEFINITIONS.default;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** 매니페스트 기반 필터. 매니페스트 없으면 전체 허용 (레거시). */
|
|
471
|
+
function applyManifestFilter(servers) {
|
|
472
|
+
const manifest = readManifest();
|
|
473
|
+
if (!manifest) return servers;
|
|
474
|
+
const enabled = new Set([...(manifest.enabled || []), ...CORE_SERVERS]);
|
|
475
|
+
return servers.filter((s) => enabled.has(s));
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
export function resolveAllowedServers(options = {}) {
|
|
479
|
+
const resolvedProfile = resolveMcpProfile(options.agentType, options.requestedProfile);
|
|
480
|
+
const profile = getProfileDefinition(resolvedProfile);
|
|
481
|
+
const availableServers = parseAvailableServers(options.availableServers);
|
|
482
|
+
const inventory = options.inventory || loadInventory(options.inventoryFile);
|
|
483
|
+
const inventoryIndex = buildInventoryIndex(inventory);
|
|
484
|
+
const baseServers = availableServers.length
|
|
485
|
+
? profile.allowedServers.filter((server) => availableServers.includes(server))
|
|
486
|
+
: [...profile.allowedServers];
|
|
487
|
+
const manifestFiltered = availableServers.length
|
|
488
|
+
? baseServers
|
|
489
|
+
: applyManifestFilter(baseServers);
|
|
490
|
+
return selectContextualServers(manifestFiltered, profile, { ...options, inventory, inventoryIndex });
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
export function buildPromptHint(options = {}) {
|
|
494
|
+
const resolvedProfile = resolveMcpProfile(options.agentType, options.requestedProfile);
|
|
495
|
+
if (resolvedProfile === 'none') return '';
|
|
496
|
+
|
|
497
|
+
const inventory = options.inventory || loadInventory(options.inventoryFile);
|
|
498
|
+
const inventoryIndex = buildInventoryIndex(inventory);
|
|
499
|
+
const allowedServers = resolveAllowedServers({ ...options, inventory, inventoryIndex });
|
|
500
|
+
const orderedTools = resolveSearchToolOrder(
|
|
501
|
+
options.searchTool,
|
|
502
|
+
Number.isInteger(options.workerIndex) ? options.workerIndex : undefined,
|
|
503
|
+
allowedServers,
|
|
504
|
+
options.taskText,
|
|
505
|
+
{ inventory, inventoryIndex },
|
|
506
|
+
);
|
|
507
|
+
const has = (server) => allowedServers.includes(server);
|
|
508
|
+
const orderedSearchHint = orderedTools.length > 1
|
|
509
|
+
? `웹 검색 우선순위: ${orderedTools.join(', ')}.`
|
|
510
|
+
: orderedTools[0]
|
|
511
|
+
? `웹 검색은 ${orderedTools[0]}를 사용하세요.`
|
|
512
|
+
: '';
|
|
513
|
+
const searchFallbackHint = orderedTools.length > 1
|
|
514
|
+
? '검색 도구 실패 시 402, 429, 432, 433, quota 에러에서 재시도하지 말고 다음 도구로 전환하세요.'
|
|
515
|
+
: '';
|
|
516
|
+
return [
|
|
517
|
+
has('context7') ? 'context7으로 관련 문서를 조회하세요.' : '',
|
|
518
|
+
has('playwright')
|
|
519
|
+
? resolvedProfile === 'designer'
|
|
520
|
+
? '화면/레이아웃 확인은 playwright를 우선 사용하세요.'
|
|
521
|
+
: '브라우저/UI 검증이 필요하면 playwright를 사용하세요.'
|
|
522
|
+
: '',
|
|
523
|
+
has('sequential-thinking') ? 'sequential-thinking으로 체계적으로 분석하세요.' : '',
|
|
524
|
+
resolvedProfile === 'reviewer' && orderedTools[0] ? `외부 근거가 더 필요하면 ${orderedTools[0]}를 사용하세요.` : '',
|
|
525
|
+
resolvedProfile !== 'reviewer' ? orderedSearchHint : '',
|
|
526
|
+
resolvedProfile !== 'reviewer' ? searchFallbackHint : '',
|
|
527
|
+
resolvedProfile === 'explore' ? '검색 깊이를 제한하고 읽기 전용 조사에 집중하세요.' : '',
|
|
528
|
+
resolvedProfile === 'writer' ? '검색 결과의 출처 URL을 함께 제시하세요.' : '',
|
|
529
|
+
].filter(Boolean).join(' ');
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
export function getGeminiAllowedServers(options = {}) {
|
|
533
|
+
return resolveAllowedServers(options);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
export function getCodexMcpConfig(options = {}) {
|
|
537
|
+
const allowedServers = new Set(resolveAllowedServers(options));
|
|
538
|
+
const resolvedProfile = resolveMcpProfile(options.agentType, options.requestedProfile);
|
|
539
|
+
// Codex에 실제 등록된 서버만 대상으로 config override 생성.
|
|
540
|
+
// 미등록 서버에 enabled=false를 보내면 "invalid transport" 에러 발생.
|
|
541
|
+
const registeredServers = parseAvailableServers(options.availableServers);
|
|
542
|
+
// Codex 0.115+: 미등록 서버에 config override를 보내면 "invalid transport" 에러.
|
|
543
|
+
// 등록 서버 정보가 없으면 override를 생성하지 않는다 (안전 기본값).
|
|
544
|
+
if (registeredServers.length === 0) {
|
|
545
|
+
return { mcp_servers: {} };
|
|
546
|
+
}
|
|
547
|
+
const targetServers = registeredServers;
|
|
548
|
+
|
|
549
|
+
if (resolvedProfile === 'none') {
|
|
550
|
+
// Codex 0.115+: transport 없는 서버에 enabled=false를 보내면 "invalid transport" 에러.
|
|
551
|
+
// 비허용 서버는 override에서 제외하고, 허용 서버만 명시적으로 설정한다.
|
|
552
|
+
return { mcp_servers: {} };
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const config = { mcp_servers: {} };
|
|
556
|
+
const allowedToolsByServer = getProfileDefinition(resolvedProfile).allowedToolsByServer;
|
|
557
|
+
for (const server of targetServers) {
|
|
558
|
+
// Codex 0.115+: transport 없는 서버에 enabled=false를 보내면 "invalid transport" 에러.
|
|
559
|
+
// 비허용 서버는 override에서 제외한다 (Codex 기본 설정이 유지됨).
|
|
560
|
+
if (!allowedServers.has(server)) {
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
config.mcp_servers[server] = {
|
|
565
|
+
enabled: true,
|
|
566
|
+
enabled_tools: [...(allowedToolsByServer[server] || [])],
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
return config;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function toTomlLiteral(value) {
|
|
573
|
+
if (Array.isArray(value)) {
|
|
574
|
+
return `[${value.map((item) => toTomlLiteral(item)).join(',')}]`;
|
|
575
|
+
}
|
|
576
|
+
if (typeof value === 'string') return JSON.stringify(value);
|
|
577
|
+
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
|
578
|
+
throw new Error(`지원하지 않는 TOML 값 타입: ${typeof value}`);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function flattenConfig(prefix, value, output) {
|
|
582
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
583
|
+
for (const [key, nestedValue] of Object.entries(value)) {
|
|
584
|
+
flattenConfig(prefix ? `${prefix}.${key}` : key, nestedValue, output);
|
|
585
|
+
}
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
output.push(`${prefix}=${toTomlLiteral(value)}`);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
export function getCodexConfigOverrides(options = {}) {
|
|
592
|
+
const config = getCodexMcpConfig(options);
|
|
593
|
+
const overrides = [];
|
|
594
|
+
flattenConfig('', config, overrides);
|
|
595
|
+
return overrides;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
export function buildMcpPolicy(options = {}) {
|
|
599
|
+
const inventory = options.inventory || loadInventory(options.inventoryFile);
|
|
600
|
+
const inventoryIndex = buildInventoryIndex(inventory);
|
|
601
|
+
const resolvedOptions = { ...options, inventory, inventoryIndex };
|
|
602
|
+
const resolvedProfile = resolveMcpProfile(options.agentType, options.requestedProfile);
|
|
603
|
+
let allowedServers = resolveAllowedServers(resolvedOptions);
|
|
604
|
+
const hint = buildPromptHint(resolvedOptions);
|
|
605
|
+
|
|
606
|
+
// Phase-aware post-filter: 파이프라인 단계별 서버 제한 적용
|
|
607
|
+
const phase = options.phase;
|
|
608
|
+
const phaseOverride = phase && PHASE_OVERRIDES[phase];
|
|
609
|
+
if (phaseOverride && phaseOverride.blockedServers) {
|
|
610
|
+
const blocked = new Set(phaseOverride.blockedServers);
|
|
611
|
+
allowedServers = allowedServers.filter((s) => !blocked.has(s));
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
return {
|
|
615
|
+
requestedProfile: typeof options.requestedProfile === 'string' && options.requestedProfile
|
|
616
|
+
? options.requestedProfile
|
|
617
|
+
: 'auto',
|
|
618
|
+
resolvedProfile,
|
|
619
|
+
resolvedPhase: phase || null,
|
|
620
|
+
allowedServers,
|
|
621
|
+
hint,
|
|
622
|
+
geminiAllowedServers: getGeminiAllowedServers(resolvedOptions),
|
|
623
|
+
codexConfig: getCodexMcpConfig(resolvedOptions),
|
|
624
|
+
codexConfigOverrides: getCodexConfigOverrides(resolvedOptions),
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function shellEscape(value) {
|
|
629
|
+
return `'${String(value).replace(/'/g, `'\"'\"'`)}'`;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function shellArray(name, values) {
|
|
633
|
+
return `${name}=(${values.map((value) => shellEscape(value)).join(' ')})`;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
export function toShellExports(policy) {
|
|
637
|
+
const lines = [
|
|
638
|
+
`MCP_PROFILE_REQUESTED=${shellEscape(policy.requestedProfile)}`,
|
|
639
|
+
`MCP_RESOLVED_PROFILE=${shellEscape(policy.resolvedProfile)}`,
|
|
640
|
+
`MCP_HINT=${shellEscape(policy.hint)}`,
|
|
641
|
+
shellArray('GEMINI_ALLOWED_SERVERS', policy.geminiAllowedServers),
|
|
642
|
+
shellArray('CODEX_CONFIG_FLAGS', policy.codexConfigOverrides.flatMap((override) => ['-c', override])),
|
|
643
|
+
`CODEX_CONFIG_JSON=${shellEscape(JSON.stringify(policy.codexConfig))}`,
|
|
644
|
+
];
|
|
645
|
+
if (policy.resolvedPhase) {
|
|
646
|
+
lines.push(`MCP_PIPELINE_PHASE=${shellEscape(policy.resolvedPhase)}`);
|
|
647
|
+
}
|
|
648
|
+
return lines.join('\n');
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function parseCliArgs(argv) {
|
|
652
|
+
const args = {
|
|
653
|
+
command: 'json',
|
|
654
|
+
agentType: '',
|
|
655
|
+
requestedProfile: 'auto',
|
|
656
|
+
availableServers: [],
|
|
657
|
+
inventoryFile: '',
|
|
658
|
+
searchTool: '',
|
|
659
|
+
taskText: '',
|
|
660
|
+
workerIndex: undefined,
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
const [first = 'json'] = argv;
|
|
664
|
+
if (!first.startsWith('--')) {
|
|
665
|
+
args.command = first;
|
|
666
|
+
argv = argv.slice(1);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
670
|
+
const token = argv[i];
|
|
671
|
+
const next = () => {
|
|
672
|
+
const value = argv[i + 1];
|
|
673
|
+
if (value === undefined) throw new Error(`${token} 값이 필요합니다.`);
|
|
674
|
+
i += 1;
|
|
675
|
+
return value;
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
switch (token) {
|
|
679
|
+
case '--agent':
|
|
680
|
+
args.agentType = next();
|
|
681
|
+
break;
|
|
682
|
+
case '--profile':
|
|
683
|
+
args.requestedProfile = next();
|
|
684
|
+
break;
|
|
685
|
+
case '--available':
|
|
686
|
+
args.availableServers = parseAvailableServers(next());
|
|
687
|
+
break;
|
|
688
|
+
case '--inventory-file':
|
|
689
|
+
args.inventoryFile = next();
|
|
690
|
+
break;
|
|
691
|
+
case '--search-tool':
|
|
692
|
+
args.searchTool = next();
|
|
693
|
+
break;
|
|
694
|
+
case '--task-text':
|
|
695
|
+
args.taskText = next();
|
|
696
|
+
break;
|
|
697
|
+
case '--worker-index':
|
|
698
|
+
args.workerIndex = Number.parseInt(next(), 10);
|
|
699
|
+
break;
|
|
700
|
+
case '--phase':
|
|
701
|
+
args.phase = next();
|
|
702
|
+
break;
|
|
703
|
+
default:
|
|
704
|
+
throw new Error(`알 수 없는 옵션: ${token}`);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
return args;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
export async function runCli(argv = process.argv.slice(2)) {
|
|
712
|
+
let args;
|
|
713
|
+
try {
|
|
714
|
+
args = parseCliArgs(argv);
|
|
715
|
+
} catch (error) {
|
|
716
|
+
console.error(`[mcp-filter] ${error instanceof Error ? error.message : String(error)}`);
|
|
717
|
+
process.exitCode = 64;
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
let policy;
|
|
722
|
+
try {
|
|
723
|
+
policy = buildMcpPolicy(args);
|
|
724
|
+
} catch (error) {
|
|
725
|
+
console.error(`[mcp-filter] ${error instanceof Error ? error.message : String(error)}`);
|
|
726
|
+
process.exitCode = 65;
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
if (args.command === 'shell') {
|
|
730
|
+
process.stdout.write(`${toShellExports(policy)}\n`);
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
process.stdout.write(`${JSON.stringify(policy, null, 2)}\n`);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
738
|
+
await runCli();
|
|
739
|
+
}
|