@triflux/core 10.50.2 → 10.52.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/hooks/agy-session-hook.mjs +11 -56
  2. package/hooks/codex-session-hook.mjs +11 -101
  3. package/hub/bridge.mjs +65 -1250
  4. package/hub/cli-adapter-base.mjs +12 -90
  5. package/hub/codex-adapter.mjs +0 -1
  6. package/hub/codex-preflight.mjs +5 -28
  7. package/hub/index.mjs +3 -19
  8. package/hub/lib/process-utils.mjs +1 -156
  9. package/hub/lib/spawn-trace.mjs +3 -2
  10. package/hub/lib/timeout-defaults.mjs +0 -1
  11. package/hub/team/claude-daemon-control.mjs +1 -2
  12. package/hub/team/claude-session-projection.mjs +5 -73
  13. package/hub/team/nativeProxy.mjs +759 -0
  14. package/hub/team/session-context.mjs +1 -1
  15. package/hud/cli-policy.mjs +0 -22
  16. package/hud/constants.mjs +13 -26
  17. package/hud/hud-qos-status.mjs +42 -54
  18. package/hud/providers/gemini.mjs +125 -20
  19. package/hud/renderers.mjs +101 -122
  20. package/hud/utils.mjs +10 -6
  21. package/package.json +2 -6
  22. package/scripts/lib/agent-route-policy.mjs +143 -62
  23. package/scripts/lib/cli-claude.mjs +0 -14
  24. package/scripts/lib/env-probe.mjs +2 -131
  25. package/scripts/lib/gemini-profiles.mjs +2 -69
  26. package/scripts/lib/legacy-mcp-cleanup.mjs +283 -0
  27. package/scripts/lib/machine-profile.mjs +3 -17
  28. package/scripts/lib/mcp-filter.mjs +109 -53
  29. package/scripts/lib/mcp-guard-engine.mjs +40 -98
  30. package/scripts/lib/mcp-health.mjs +2 -2
  31. package/scripts/lib/mcp-server-catalog.mjs +2 -2
  32. package/scripts/lib/psmux-info.mjs +11 -59
  33. package/scripts/lib/session-presence.mjs +2 -100
  34. package/scripts/lib/skill-template.mjs +0 -245
  35. package/hub/adaptive-diagnostic.mjs +0 -350
  36. package/hub/adaptive-inject.mjs +0 -224
  37. package/hub/adaptive-memory.mjs +0 -277
  38. package/hub/adaptive.mjs +0 -172
  39. package/hub/assign-callbacks.mjs +0 -170
  40. package/hub/delegator/contracts.mjs +0 -38
  41. package/hub/delegator/index.mjs +0 -14
  42. package/hub/delegator/schema/delegator-tools.schema.json +0 -250
  43. package/hub/delegator/service.mjs +0 -336
  44. package/hub/delegator/tool-definitions.mjs +0 -35
  45. package/hub/dynamic-routing-engine.mjs +0 -511
  46. package/hub/hitl.mjs +0 -202
  47. package/hub/lib/known-errors.json +0 -72
  48. package/hub/lib/memory-store.mjs +0 -1001
  49. package/hub/lib/phase-manager.mjs +0 -493
  50. package/hub/lib/risk-tier.mjs +0 -53
  51. package/hub/lib/ssh-command.mjs +0 -231
  52. package/hub/lib/staged-file-detect.mjs +0 -43
  53. package/hub/lib/state-snapshot.mjs +0 -293
  54. package/hub/lib/tfx-route-args.mjs +0 -353
  55. package/hub/lib/trace-recorder.mjs +0 -153
  56. package/hub/lib/uuidv7.mjs +0 -45
  57. package/hub/memory-doctor.mjs +0 -640
  58. package/hub/middleware/quota-middleware.mjs +0 -70
  59. package/hub/middleware/request-logger.mjs +0 -191
  60. package/hub/paths.mjs +0 -24
  61. package/hub/pipeline/index.mjs +0 -163
  62. package/hub/pipeline/state.mjs +0 -191
  63. package/hub/pipeline/transitions.mjs +0 -131
  64. package/hub/reflexion.mjs +0 -290
  65. package/hub/role-contract.mjs +0 -314
  66. package/hub/router.mjs +0 -2026
  67. package/hub/routing-snapshot.mjs +0 -261
  68. package/hub/session-fingerprint.mjs +0 -418
  69. package/hub/state.mjs +0 -300
  70. package/hub/team/synapse-http.mjs +0 -311
  71. package/hub/team-bridge.mjs +0 -27
  72. package/mesh/index.mjs +0 -69
  73. package/mesh/mesh-budget.mjs +0 -137
  74. package/mesh/mesh-heartbeat.mjs +0 -100
  75. package/mesh/mesh-protocol.mjs +0 -96
  76. package/mesh/mesh-queue.mjs +0 -163
  77. package/mesh/mesh-registry.mjs +0 -78
  78. package/mesh/mesh-router.mjs +0 -77
  79. package/scripts/lib/claudemd-scanner.mjs +0 -223
  80. package/scripts/lib/context.mjs +0 -67
  81. package/scripts/lib/dynamic-route-cli.mjs +0 -107
  82. package/scripts/lib/handoff.mjs +0 -187
  83. package/scripts/lib/logger.mjs +0 -122
  84. package/scripts/lib/stealth-fetch.mjs +0 -176
@@ -15,10 +15,6 @@ import {
15
15
  uniqueStrings,
16
16
  } from "./mcp-server-catalog.mjs";
17
17
 
18
- export const KNOWN_MCP_SERVERS = Object.freeze(
19
- Object.keys(MCP_SERVER_TOOL_CATALOG),
20
- );
21
-
22
18
  const SEARCH_INTENT_PATTERNS = Object.freeze([
23
19
  /\b(search|web|browse|look ?up|find|latest|recent|news|current|today|release(?: note)?s?|changelog|announcement|pricing|status|verify|fact[- ]?check)\b/i,
24
20
  /(검색|웹|브라우즈|찾아|조회|최신|최근|뉴스|현재|오늘|릴리즈|배포|변경사항|공지|가격|상태|검증)/u,
@@ -145,7 +141,7 @@ const PROFILE_DEFINITIONS = Object.freeze({
145
141
  * 파이프라인 단계별 MCP 서버/도구 제한 (post-filter).
146
142
  * role-based 프로필 위에 추가 적용. 빈 배열 = 전체 차단, 미정의 = 제한 없음.
147
143
  */
148
- export const PHASE_OVERRIDES = Object.freeze({
144
+ const PHASE_OVERRIDES = Object.freeze({
149
145
  plan: Object.freeze({
150
146
  description: "계획 단계: 읽기 전용 탐색만 허용",
151
147
  allowedServers: Object.freeze(["context7"]),
@@ -166,7 +162,7 @@ export const PHASE_OVERRIDES = Object.freeze({
166
162
  }),
167
163
  });
168
164
 
169
- export const LEGACY_PROFILE_ALIASES = Object.freeze({
165
+ const LEGACY_PROFILE_ALIASES = Object.freeze({
170
166
  implement: "executor",
171
167
  analyze: "analyze",
172
168
  review: "reviewer",
@@ -257,30 +253,51 @@ function loadInventory(inventoryFile = "") {
257
253
  }
258
254
  }
259
255
 
256
+ function availableServersFromInventory(inventory, cliType) {
257
+ const servers = inventory?.[cliType]?.servers || [];
258
+ return uniqueStrings(
259
+ servers
260
+ .filter((server) => ["enabled", "configured"].includes(server.status))
261
+ .map((server) => server.name),
262
+ );
263
+ }
264
+
265
+ function codexServersFromConfig(configFile) {
266
+ if (!configFile) return [];
267
+ try {
268
+ return uniqueStrings(
269
+ readFileSync(configFile, "utf8")
270
+ .split(/\r?\n/)
271
+ .map((line) => /^\[mcp_servers\.([^\].]+)\]$/.exec(line)?.[1])
272
+ .filter(Boolean),
273
+ ).sort();
274
+ } catch {
275
+ return [];
276
+ }
277
+ }
278
+
260
279
  function buildInventoryIndex(inventory = null) {
261
280
  const index = new Map();
262
281
  if (!inventory || typeof inventory !== "object") return index;
263
282
 
264
- for (const client of ["codex", "gemini"]) {
265
- const servers = Array.isArray(inventory[client]?.servers)
266
- ? inventory[client].servers
267
- : [];
268
- for (const server of servers) {
269
- if (!server || typeof server.name !== "string" || !server.name.trim())
270
- continue;
271
- const name = server.name.trim();
272
- const previous = index.get(name) || {};
273
- index.set(name, {
274
- ...previous,
275
- tool_count: Number.isFinite(server.tool_count)
276
- ? Math.max(previous.tool_count ?? 0, Math.trunc(server.tool_count))
277
- : previous.tool_count,
278
- domain_tags: uniqueStrings([
279
- ...(Array.isArray(previous.domain_tags) ? previous.domain_tags : []),
280
- ...(Array.isArray(server.domain_tags) ? server.domain_tags : []),
281
- ]),
282
- });
283
- }
283
+ const servers = Array.isArray(inventory.codex?.servers)
284
+ ? inventory.codex.servers
285
+ : [];
286
+ for (const server of servers) {
287
+ if (!server || typeof server.name !== "string" || !server.name.trim())
288
+ continue;
289
+ const name = server.name.trim();
290
+ const previous = index.get(name) || {};
291
+ index.set(name, {
292
+ ...previous,
293
+ tool_count: Number.isFinite(server.tool_count)
294
+ ? Math.max(previous.tool_count ?? 0, Math.trunc(server.tool_count))
295
+ : previous.tool_count,
296
+ domain_tags: uniqueStrings([
297
+ ...(Array.isArray(previous.domain_tags) ? previous.domain_tags : []),
298
+ ...(Array.isArray(server.domain_tags) ? server.domain_tags : []),
299
+ ]),
300
+ });
284
301
  }
285
302
 
286
303
  return index;
@@ -498,7 +515,7 @@ export function resolveMcpProfile(agentType = "", requestedProfile = "auto") {
498
515
  return normalized === "auto" ? resolveAutoProfile(agentType) : normalized;
499
516
  }
500
517
 
501
- export function parseAvailableServers(rawAvailableServers = "") {
518
+ function parseAvailableServers(rawAvailableServers = "") {
502
519
  if (Array.isArray(rawAvailableServers))
503
520
  return uniqueStrings(rawAvailableServers);
504
521
  if (typeof rawAvailableServers !== "string" || !rawAvailableServers.trim())
@@ -554,15 +571,21 @@ function applyManifestFilter(servers) {
554
571
  return servers.filter((s) => enabled.has(s));
555
572
  }
556
573
 
557
- export function resolveAllowedServers(options = {}) {
574
+ function resolveAllowedServers(options = {}) {
558
575
  const resolvedProfile = resolveMcpProfile(
559
576
  options.agentType,
560
577
  options.requestedProfile,
561
578
  );
562
579
  const profile = getProfileDefinition(resolvedProfile);
563
580
  const availableServers = parseAvailableServers(options.availableServers);
564
- const inventory = options.inventory || loadInventory(options.inventoryFile);
565
- const inventoryIndex = buildInventoryIndex(inventory);
581
+ const inventory =
582
+ options.inventory !== undefined
583
+ ? options.inventory
584
+ : loadInventory(options.inventoryFile);
585
+ const inventoryIndex =
586
+ options.inventoryIndex instanceof Map
587
+ ? options.inventoryIndex
588
+ : buildInventoryIndex(inventory);
566
589
  const baseServers = availableServers.length
567
590
  ? profile.allowedServers.filter((server) =>
568
591
  availableServers.includes(server),
@@ -585,8 +608,14 @@ export function buildPromptHint(options = {}) {
585
608
  );
586
609
  if (resolvedProfile === "none") return "";
587
610
 
588
- const inventory = options.inventory || loadInventory(options.inventoryFile);
589
- const inventoryIndex = buildInventoryIndex(inventory);
611
+ const inventory =
612
+ options.inventory !== undefined
613
+ ? options.inventory
614
+ : loadInventory(options.inventoryFile);
615
+ const inventoryIndex =
616
+ options.inventoryIndex instanceof Map
617
+ ? options.inventoryIndex
618
+ : buildInventoryIndex(inventory);
590
619
  const allowedServers = resolveAllowedServers({
591
620
  ...options,
592
621
  inventory,
@@ -636,16 +665,7 @@ export function buildPromptHint(options = {}) {
636
665
  .join(" ");
637
666
  }
638
667
 
639
- export function getGeminiAllowedServers(options = {}) {
640
- return resolveAllowedServers(options);
641
- }
642
-
643
668
  export function getCodexMcpConfig(options = {}) {
644
- const allowedServers = new Set(resolveAllowedServers(options));
645
- const resolvedProfile = resolveMcpProfile(
646
- options.agentType,
647
- options.requestedProfile,
648
- );
649
669
  // Codex에 실제 등록된 서버만 대상으로 config override 생성.
650
670
  // 미등록 서버에 enabled=false를 보내면 "invalid transport" 에러 발생.
651
671
  const registeredServers = parseAvailableServers(options.availableServers);
@@ -654,6 +674,11 @@ export function getCodexMcpConfig(options = {}) {
654
674
  if (registeredServers.length === 0) {
655
675
  return { mcp_servers: {} };
656
676
  }
677
+ const allowedServers = new Set(resolveAllowedServers(options));
678
+ const resolvedProfile = resolveMcpProfile(
679
+ options.agentType,
680
+ options.requestedProfile,
681
+ );
657
682
  const targetServers = registeredServers;
658
683
 
659
684
  if (resolvedProfile === "none") {
@@ -712,9 +737,27 @@ export function getCodexConfigOverrides(options = {}) {
712
737
  }
713
738
 
714
739
  export function buildMcpPolicy(options = {}) {
715
- const inventory = options.inventory || loadInventory(options.inventoryFile);
740
+ const inventory =
741
+ options.inventory !== undefined
742
+ ? options.inventory
743
+ : loadInventory(options.inventoryFile);
716
744
  const inventoryIndex = buildInventoryIndex(inventory);
717
- const resolvedOptions = { ...options, inventory, inventoryIndex };
745
+ let availableServers = options.availableServers;
746
+ if (availableServers === undefined) {
747
+ availableServers = availableServersFromInventory(
748
+ inventory,
749
+ options.cliType,
750
+ );
751
+ if (availableServers.length === 0 && options.cliType === "codex") {
752
+ availableServers = codexServersFromConfig(options.codexConfig);
753
+ }
754
+ }
755
+ const resolvedOptions = {
756
+ ...options,
757
+ availableServers,
758
+ inventory,
759
+ inventoryIndex,
760
+ };
718
761
  const resolvedProfile = resolveMcpProfile(
719
762
  options.agentType,
720
763
  options.requestedProfile,
@@ -730,6 +773,12 @@ export function buildMcpPolicy(options = {}) {
730
773
  allowedServers = allowedServers.filter((s) => !blocked.has(s));
731
774
  }
732
775
 
776
+ // 플러그인 서버는 transport 설정이 없어 CLI override를 받으면 부팅이 실패한다.
777
+ const codexOptions = {
778
+ ...resolvedOptions,
779
+ availableServers: codexServersFromConfig(options.codexConfig),
780
+ };
781
+
733
782
  return {
734
783
  requestedProfile:
735
784
  typeof options.requestedProfile === "string" && options.requestedProfile
@@ -739,9 +788,8 @@ export function buildMcpPolicy(options = {}) {
739
788
  resolvedPhase: phase || null,
740
789
  allowedServers,
741
790
  hint,
742
- geminiAllowedServers: getGeminiAllowedServers(resolvedOptions),
743
- codexConfig: getCodexMcpConfig(resolvedOptions),
744
- codexConfigOverrides: getCodexConfigOverrides(resolvedOptions),
791
+ codexConfig: getCodexMcpConfig(codexOptions),
792
+ codexConfigOverrides: getCodexConfigOverrides(codexOptions),
745
793
  };
746
794
  }
747
795
 
@@ -753,25 +801,25 @@ function shellArray(name, values) {
753
801
  return `${name}=(${values.map((value) => shellEscape(value)).join(" ")})`;
754
802
  }
755
803
 
756
- export function toDelimited(policy) {
804
+ function toDelimited(policy) {
757
805
  const RS = "\x1e";
758
806
  return [
759
807
  policy.requestedProfile,
760
808
  policy.resolvedProfile,
761
809
  policy.hint,
762
- policy.geminiAllowedServers.join(","),
763
- policy.codexConfigOverrides.flatMap((o) => ["-c", o]).join(","),
810
+ policy.allowedServers.join(","),
811
+ policy.codexConfigOverrides.flatMap((o) => ["-c", o]).join("\x1f"),
764
812
  JSON.stringify(policy.codexConfig),
765
813
  policy.resolvedPhase || "",
766
814
  ].join(RS);
767
815
  }
768
816
 
769
- export function toShellExports(policy) {
817
+ function toShellExports(policy) {
770
818
  const lines = [
771
819
  `MCP_PROFILE_REQUESTED=${shellEscape(policy.requestedProfile)}`,
772
820
  `MCP_RESOLVED_PROFILE=${shellEscape(policy.resolvedProfile)}`,
773
821
  `MCP_HINT=${shellEscape(policy.hint)}`,
774
- shellArray("GEMINI_ALLOWED_SERVERS", policy.geminiAllowedServers),
822
+ shellArray("ALLOWED_MCP_SERVERS", policy.allowedServers),
775
823
  shellArray(
776
824
  "CODEX_CONFIG_FLAGS",
777
825
  policy.codexConfigOverrides.flatMap((override) => ["-c", override]),
@@ -789,8 +837,10 @@ function parseCliArgs(argv) {
789
837
  command: "json",
790
838
  agentType: "",
791
839
  requestedProfile: "auto",
792
- availableServers: [],
840
+ availableServers: undefined,
793
841
  inventoryFile: "",
842
+ cliType: "",
843
+ codexConfig: "",
794
844
  searchTool: "",
795
845
  taskText: "",
796
846
  workerIndex: undefined,
@@ -821,6 +871,12 @@ function parseCliArgs(argv) {
821
871
  case "--available":
822
872
  args.availableServers = parseAvailableServers(next());
823
873
  break;
874
+ case "--cli-type":
875
+ args.cliType = next();
876
+ break;
877
+ case "--codex-config":
878
+ args.codexConfig = next();
879
+ break;
824
880
  case "--inventory-file":
825
881
  args.inventoryFile = next();
826
882
  break;
@@ -844,7 +900,7 @@ function parseCliArgs(argv) {
844
900
  return args;
845
901
  }
846
902
 
847
- export async function runCli(argv = process.argv.slice(2)) {
903
+ async function runCli(argv = process.argv.slice(2)) {
848
904
  let args;
849
905
  try {
850
906
  args = parseCliArgs(argv);
@@ -19,35 +19,23 @@ import { fileURLToPath } from "node:url";
19
19
 
20
20
  const PROJECT_ROOT = fileURLToPath(new URL("../../", import.meta.url));
21
21
  const DEFAULT_REGISTRY_PATH = join(PROJECT_ROOT, "config", "mcp-registry.json");
22
- const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1"]);
23
- const DEFAULT_HUB_PATH = "/mcp";
24
22
  const SERVER_POLICIES = new Set(["hosted", "stdio"]);
25
23
  const DEFAULT_REGISTRY = Object.freeze({
26
24
  $schema: "mcp-registry-schema",
27
25
  version: 1,
28
26
  description: "MCP 서버 중앙 레지스트리 — 진실의 원천",
29
27
  defaults: {
30
- transport: "hub-url",
31
- hub_base: "http://127.0.0.1:27888",
28
+ transport: "http",
32
29
  },
33
30
  policy_notes: {
34
31
  transport:
35
- 'Server transport accepts "hub-url" for triflux Hub URL flow, "http" for direct Streamable HTTP MCP endpoints, or "stdio" for upstream-stdio-only MCP servers (rare; only for servers that have no hosted HTTP endpoint, e.g. brave-search). stdio servers require command, args, and may include env.',
32
+ 'Server transport accepts "http" for direct Streamable HTTP MCP endpoints, or "stdio" for upstream-stdio-only MCP servers (rare; only for servers that have no hosted HTTP endpoint, e.g. brave-search). stdio servers require command, args, and may include env.',
36
33
  headers:
37
34
  'Optional headers are allowed only for HTTP-compatible transports. Each header value must be a descriptor: {"value":"literal"} for non-secret static values, {"env":"ENV_VAR_NAME"} for secrets resolved at sync/runtime, or {"env":"ENV_VAR_NAME","prefix":"Bearer "} for common authorization formats.',
38
35
  secret_safety:
39
36
  "Resolved secret values must not be written back to this registry file. Missing env vars warn during sync and do not emit empty secret headers.",
40
37
  },
41
- servers: {
42
- "tfx-hub": {
43
- policy: "hosted",
44
- transport: "hub-url",
45
- url: "http://127.0.0.1:27888/mcp",
46
- safe: true,
47
- targets: ["claude", "gemini", "codex", "antigravity"],
48
- description: "triflux Hub MCP 서버",
49
- },
50
- },
38
+ servers: {},
51
39
  policies: {
52
40
  unknown_server_action: "warn",
53
41
  sync_denylist: [],
@@ -711,28 +699,6 @@ function upsertTomlServer(raw, name, config, codex = {}) {
711
699
  return output.join("\n");
712
700
  }
713
701
 
714
- function getHubServerEntry(registry) {
715
- const entries = Object.entries(registry?.servers || {});
716
- if (entries.length === 0) {
717
- return [
718
- "tfx-hub",
719
- {
720
- url: `${registry?.defaults?.hub_base || "http://127.0.0.1:27888"}${DEFAULT_HUB_PATH}`,
721
- },
722
- ];
723
- }
724
-
725
- return (
726
- entries.find(([name]) => name === "tfx-hub") ||
727
- entries.find(([, config]) => config?.transport === "hub-url") ||
728
- entries[0]
729
- );
730
- }
731
-
732
- function _makeHubRuntimeConfig() {
733
- return { url: resolveHubUrl() };
734
- }
735
-
736
702
  function serverTargets(serverConfig) {
737
703
  if (Array.isArray(serverConfig?.targets) && serverConfig.targets.length > 0) {
738
704
  return [
@@ -806,10 +772,7 @@ export function buildDesiredServerRecord(name, serverConfig, filePath) {
806
772
  };
807
773
  }
808
774
 
809
- const url =
810
- serverConfig?.transport === "hub-url"
811
- ? resolveHubUrl()
812
- : normalizeUrl(serverConfig?.url || "");
775
+ const url = normalizeUrl(serverConfig?.url || "");
813
776
  const basenameValue = pathBasename(filePath);
814
777
  const resolvedHeaders = resolveHeaderDescriptors(
815
778
  name,
@@ -1205,10 +1168,8 @@ export function validateRegistry(registry) {
1205
1168
  errors.push(`registry.servers.${name}.policy must be hosted or stdio`);
1206
1169
  }
1207
1170
  const transport = server.transport || registry.defaults?.transport;
1208
- if (!["hub-url", "http", "stdio"].includes(transport)) {
1209
- errors.push(
1210
- `registry.servers.${name}.transport must be hub-url, http, or stdio`,
1211
- );
1171
+ if (!["http", "stdio"].includes(transport)) {
1172
+ errors.push(`registry.servers.${name}.transport must be http or stdio`);
1212
1173
  }
1213
1174
  if (
1214
1175
  policy === "hosted" &&
@@ -1407,6 +1368,38 @@ export function validateRegistry(registry) {
1407
1368
  return errors;
1408
1369
  }
1409
1370
 
1371
+ const warnedLegacyHub = new Set();
1372
+
1373
+ // 허브가 있던 시기의 사용자 레지스트리는 hub-url 항목을 걸러내고 기본 transport 를 http 로 읽는다.
1374
+ function dropLegacyHub(registry) {
1375
+ if (!registry || typeof registry !== "object" || Array.isArray(registry))
1376
+ return registry;
1377
+ const next = { ...registry };
1378
+ if (next.defaults?.transport === "hub-url") {
1379
+ const { hub_base: _hubBase, ...defaults } = next.defaults;
1380
+ next.defaults = { ...defaults, transport: "http" };
1381
+ }
1382
+ if (
1383
+ next.servers &&
1384
+ typeof next.servers === "object" &&
1385
+ !Array.isArray(next.servers)
1386
+ ) {
1387
+ next.servers = Object.fromEntries(
1388
+ Object.entries(next.servers).filter(([name, server]) => {
1389
+ if (server?.transport !== "hub-url") return true;
1390
+ if (!warnedLegacyHub.has(name)) {
1391
+ warnedLegacyHub.add(name);
1392
+ process.stderr.write(
1393
+ `[mcp-guard] 제거된 hub-url 서버 ${name} 항목을 무시합니다\n`,
1394
+ );
1395
+ }
1396
+ return false;
1397
+ }),
1398
+ );
1399
+ }
1400
+ return next;
1401
+ }
1402
+
1410
1403
  export function inspectRegistry() {
1411
1404
  if (!existsSync(registryPath())) {
1412
1405
  return {
@@ -1419,7 +1412,7 @@ export function inspectRegistry() {
1419
1412
  }
1420
1413
 
1421
1414
  try {
1422
- const registry = readJsonFile(registryPath());
1415
+ const registry = dropLegacyHub(readJsonFile(registryPath()));
1423
1416
  const errors = validateRegistry(registry);
1424
1417
  return {
1425
1418
  path: registryPath(),
@@ -1669,56 +1662,6 @@ export function scanForStdioServers(filePath) {
1669
1662
  return scanConfig(filePath).stdioServers;
1670
1663
  }
1671
1664
 
1672
- export function resolveHubUrl() {
1673
- const registryState = inspectRegistry();
1674
- const registry = registryState.valid
1675
- ? registryState.registry
1676
- : cloneDefaultRegistry();
1677
- const [, hubServer] = getHubServerEntry(registry);
1678
- const fallbackRaw =
1679
- hubServer?.url ||
1680
- `${registry?.defaults?.hub_base || "http://127.0.0.1:27888"}${DEFAULT_HUB_PATH}`;
1681
-
1682
- let fallback;
1683
- try {
1684
- fallback = new URL(fallbackRaw);
1685
- } catch {
1686
- fallback = new URL(`http://127.0.0.1:27888${DEFAULT_HUB_PATH}`);
1687
- }
1688
-
1689
- const envPortRaw = Number(process.env.TFX_HUB_PORT || "");
1690
- const envPort =
1691
- Number.isFinite(envPortRaw) && envPortRaw > 0 ? envPortRaw : null;
1692
- const target = {
1693
- protocol: fallback.protocol || "http:",
1694
- host: fallback.hostname || "127.0.0.1",
1695
- port: envPort || Number(fallback.port || 27888),
1696
- pathname:
1697
- fallback.pathname && fallback.pathname !== "/"
1698
- ? fallback.pathname
1699
- : DEFAULT_HUB_PATH,
1700
- };
1701
-
1702
- // PR #158 정책: port = TFX_HUB_PORT env (없으면 registry/default 27888) single source.
1703
- // hub.pid 는 loopback host 힌트 전용. 과거 pid port cascade 는 오염된 port 영속화의
1704
- // 원인이었고 hub-ensure.resolveHubTarget 에서 이미 제거됨. 여기도 일관 적용.
1705
- const hubPidPath = join(homedir(), ".claude", "cache", "tfx-hub", "hub.pid");
1706
- if (existsSync(hubPidPath)) {
1707
- try {
1708
- const info = readJsonFile(hubPidPath);
1709
- if (typeof info?.host === "string") {
1710
- const host = info.host.trim();
1711
- if (LOOPBACK_HOSTS.has(host)) target.host = host;
1712
- }
1713
- } catch {
1714
- // pid 파일 파싱 실패 시 registry 기본값 사용
1715
- }
1716
- }
1717
-
1718
- const hostPart = target.host.includes(":") ? `[${target.host}]` : target.host;
1719
- return `${target.protocol}//${hostPart}:${target.port}${target.pathname}`;
1720
- }
1721
-
1722
1665
  export function isWatchedPath(filePath) {
1723
1666
  const registryState = inspectRegistry();
1724
1667
  const registry = registryState.valid
@@ -1758,8 +1701,7 @@ export function addRegistryServer(name, url, options = {}) {
1758
1701
  const registry = registryState.valid
1759
1702
  ? loadRegistry()
1760
1703
  : cloneDefaultRegistry();
1761
- const transport =
1762
- options.transport || (trimmedName === "tfx-hub" ? "hub-url" : "http");
1704
+ const transport = options.transport || "http";
1763
1705
  const policy = options.policy || (transport === "stdio" ? "stdio" : "hosted");
1764
1706
 
1765
1707
  registry.servers[trimmedName] = {
@@ -224,7 +224,7 @@ export function parseMcpServersFromToml(content = "") {
224
224
  return servers;
225
225
  }
226
226
 
227
- export function readMcpServers(configPath = DEFAULT_CONFIG_PATH) {
227
+ function readMcpServers(configPath = DEFAULT_CONFIG_PATH) {
228
228
  if (!existsSync(configPath)) return {};
229
229
  try {
230
230
  const content = readFileSync(configPath, "utf8");
@@ -754,7 +754,7 @@ function renderOutput(results, source, format) {
754
754
  return JSON.stringify({ source, healthy, dead, results }, null, 2);
755
755
  }
756
756
 
757
- export async function runCli(argv = process.argv.slice(2)) {
757
+ async function runCli(argv = process.argv.slice(2)) {
758
758
  let args;
759
759
  try {
760
760
  args = parseCliArgs(argv);
@@ -296,7 +296,7 @@ export function uniqueStrings(values = []) {
296
296
  ];
297
297
  }
298
298
 
299
- export function inferDomainTagsFromText(text = "") {
299
+ function inferDomainTagsFromText(text = "") {
300
300
  if (typeof text !== "string" || !text.trim()) return [];
301
301
  const normalized = text.toLocaleLowerCase();
302
302
  const matched = [];
@@ -314,7 +314,7 @@ export function inferDomainTagsFromText(text = "") {
314
314
  return uniqueStrings(matched);
315
315
  }
316
316
 
317
- export function getDefaultServerMetadata(serverName = "") {
317
+ function getDefaultServerMetadata(serverName = "") {
318
318
  const toolCount = MCP_SERVER_TOOL_CATALOG[serverName]?.length || 0;
319
319
  const domainTags = uniqueStrings([
320
320
  ...(MCP_SERVER_DOMAIN_TAGS[serverName] || []),
@@ -1,36 +1,36 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
 
3
- export const PSMUX_RECOMMENDED_VERSION = "3.3.1";
4
- export const PSMUX_REQUIRED_COMMANDS = [
3
+ const PSMUX_RECOMMENDED_VERSION = "3.3.1";
4
+ const PSMUX_REQUIRED_COMMANDS = [
5
5
  "new-session",
6
6
  "attach-session",
7
7
  "kill-session",
8
8
  "capture-pane",
9
9
  ];
10
10
 
11
- export const PSMUX_OPTIONAL_COMMANDS = ["detach-client"];
11
+ const PSMUX_OPTIONAL_COMMANDS = ["detach-client"];
12
12
 
13
13
  // Windows 전용 설치 명령
14
- export const PSMUX_INSTALL_COMMANDS = [
14
+ const PSMUX_INSTALL_COMMANDS = [
15
15
  "winget install psmux",
16
16
  "scoop install psmux",
17
17
  "choco install psmux",
18
18
  "cargo install psmux",
19
19
  ];
20
20
 
21
- export const PSMUX_UPDATE_COMMANDS = [
21
+ const PSMUX_UPDATE_COMMANDS = [
22
22
  "winget upgrade psmux",
23
23
  "scoop update psmux",
24
24
  "choco upgrade psmux",
25
25
  "cargo install psmux --force",
26
26
  ];
27
27
 
28
- export function getPsmuxInstallCommandsFor(platform = process.platform) {
28
+ function getPsmuxInstallCommandsFor(platform = process.platform) {
29
29
  if (platform === "win32") return PSMUX_INSTALL_COMMANDS;
30
30
  return [platform === "darwin" ? "brew install tmux" : "apt install tmux"];
31
31
  }
32
32
 
33
- export function getPsmuxUpdateCommandsFor(platform = process.platform) {
33
+ function getPsmuxUpdateCommandsFor(platform = process.platform) {
34
34
  if (platform === "win32") return PSMUX_UPDATE_COMMANDS;
35
35
  return [
36
36
  platform === "darwin"
@@ -39,7 +39,7 @@ export function getPsmuxUpdateCommandsFor(platform = process.platform) {
39
39
  ];
40
40
  }
41
41
 
42
- export function formatPsmuxCommandList(
42
+ function formatPsmuxCommandList(
43
43
  commands = PSMUX_INSTALL_COMMANDS,
44
44
  indent = "",
45
45
  ) {
@@ -60,12 +60,12 @@ export function formatPsmuxUpdateGuidance(
60
60
  return formatPsmuxCommandList(getPsmuxUpdateCommandsFor(platform), indent);
61
61
  }
62
62
 
63
- export function parsePsmuxVersion(output = "") {
63
+ function parsePsmuxVersion(output = "") {
64
64
  const match = String(output).match(/psmux\s+v?(\d+\.\d+\.\d+)/i);
65
65
  return match?.[1] || null;
66
66
  }
67
67
 
68
- export function compareSemver(a, b) {
68
+ function compareSemver(a, b) {
69
69
  const left = String(a || "")
70
70
  .split(".")
71
71
  .map((part) => Number.parseInt(part, 10) || 0);
@@ -79,7 +79,7 @@ export function compareSemver(a, b) {
79
79
  return 0;
80
80
  }
81
81
 
82
- export function isRecommendedPsmuxVersion(version) {
82
+ function isRecommendedPsmuxVersion(version) {
83
83
  if (!version) return false;
84
84
  return compareSemver(version, PSMUX_RECOMMENDED_VERSION) >= 0;
85
85
  }
@@ -143,51 +143,3 @@ export function probePsmuxSupport(options = {}) {
143
143
  };
144
144
  }
145
145
  }
146
-
147
- export function probePrimaryMultiplexerSupport(options = {}) {
148
- const platform = options.platform || process.platform;
149
- const execFileSyncFn = options.execFileSyncFn || execFileSync;
150
-
151
- if (platform === "win32") {
152
- return {
153
- kind: "psmux",
154
- ...probePsmuxSupport(options),
155
- };
156
- }
157
-
158
- const bin = options.bin || "tmux";
159
- try {
160
- const versionOutput = execFileSyncFn(bin, ["-V"], {
161
- encoding: "utf8",
162
- timeout: 2000,
163
- stdio: ["ignore", "pipe", "pipe"],
164
- windowsHide: true,
165
- });
166
- return {
167
- ok: true,
168
- installed: true,
169
- kind: "tmux",
170
- version: String(versionOutput || "").trim() || null,
171
- recommendedVersion: null,
172
- recommended: true,
173
- missingCommands: [],
174
- missingOptionalCommands: [],
175
- installHint: formatPsmuxInstallGuidance(" ", platform),
176
- updateHint: formatPsmuxUpdateGuidance(" ", platform),
177
- };
178
- } catch (error) {
179
- return {
180
- ok: false,
181
- installed: false,
182
- kind: "tmux",
183
- version: null,
184
- recommendedVersion: null,
185
- recommended: false,
186
- missingCommands: ["tmux"],
187
- missingOptionalCommands: [],
188
- installHint: formatPsmuxInstallGuidance(" ", platform),
189
- updateHint: formatPsmuxUpdateGuidance(" ", platform),
190
- error: error?.message || String(error),
191
- };
192
- }
193
- }