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