@triflux/remote 10.86.1 → 10.88.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.
@@ -9,6 +9,11 @@ import {
9
9
  import { homedir } from "node:os";
10
10
  import { dirname, join } from "node:path";
11
11
  import { writeRotatedBackup } from "./backup-rotation.mjs";
12
+ import {
13
+ modelEffortFromProfile,
14
+ normalizeCodexProfileName,
15
+ resolveCodexModelEffort,
16
+ } from "./model-effort.mjs";
12
17
  import {
13
18
  CODEX_FAMILY_PROFILES,
14
19
  modelFamilyPaths,
@@ -16,46 +21,42 @@ import {
16
21
  writeModelFile,
17
22
  } from "./model-family-resolver.mjs";
18
23
 
19
- const EFFORT_BY_SUFFIX = {
20
- ultra: "ultra",
21
- max: "max",
22
- xhigh: "xhigh",
23
- high: "high",
24
- med: "medium",
25
- medium: "medium",
26
- low: "low",
27
- };
28
-
29
- const LEGACY_CANONICAL_PROFILE_ALIASES = Object.freeze({
30
- gpt56_sol_xhigh: "astra_xhigh",
31
- gpt56_sol_max: "astra_max",
32
- gpt56_sol_ultra: "astra_ultra",
33
- gpt56_terra_high: "sol_high",
34
- gpt56_terra_med: "sol_med",
35
- });
24
+ export { normalizeCodexProfileName };
36
25
 
37
- export function normalizeCodexProfileName(profileName) {
38
- const normalized = String(profileName ?? "").trim();
39
- const legacy = LEGACY_CANONICAL_PROFILE_ALIASES[normalized];
40
- if (legacy) return legacy;
41
- const familyName = normalized.replace(/^gpt\d+_(sol|astra|luna)_/, "$1_");
42
- return Object.hasOwn(CODEX_FAMILY_PROFILES, familyName)
43
- ? familyName
44
- : normalized;
26
+ export function managedCodexProfileContents(opts = {}) {
27
+ const { profiles, families } = resolveModelFamilies("codex", opts);
28
+ const contents = {};
29
+ for (const [name, { model, effort }] of Object.entries(profiles)) {
30
+ contents[name] =
31
+ `model = "${model}"\nmodel_reasoning_effort = "${effort}"\n`;
32
+ }
33
+ for (const [family, { model }] of Object.entries(families)) {
34
+ contents[family] = `model = "${model}"\n`;
35
+ }
36
+ return { profiles, contents };
45
37
  }
46
38
 
47
39
  export function ensureManagedCodexProfiles(opts = {}) {
48
40
  const { codexHome } = modelFamilyPaths(opts);
49
- const { profiles } = resolveModelFamilies("codex", opts);
41
+ const { profiles, contents } = managedCodexProfileContents(opts);
50
42
  let changed = 0;
51
- for (const [name, { model, effort }] of Object.entries(profiles)) {
52
- if (
53
- writeModelFile(
54
- join(codexHome, `${name}.config.toml`),
55
- `model = "${model}"\nmodel_reasoning_effort = "${effort}"\n`,
56
- )
57
- )
58
- changed++;
43
+ const families = new Set(
44
+ Object.values(CODEX_FAMILY_PROFILES).map(({ family }) => family),
45
+ );
46
+ for (const [name, content] of Object.entries(contents)) {
47
+ const path = join(codexHome, `${name}.config.toml`);
48
+ if (families.has(name) && existsSync(path)) {
49
+ const existing = readFileSync(path, "utf8");
50
+ if (existing !== content) {
51
+ const backup = writeRotatedBackup(path, existing, {
52
+ label: "bak-model-family",
53
+ suffix: stamp(),
54
+ mode: privateFileMode(path),
55
+ });
56
+ opts.notice?.(`Codex 가족 프로필 백업: ${backup}`);
57
+ }
58
+ }
59
+ if (writeModelFile(path, content)) changed++;
59
60
  }
60
61
  return { changed, profiles };
61
62
  }
@@ -72,45 +73,25 @@ function readProfileScalar(raw, key) {
72
73
  return value || null;
73
74
  }
74
75
 
75
- /**
76
- * Resolve a Codex effort profile name (e.g. "sol_high") to the `-c key=value`
77
- * config overrides that `codex exec` accepts, so the headless CLI lanes can
78
- * select a profile WITHOUT `codex exec --profile <name>`. codex 0.134+ rejects
79
- * `--profile X` whenever config.toml still contains an inline `[profiles.X]`
80
- * table (which codex itself re-injects when it rewrites config.toml), so the
81
- * `-c` form is immune and needs no config mutation / sanitize race.
82
- *
83
- * SSOT = the per-profile file `~/.codex/<name>.config.toml` written by
84
- * `tfx setup`; falls back to deriving the effort from the `<...>_<effort>`
85
- * naming convention when the file is absent. Returns the keys that resolve, in
86
- * `key="value"` TOML-scalar form ready to be shell-quoted by the caller.
87
- *
88
- * @param {string} profileName
89
- * @param {{ codexHome?: string }} [opts]
90
- * @returns {string[]} e.g. ['model="gpt-6.1-sol"', 'model_reasoning_effort="high"']
91
- */
76
+ // 관리 이름의 effort는 변환 표가 정한다. custom 파일은 ultra 제한 판정에 사용한다.
92
77
  export function resolveCodexProfileConfigValues(profileName, opts = {}) {
93
78
  if (typeof profileName !== "string" || !profileName) {
94
79
  return { model: null, effort: null };
95
80
  }
96
81
  const normalizedProfileName = normalizeCodexProfileName(profileName);
82
+ const managed = modelEffortFromProfile("codex", normalizedProfileName);
97
83
  const codexHome =
98
84
  opts.codexHome || process.env.CODEX_HOME || join(homedir(), ".codex");
99
85
  let model = null;
100
- let effort = null;
86
+ let effort = managed ? resolveCodexModelEffort(managed, opts).effort : null;
101
87
  try {
102
88
  const raw = readFileSync(
103
89
  join(codexHome, `${normalizedProfileName}.config.toml`),
104
90
  "utf8",
105
91
  );
106
92
  model = readProfileScalar(raw, "model");
107
- effort = readProfileScalar(raw, "model_reasoning_effort");
108
- } catch {
109
- const suffix = /_(ultra|max|xhigh|high|med|medium|low)$/.exec(
110
- normalizedProfileName,
111
- );
112
- if (suffix) effort = EFFORT_BY_SUFFIX[suffix[1]] || null;
113
- }
93
+ if (!managed) effort = readProfileScalar(raw, "model_reasoning_effort");
94
+ } catch {}
114
95
  return { model, effort };
115
96
  }
116
97
 
@@ -1,4 +1,381 @@
1
1
  import { execFileSync } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import {
4
+ chmodSync,
5
+ copyFileSync,
6
+ existsSync,
7
+ readFileSync,
8
+ realpathSync,
9
+ renameSync,
10
+ unlinkSync,
11
+ writeFileSync,
12
+ } from "node:fs";
13
+
14
+ const defaultFs = {
15
+ chmodSync,
16
+ copyFileSync,
17
+ existsSync,
18
+ readFileSync,
19
+ realpathSync,
20
+ renameSync,
21
+ unlinkSync,
22
+ writeFileSync,
23
+ };
24
+
25
+ // ARM64 Mach-O agy 바이너리의 time.After 함수와 유휴 대기 호출부 시그니처 (#1022 peer-wake 패치)
26
+ export const AGY_TIME_AFTER_SIG = Buffer.from(
27
+ "900b40f9ff6330eb29010054fe0f1ef8fd831ff8fd2300d16affff97000040f9fd835ff8fe0742f8c0035fd6",
28
+ "hex",
29
+ );
30
+ export const AGY_PEER_WAKE_ORIG_CALL = Buffer.from(
31
+ "e13740f9220041a9423040f9210c40a9e40302aae20303aa80003fd6",
32
+ "hex",
33
+ );
34
+ export const AGY_PEER_WAKE_PATCH_PREFIX = Buffer.from(
35
+ "1f2003d51f2003d51f2003d51f2003d5004099524073a772",
36
+ "hex",
37
+ );
38
+ export const AGY_PEER_WAKE_ORIG_STP = Buffer.from("e08708a9", "hex");
39
+ export const AGY_PEER_WAKE_PATCH_STP = Buffer.from("e0ff08a9", "hex");
40
+ const AGY_PEER_WAKE_STP_DELTA = 0x38;
41
+
42
+ export function encodeArm64Bl(pcOffset, targetOffset) {
43
+ const delta = targetOffset - pcOffset;
44
+ if (delta % 4 !== 0 || delta < -0x8000000 || delta > 0x7fffffc) {
45
+ return null;
46
+ }
47
+ const imm26 = (delta >> 2) & 0x03ffffff;
48
+ const out = Buffer.alloc(4);
49
+ out.writeUInt32LE((0x94000000 | imm26) >>> 0, 0);
50
+ return out;
51
+ }
52
+
53
+ function findUniqueOffset(buf, pattern) {
54
+ const first = buf.indexOf(pattern);
55
+ if (first === -1) return -1;
56
+ if (buf.indexOf(pattern, first + 1) !== -1) return -2;
57
+ return first;
58
+ }
59
+
60
+ export function inspectAgyPeerWakePatch({
61
+ agyPath,
62
+ platform = process.platform,
63
+ arch = process.arch,
64
+ fs = defaultFs,
65
+ } = {}) {
66
+ if (platform !== "darwin" || arch !== "arm64") {
67
+ return { ok: true, status: "skipped", platform, arch };
68
+ }
69
+ if (!agyPath) {
70
+ return { ok: true, status: "skipped", reason: "agy-not-installed" };
71
+ }
72
+ let resolvedPath = agyPath;
73
+ try {
74
+ resolvedPath = fs.realpathSync(agyPath);
75
+ } catch {
76
+ return { ok: true, status: "skipped", reason: "agy-not-found" };
77
+ }
78
+ if (!fs.existsSync(resolvedPath)) {
79
+ return { ok: true, status: "skipped", reason: "agy-not-found" };
80
+ }
81
+
82
+ const patchJsonPath = `${resolvedPath}.patch.json`;
83
+ let buf;
84
+ try {
85
+ buf = fs.readFileSync(resolvedPath);
86
+ } catch (error) {
87
+ return {
88
+ ok: false,
89
+ status: "error",
90
+ patchable: false,
91
+ path: resolvedPath,
92
+ patchJsonPath,
93
+ reason: "read-failed",
94
+ message: error?.message || String(error),
95
+ };
96
+ }
97
+ const sha256 = createHash("sha256").update(buf).digest("hex");
98
+ const timeAfterOffset = findUniqueOffset(buf, AGY_TIME_AFTER_SIG);
99
+ if (timeAfterOffset < 0) {
100
+ return {
101
+ ok: false,
102
+ status: "unsupported",
103
+ patchable: false,
104
+ path: resolvedPath,
105
+ patchJsonPath,
106
+ sha256,
107
+ reason:
108
+ timeAfterOffset === -2
109
+ ? "ambiguous-time-after-signature"
110
+ : "time-after-signature-not-found",
111
+ };
112
+ }
113
+
114
+ const patchedOffset = findUniqueOffset(buf, AGY_PEER_WAKE_PATCH_PREFIX);
115
+ if (
116
+ patchedOffset >= 0 &&
117
+ patchedOffset + AGY_PEER_WAKE_STP_DELTA + 4 <= buf.length
118
+ ) {
119
+ const expectedBl = encodeArm64Bl(
120
+ patchedOffset + AGY_PEER_WAKE_PATCH_PREFIX.length,
121
+ timeAfterOffset,
122
+ );
123
+ const actualBl = buf.subarray(
124
+ patchedOffset + AGY_PEER_WAKE_PATCH_PREFIX.length,
125
+ patchedOffset + AGY_PEER_WAKE_PATCH_PREFIX.length + 4,
126
+ );
127
+ const actualStp = buf.subarray(
128
+ patchedOffset + AGY_PEER_WAKE_STP_DELTA,
129
+ patchedOffset + AGY_PEER_WAKE_STP_DELTA + 4,
130
+ );
131
+ if (
132
+ expectedBl &&
133
+ actualBl.equals(expectedBl) &&
134
+ actualStp.equals(AGY_PEER_WAKE_PATCH_STP)
135
+ ) {
136
+ let metadataOk = false;
137
+ if (fs.existsSync(patchJsonPath)) {
138
+ try {
139
+ const meta = JSON.parse(fs.readFileSync(patchJsonPath, "utf8"));
140
+ metadataOk =
141
+ meta?.sha256 === sha256 &&
142
+ Array.isArray(meta?.patches) &&
143
+ meta.patches.includes("peer-wake");
144
+ } catch {
145
+ metadataOk = false;
146
+ }
147
+ }
148
+ if (metadataOk) {
149
+ return {
150
+ ok: true,
151
+ status: "patched",
152
+ patchable: false,
153
+ binaryPatched: true,
154
+ path: resolvedPath,
155
+ patchJsonPath,
156
+ sha256,
157
+ callOffset: patchedOffset,
158
+ timeAfterOffset,
159
+ };
160
+ }
161
+ return {
162
+ ok: false,
163
+ status: "metadata-outdated",
164
+ patchable: true,
165
+ binaryPatched: true,
166
+ path: resolvedPath,
167
+ patchJsonPath,
168
+ sha256,
169
+ callOffset: patchedOffset,
170
+ timeAfterOffset,
171
+ fix: "tfx doctor --fix",
172
+ };
173
+ }
174
+ }
175
+
176
+ const origOffset = findUniqueOffset(buf, AGY_PEER_WAKE_ORIG_CALL);
177
+ if (
178
+ origOffset >= 0 &&
179
+ origOffset + AGY_PEER_WAKE_STP_DELTA + 4 <= buf.length
180
+ ) {
181
+ const expectedBl = encodeArm64Bl(
182
+ origOffset + AGY_PEER_WAKE_PATCH_PREFIX.length,
183
+ timeAfterOffset,
184
+ );
185
+ const actualStp = buf.subarray(
186
+ origOffset + AGY_PEER_WAKE_STP_DELTA,
187
+ origOffset + AGY_PEER_WAKE_STP_DELTA + 4,
188
+ );
189
+ if (expectedBl && actualStp.equals(AGY_PEER_WAKE_ORIG_STP)) {
190
+ return {
191
+ ok: false,
192
+ status: "unpatched",
193
+ patchable: true,
194
+ binaryPatched: false,
195
+ path: resolvedPath,
196
+ patchJsonPath,
197
+ sha256,
198
+ callOffset: origOffset,
199
+ timeAfterOffset,
200
+ fix: "tfx doctor --fix",
201
+ };
202
+ }
203
+ }
204
+
205
+ return {
206
+ ok: false,
207
+ status: "unsupported",
208
+ patchable: false,
209
+ path: resolvedPath,
210
+ patchJsonPath,
211
+ sha256,
212
+ reason:
213
+ origOffset === -2
214
+ ? "ambiguous-call-site-signature"
215
+ : "call-site-signature-not-found",
216
+ };
217
+ }
218
+
219
+ export function applyAgyPeerWakePatch({
220
+ agyPath,
221
+ version,
222
+ platform = process.platform,
223
+ arch = process.arch,
224
+ fs = defaultFs,
225
+ execFileSyncFn = execFileSync,
226
+ } = {}) {
227
+ const state = inspectAgyPeerWakePatch({ agyPath, platform, arch, fs });
228
+ if (state.status === "patched" || state.status === "skipped") {
229
+ return { ...state, changed: false };
230
+ }
231
+ if (!state.patchable) {
232
+ return { ...state, ok: false, changed: false };
233
+ }
234
+
235
+ let resolvedVersion = version || "";
236
+ if (!resolvedVersion) {
237
+ try {
238
+ const out = String(
239
+ execFileSyncFn(state.path, ["--version"], {
240
+ encoding: "utf8",
241
+ timeout: 5000,
242
+ stdio: ["ignore", "pipe", "ignore"],
243
+ }) || "",
244
+ );
245
+ resolvedVersion = out.match(/\b(\d+\.\d+\.\d+)\b/)?.[1] || "unknown";
246
+ } catch {
247
+ resolvedVersion = "unknown";
248
+ }
249
+ }
250
+
251
+ let finalSha256 = state.sha256;
252
+ let backupEntry = null;
253
+ if (state.status === "unpatched") {
254
+ const rawBuf = fs.readFileSync(state.path);
255
+ const currentSha256 = createHash("sha256").update(rawBuf).digest("hex");
256
+ if (currentSha256 !== state.sha256) {
257
+ throw new Error("agy binary changed during patch preparation");
258
+ }
259
+
260
+ let backupKey = `${resolvedVersion}-orig`;
261
+ let backupPath = `${state.path}.bak-${backupKey}`;
262
+ if (fs.existsSync(backupPath)) {
263
+ const existingBackupSha = createHash("sha256")
264
+ .update(fs.readFileSync(backupPath))
265
+ .digest("hex");
266
+ if (existingBackupSha !== state.sha256) {
267
+ backupKey = `${resolvedVersion}-orig-${state.sha256.slice(0, 8)}`;
268
+ backupPath = `${state.path}.bak-${backupKey}`;
269
+ if (fs.existsSync(backupPath)) {
270
+ const suffixBackupSha = createHash("sha256")
271
+ .update(fs.readFileSync(backupPath))
272
+ .digest("hex");
273
+ if (suffixBackupSha !== state.sha256) {
274
+ throw new Error(`agy backup conflict: ${backupPath}`);
275
+ }
276
+ } else {
277
+ fs.copyFileSync(state.path, backupPath);
278
+ }
279
+ }
280
+ } else {
281
+ fs.copyFileSync(state.path, backupPath);
282
+ }
283
+ backupEntry = { key: backupKey, path: backupPath, sha256: state.sha256 };
284
+
285
+ const buf = Buffer.from(rawBuf);
286
+ const expectedBl = encodeArm64Bl(
287
+ state.callOffset + AGY_PEER_WAKE_PATCH_PREFIX.length,
288
+ state.timeAfterOffset,
289
+ );
290
+ AGY_PEER_WAKE_PATCH_PREFIX.copy(buf, state.callOffset);
291
+ expectedBl.copy(buf, state.callOffset + AGY_PEER_WAKE_PATCH_PREFIX.length);
292
+ AGY_PEER_WAKE_PATCH_STP.copy(
293
+ buf,
294
+ state.callOffset + AGY_PEER_WAKE_STP_DELTA,
295
+ );
296
+
297
+ const tmpPath = `${state.path}.tmp-${process.pid}`;
298
+ fs.writeFileSync(tmpPath, buf, { mode: 0o755 });
299
+ try {
300
+ fs.chmodSync(tmpPath, 0o755);
301
+ execFileSyncFn("codesign", ["-f", "-s", "-", tmpPath], {
302
+ stdio: "ignore",
303
+ timeout: 30000,
304
+ });
305
+ const preRenameSha256 = createHash("sha256")
306
+ .update(fs.readFileSync(state.path))
307
+ .digest("hex");
308
+ if (preRenameSha256 !== state.sha256) {
309
+ throw new Error("agy binary changed before atomic replace");
310
+ }
311
+ fs.renameSync(tmpPath, state.path);
312
+ } catch (error) {
313
+ try {
314
+ fs.unlinkSync(tmpPath);
315
+ } catch {}
316
+ throw error;
317
+ }
318
+ finalSha256 = createHash("sha256")
319
+ .update(fs.readFileSync(state.path))
320
+ .digest("hex");
321
+ }
322
+
323
+ let existingMeta = {};
324
+ if (fs.existsSync(state.patchJsonPath)) {
325
+ try {
326
+ const parsed = JSON.parse(fs.readFileSync(state.patchJsonPath, "utf8"));
327
+ existingMeta =
328
+ parsed && typeof parsed === "object" && !Array.isArray(parsed)
329
+ ? parsed
330
+ : {};
331
+ } catch {
332
+ existingMeta = {};
333
+ }
334
+ }
335
+ const patches = Array.isArray(existingMeta.patches)
336
+ ? [...new Set([...existingMeta.patches, "peer-wake"])]
337
+ : ["peer-wake"];
338
+ const backups = {
339
+ ...(backupEntry
340
+ ? {
341
+ [backupEntry.key]: {
342
+ path: backupEntry.path,
343
+ sha256: backupEntry.sha256,
344
+ },
345
+ }
346
+ : {}),
347
+ ...(existingMeta.backups && typeof existingMeta.backups === "object"
348
+ ? existingMeta.backups
349
+ : {}),
350
+ };
351
+ const nextMeta = {
352
+ ...existingMeta,
353
+ version:
354
+ resolvedVersion !== "unknown"
355
+ ? resolvedVersion
356
+ : existingMeta.version || "unknown",
357
+ sha256: finalSha256,
358
+ patches,
359
+ appliedAt: new Date().toISOString(),
360
+ backups,
361
+ };
362
+ fs.writeFileSync(
363
+ state.patchJsonPath,
364
+ `${JSON.stringify(nextMeta, null, 2)}\n`,
365
+ "utf8",
366
+ );
367
+
368
+ return {
369
+ ok: true,
370
+ status: "patched",
371
+ changed: true,
372
+ binaryPatched: true,
373
+ previousStatus: state.status,
374
+ path: state.path,
375
+ patchJsonPath: state.patchJsonPath,
376
+ sha256: finalSha256,
377
+ };
378
+ }
2
379
 
3
380
  export function commandExists(name) {
4
381
  try {
@@ -1,5 +1,6 @@
1
1
  import { copyFileSync, existsSync, readFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
+ import { normalizeGeminiProfileName } from "./model-effort.mjs";
3
4
  import {
4
5
  AGY_FAMILY_PROFILES,
5
6
  modelFamilyPaths,
@@ -17,11 +18,7 @@ const DEFAULT_GEMINI_PROFILES = {
17
18
  ),
18
19
  };
19
20
 
20
- export function normalizeGeminiProfileName(name) {
21
- const raw = String(name ?? "").trim();
22
- // Flash 가족은 뺐다. 옛 Flash 이름은 effort 와 상관없이 더 빠른 Argon Low 로 보낸다.
23
- return raw.replace(/^flash\d*(?:_(?:low|high))?$/, "argon_low");
24
- }
21
+ export { normalizeGeminiProfileName };
25
22
 
26
23
  // ── 용도별 effort (SSOT) ──
27
24
  // Argon 가족 안에서 역할에 따라 effort 를 나눈다. 키는 tfx 에이전트/역할 이름
@@ -5,6 +5,7 @@
5
5
  import { readFileSync } from "node:fs";
6
6
  import process from "node:process";
7
7
  import { fileURLToPath } from "node:url";
8
+ import { isJevEnabled, judgeMcpSelection } from "@triflux/core/hub/lib/jev-addon.mjs";
8
9
  import { CORE_SERVERS, readManifest } from "./mcp-manifest.mjs";
9
10
  import {
10
11
  DOMAIN_TAG_KEYWORDS,
@@ -639,7 +640,11 @@ export function getCodexMcpConfig(options = {}) {
639
640
  if (registeredServers.length === 0) {
640
641
  return { mcp_servers: {} };
641
642
  }
642
- const allowedServers = new Set(resolveAllowedServers(options));
643
+ const allowedServers = new Set(
644
+ Array.isArray(options.allowedServers)
645
+ ? parseAvailableServers(options.allowedServers)
646
+ : resolveAllowedServers(options),
647
+ );
643
648
  const resolvedProfile = resolveMcpProfile(
644
649
  options.agentType,
645
650
  options.requestedProfile,
@@ -731,9 +736,17 @@ export function buildMcpPolicy(options = {}) {
731
736
  const hint = buildPromptHint(resolvedOptions);
732
737
 
733
738
  // 플러그인 서버는 transport 설정이 없어 CLI override를 받으면 부팅이 실패한다.
739
+ const configServers = codexServersFromConfig(options.codexConfig);
740
+ const registeredServers =
741
+ configServers.length > 0
742
+ ? configServers
743
+ : options.registeredServers !== undefined
744
+ ? parseAvailableServers(options.registeredServers)
745
+ : [];
734
746
  const codexOptions = {
735
747
  ...resolvedOptions,
736
- availableServers: codexServersFromConfig(options.codexConfig),
748
+ availableServers: registeredServers,
749
+ allowedServers,
737
750
  };
738
751
 
739
752
  return {
@@ -749,6 +762,74 @@ export function buildMcpPolicy(options = {}) {
749
762
  };
750
763
  }
751
764
 
765
+ export async function buildMcpPolicyWithJev(options = {}, jevOptions = {}) {
766
+ const basePolicy = buildMcpPolicy(options);
767
+ if (!isJevEnabled(jevOptions)) return basePolicy;
768
+ const taskText = String(options.taskText || "").trim();
769
+ if (!taskText) return basePolicy;
770
+
771
+ const isAutoProfile =
772
+ !options.requestedProfile || options.requestedProfile === "auto";
773
+ if (!isAutoProfile && options.requestedProfile === "none") {
774
+ return basePolicy;
775
+ }
776
+
777
+ const candidateServers = basePolicy.allowedServers;
778
+ if (candidateServers.length === 0) return basePolicy;
779
+
780
+ const selection = await judgeMcpSelection(
781
+ {
782
+ taskText,
783
+ agentType: options.agentType || basePolicy.resolvedProfile,
784
+ availableServers: candidateServers,
785
+ },
786
+ jevOptions,
787
+ );
788
+ if (!selection || !Array.isArray(selection.neededMcpServers))
789
+ return basePolicy;
790
+
791
+ const narrowedAvailable = selection.neededMcpServers.filter((server) =>
792
+ basePolicy.allowedServers.includes(server),
793
+ );
794
+ if (narrowedAvailable.length === 0) {
795
+ return {
796
+ ...buildMcpPolicy({
797
+ ...options,
798
+ requestedProfile: "none",
799
+ registeredServers:
800
+ options.registeredServers ??
801
+ (options.codexConfig === undefined
802
+ ? options.availableServers
803
+ : undefined),
804
+ }),
805
+ requestedProfile: options.requestedProfile || "auto",
806
+ };
807
+ }
808
+
809
+ const nextProfile =
810
+ isAutoProfile &&
811
+ selection.mcpProfile &&
812
+ selection.mcpProfile in PROFILE_DEFINITIONS
813
+ ? selection.mcpProfile
814
+ : options.requestedProfile;
815
+
816
+ const resolved = buildMcpPolicy({
817
+ ...options,
818
+ requestedProfile: nextProfile,
819
+ availableServers: narrowedAvailable,
820
+ registeredServers:
821
+ options.registeredServers ??
822
+ (options.codexConfig === undefined
823
+ ? options.availableServers
824
+ : undefined),
825
+ });
826
+ return {
827
+ ...resolved,
828
+ enabledServers: resolved.allowedServers,
829
+ requestedProfile: options.requestedProfile || "auto",
830
+ };
831
+ }
832
+
752
833
  function shellEscape(value) {
753
834
  return `'${String(value).replace(/'/g, `'"'"'`)}'`;
754
835
  }
@@ -863,7 +944,9 @@ async function runCli(argv = process.argv.slice(2)) {
863
944
 
864
945
  let policy;
865
946
  try {
866
- policy = buildMcpPolicy(args);
947
+ policy = isJevEnabled()
948
+ ? await buildMcpPolicyWithJev(args)
949
+ : buildMcpPolicy(args);
867
950
  } catch (error) {
868
951
  console.error(
869
952
  `[mcp-filter] ${error instanceof Error ? error.message : String(error)}`,