triflux 10.17.3 → 10.17.4

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,7 +9,7 @@
9
9
  {
10
10
  "name": "triflux",
11
11
  "description": "Tri-CLI orchestrator for Claude Code. Routes tasks across Claude + Codex + Gemini with consensus intelligence, natural language routing, 42 skills, and cross-model review.",
12
- "version": "10.17.3",
12
+ "version": "10.17.4",
13
13
  "author": {
14
14
  "name": "tellang"
15
15
  },
@@ -30,5 +30,5 @@
30
30
  ]
31
31
  }
32
32
  ],
33
- "version": "10.17.3"
33
+ "version": "10.17.4"
34
34
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triflux",
3
- "version": "10.17.3",
3
+ "version": "10.17.4",
4
4
  "description": "CLI-first multi-model orchestrator for Claude Code — route tasks to Codex, Gemini, and Claude",
5
5
  "author": {
6
6
  "name": "tellang"
package/bin/triflux.mjs CHANGED
@@ -5158,11 +5158,19 @@ function autoRegisterMcp(mcpUrl, { codexEnabled = false } = {}) {
5158
5158
  if (existsSync(settingsFile))
5159
5159
  settings = JSON.parse(readFileSync(settingsFile, "utf8"));
5160
5160
  if (!settings.mcpServers) settings.mcpServers = {};
5161
- if (!settings.mcpServers["tfx-hub"]) {
5162
- settings.mcpServers["tfx-hub"] = { url: mcpUrl };
5161
+ const current = settings.mcpServers["tfx-hub"];
5162
+ if (!current || current.url !== mcpUrl) {
5163
+ settings.mcpServers["tfx-hub"] = {
5164
+ ...(current && typeof current === "object" ? current : {}),
5165
+ url: mcpUrl,
5166
+ };
5163
5167
  if (!existsSync(geminiDir)) mkdirSync(geminiDir, { recursive: true });
5164
5168
  writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + "\n");
5165
- ok("Gemini: settings.json에 등록 완료");
5169
+ ok(
5170
+ current
5171
+ ? "Gemini: settings.json URL 갱신 완료"
5172
+ : "Gemini: settings.json에 등록 완료",
5173
+ );
5166
5174
  } else {
5167
5175
  ok("Gemini: 이미 등록됨");
5168
5176
  }
@@ -5182,10 +5190,19 @@ function autoRegisterMcp(mcpUrl, { codexEnabled = false } = {}) {
5182
5190
  if (existsSync(mcpJsonPath))
5183
5191
  mcpJson = JSON.parse(readFileSync(mcpJsonPath, "utf8"));
5184
5192
  if (!mcpJson.mcpServers) mcpJson.mcpServers = {};
5185
- if (!mcpJson.mcpServers["tfx-hub"]) {
5186
- mcpJson.mcpServers["tfx-hub"] = { type: "url", url: mcpUrl };
5193
+ const current = mcpJson.mcpServers["tfx-hub"];
5194
+ if (!current || current.type !== "http" || current.url !== mcpUrl) {
5195
+ mcpJson.mcpServers["tfx-hub"] = {
5196
+ ...(current && typeof current === "object" ? current : {}),
5197
+ type: "http",
5198
+ url: mcpUrl,
5199
+ };
5187
5200
  writeFileSync(mcpJsonPath, JSON.stringify(mcpJson, null, 2) + "\n");
5188
- ok("Claude: .claude/mcp.json에 등록 완료");
5201
+ ok(
5202
+ current
5203
+ ? "Claude: .claude/mcp.json URL/type 갱신 완료"
5204
+ : "Claude: .claude/mcp.json에 등록 완료",
5205
+ );
5189
5206
  } else {
5190
5207
  ok("Claude: 이미 등록됨");
5191
5208
  }
@@ -5253,11 +5270,25 @@ async function cmdHub(args = [], options = {}) {
5253
5270
  try {
5254
5271
  const info = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
5255
5272
  process.kill(info.pid, 0); // 프로세스 존재 확인
5256
- autoRegisterMcp(info.url, { codexEnabled: true });
5257
- console.log(
5258
- `\n ${YELLOW}⚠${RESET} hub 이미 실행 중 (PID ${info.pid}, ${info.url})\n`,
5273
+ const host =
5274
+ typeof info.host === "string" && info.host.trim()
5275
+ ? info.host.trim()
5276
+ : "127.0.0.1";
5277
+ const port = Number(info.port) || probePort;
5278
+ const probed = await probeHubStatus(host, port, 1500);
5279
+ if (probed?.hub) {
5280
+ const url = `http://${formatHostForUrl(host)}:${probed.port || port}/mcp`;
5281
+ recoverPidFile(probed, host);
5282
+ autoRegisterMcp(url, { codexEnabled: true });
5283
+ console.log(
5284
+ `\n ${YELLOW}⚠${RESET} hub 이미 실행 중 (PID ${probed.pid || info.pid}, ${url})\n`,
5285
+ );
5286
+ return;
5287
+ }
5288
+ warn(
5289
+ `stale hub PID 파일 감지: PID ${info.pid}는 살아있지만 hub status 응답이 없음. PID 파일을 정리합니다.`,
5259
5290
  );
5260
- return;
5291
+ unlinkSync(HUB_PID_FILE);
5261
5292
  } catch {
5262
5293
  // PID 파일 있지만 프로세스 없음 — 정리
5263
5294
  try {
@@ -5267,7 +5298,7 @@ async function cmdHub(args = [], options = {}) {
5267
5298
  }
5268
5299
 
5269
5300
  const portArg = args.indexOf("--port");
5270
- const port = portArg !== -1 ? args[portArg + 1] : "27888";
5301
+ const port = portArg !== -1 ? args[portArg + 1] : String(probePort);
5271
5302
  const serverPath = join(PKG_ROOT, "hub", "server.mjs");
5272
5303
 
5273
5304
  if (!existsSync(serverPath)) {
@@ -23,6 +23,7 @@
23
23
  "~/.codex/config.toml",
24
24
  "~/.claude/settings.json",
25
25
  "~/.claude/settings.local.json",
26
+ ".claude/mcp.json",
26
27
  ".mcp.json"
27
28
  ]
28
29
  }
package/hub/bridge.mjs CHANGED
@@ -31,6 +31,17 @@ const HUB_PID_FILE = join(homedir(), ".claude", "cache", "tfx-hub", "hub.pid");
31
31
  const HUB_TOKEN_FILE = join(homedir(), ".claude", ".tfx-hub-token");
32
32
  const PROJECT_ROOT = fileURLToPath(new URL("..", import.meta.url));
33
33
  const HUB_DEFAULT_PORT = 27888;
34
+ const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1"]);
35
+
36
+ function formatHostForUrl(host) {
37
+ return host.includes(":") ? `[${host}]` : host;
38
+ }
39
+
40
+ function normalizeLoopbackHost(host) {
41
+ if (typeof host !== "string") return "127.0.0.1";
42
+ const candidate = host.trim();
43
+ return LOOPBACK_HOSTS.has(candidate) ? candidate : "127.0.0.1";
44
+ }
34
45
 
35
46
  function normalizeToken(raw) {
36
47
  if (raw == null) return null;
@@ -53,22 +64,21 @@ export function getHubUrl() {
53
64
  if (process.env.TFX_HUB_URL)
54
65
  return process.env.TFX_HUB_URL.replace(/\/mcp$/, "");
55
66
 
67
+ const envPort = Number.parseInt(String(process.env.TFX_HUB_PORT ?? ""), 10);
68
+ const port =
69
+ Number.isFinite(envPort) && envPort > 0 ? envPort : HUB_DEFAULT_PORT;
70
+ let host = "127.0.0.1";
71
+
56
72
  if (existsSync(HUB_PID_FILE)) {
57
73
  try {
58
74
  const info = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
59
- const pidPort = Number.parseInt(String(info?.port ?? ""), 10);
60
- const port =
61
- Number.isFinite(pidPort) && pidPort > 0 ? pidPort : HUB_DEFAULT_PORT;
62
- return `http://${info.host || "127.0.0.1"}:${port}`;
75
+ host = normalizeLoopbackHost(info?.host);
63
76
  } catch {
64
77
  // 무시
65
78
  }
66
79
  }
67
80
 
68
- const envPort = Number.parseInt(String(process.env.TFX_HUB_PORT ?? ""), 10);
69
- const port =
70
- Number.isFinite(envPort) && envPort > 0 ? envPort : HUB_DEFAULT_PORT;
71
- return `http://127.0.0.1:${port}`;
81
+ return `http://${formatHostForUrl(host)}:${port}`;
72
82
  }
73
83
 
74
84
  export function getHubPipePath() {
@@ -30,7 +30,11 @@ const LEGACY_ORPHAN_KILLABLE_NAMES = new Set([
30
30
  "cmd.exe",
31
31
  "uvx.exe",
32
32
  ]);
33
- const LIVE_CLI_SESSION_ROOT_NAMES = new Set(["codex.exe", "claude.exe"]);
33
+ const LIVE_CLI_SESSION_ROOT_NAMES = new Set([
34
+ "codex.exe",
35
+ "claude.exe",
36
+ "gemini.exe",
37
+ ]);
34
38
 
35
39
  /**
36
40
  * 주어진 PID의 프로세스가 살아있는지 확인한다.
@@ -179,7 +183,7 @@ function ensureHelperScripts() {
179
183
  SCAN_SCRIPT_PATH,
180
184
  [
181
185
  "$ErrorActionPreference = 'SilentlyContinue'",
182
- "Get-CimInstance Win32_Process -Filter \"Name='node.exe' OR Name='bash.exe' OR Name='cmd.exe' OR Name='codex.exe' OR Name='claude.exe' OR Name='pwsh.exe' OR Name='uvx.exe'\" | ForEach-Object {",
186
+ "Get-CimInstance Win32_Process -Filter \"Name='node.exe' OR Name='bash.exe' OR Name='cmd.exe' OR Name='codex.exe' OR Name='claude.exe' OR Name='gemini.exe' OR Name='pwsh.exe' OR Name='uvx.exe'\" | ForEach-Object {",
183
187
  ' Write-Output "$($_.ProcessId),$($_.ParentProcessId),$($_.Name)"',
184
188
  "}",
185
189
  ].join("\n"),
@@ -1014,7 +1018,7 @@ function cleanupOrphansUnix() {
1014
1018
  if (
1015
1019
  Number.isFinite(pid) &&
1016
1020
  pid > 0 &&
1017
- /^(node|bash|sh|python|codex|claude|uvx)/.test(name)
1021
+ /^(node|bash|sh|python|codex|claude|gemini|uvx)/.test(name)
1018
1022
  ) {
1019
1023
  procMap.set(pid, { ppid, name });
1020
1024
  }
@@ -1022,7 +1026,7 @@ function cleanupOrphansUnix() {
1022
1026
  } catch {}
1023
1027
 
1024
1028
  // kill 대상: node, python, codex, claude, uvx — bash/sh는 사용자 인터랙티브 쉘 가능성
1025
- const killableUnix = /^(node|python|codex|claude|uvx)/;
1029
+ const killableUnix = /^(node|python|codex|claude|gemini|uvx)/;
1026
1030
 
1027
1031
  // 고아 판정 + SIGKILL 에스컬레이션
1028
1032
  const orphanPids = [];
@@ -1030,6 +1034,7 @@ function cleanupOrphansUnix() {
1030
1034
  if (protectedPids.has(pid)) continue;
1031
1035
  if (!killableUnix.test(info.name)) continue;
1032
1036
  if (hasLiveAncestorChain(pid, procMap, protectedPids)) continue;
1037
+ if (hasLiveCliDescendant(pid, procMap)) continue;
1033
1038
  orphanPids.push(pid);
1034
1039
  }
1035
1040
 
package/hub/server.mjs CHANGED
@@ -778,7 +778,6 @@ export async function startHub({
778
778
  const port = portSpecified
779
779
  ? resolvedPort
780
780
  : resolveHubPort(process.env, {
781
- preferLivePid: true,
782
781
  detectPeer: () => livePeer,
783
782
  });
784
783
 
@@ -66,15 +66,15 @@ export async function getHubInfo() {
66
66
  if (!Number.isFinite(pid) || pid <= 0) throw new Error("invalid pid");
67
67
  process.kill(pid, 0);
68
68
  const host = normalizeLoopbackHost(raw?.host);
69
- const port = Number(raw?.port) || 27888;
69
+ const port = Number(raw?.port) || probePort;
70
70
  const status = await probeHubStatus(host, port, 1200);
71
+ if (!status) throw new Error("pid file process is not a healthy hub");
71
72
  return {
72
73
  ...raw,
73
74
  pid,
74
75
  host,
75
76
  port,
76
77
  url: `${buildHubBaseUrl(host, port)}/mcp`,
77
- ...(status ? {} : { degraded: true }),
78
78
  };
79
79
  } catch {
80
80
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triflux",
3
- "version": "10.17.3",
3
+ "version": "10.17.4",
4
4
  "description": "CLI-first multi-model orchestrator for Claude Code — route tasks to Codex, Gemini, and Claude",
5
5
  "type": "module",
6
6
  "bin": {
@@ -54,10 +54,10 @@ describe("mcp guard engine", () => {
54
54
  const registry = loadRegistry();
55
55
  assert.equal(registry.version, 1);
56
56
  assert.equal(registry.servers["tfx-hub"].url, "http://127.0.0.1:27888/mcp");
57
- assert.equal(registry.policies.watched_paths.length, 5);
57
+ assert.equal(registry.policies.watched_paths.length, 6);
58
58
  });
59
59
 
60
- it("matches watched paths for Gemini and local .mcp.json", () => {
60
+ it("matches watched paths for Gemini, Claude project MCP, and local .mcp.json", () => {
61
61
  const homeDir = createHomeDir();
62
62
  withHome(homeDir);
63
63
 
@@ -69,6 +69,10 @@ describe("mcp guard engine", () => {
69
69
  isWatchedPath(join(PROJECT_ROOT, "nested", ".mcp.json")),
70
70
  true,
71
71
  );
72
+ assert.equal(
73
+ isWatchedPath(join(PROJECT_ROOT, "nested", ".claude", "mcp.json")),
74
+ true,
75
+ );
72
76
  assert.equal(
73
77
  isWatchedPath(join(PROJECT_ROOT, "nested", "settings.yaml")),
74
78
  false,
@@ -101,6 +105,42 @@ describe("mcp guard engine", () => {
101
105
  );
102
106
  });
103
107
 
108
+ it("treats .claude/mcp.json as a Claude project MCP config", () => {
109
+ const homeDir = createHomeDir();
110
+ withHome(homeDir);
111
+
112
+ const projectMcpPath = join(homeDir, "repo", ".claude", "mcp.json");
113
+ mkdirSync(dirname(projectMcpPath), { recursive: true });
114
+ writeFileSync(
115
+ projectMcpPath,
116
+ JSON.stringify(
117
+ {
118
+ mcpServers: {
119
+ "unsafe-stdio": { command: "node", args: ["server.js"] },
120
+ },
121
+ },
122
+ null,
123
+ 2,
124
+ ),
125
+ );
126
+
127
+ const found = scanForStdioServers(projectMcpPath);
128
+ assert.deepEqual(
129
+ found.map((server) => server.name),
130
+ ["unsafe-stdio"],
131
+ );
132
+
133
+ const result = remediate(projectMcpPath, found, {
134
+ stdio_action: "replace-with-hub",
135
+ });
136
+ const updated = JSON.parse(readFileSync(projectMcpPath, "utf8"));
137
+
138
+ assert.equal(result.modified, true);
139
+ assert.equal(updated.mcpServers["tfx-hub"].type, "http");
140
+ assert.equal(updated.mcpServers["tfx-hub"].url, resolveHubUrl());
141
+ assert.equal(Object.hasOwn(updated.mcpServers, "unsafe-stdio"), false);
142
+ });
143
+
104
144
  it("replaces stdio MCP entries with tfx-hub and writes a backup (TFX_HUB_PORT env overrides)", () => {
105
145
  const homeDir = createHomeDir();
106
146
  withHome(homeDir);
@@ -155,6 +155,7 @@ function auditMcp() {
155
155
  const mcpPaths = [
156
156
  join(CLAUDE_DIR, "mcp_servers.json"),
157
157
  join(CLAUDE_DIR, ".mcp.json"),
158
+ join(process.cwd(), ".claude", "mcp.json"),
158
159
  join(process.cwd(), ".mcp.json"),
159
160
  ];
160
161
 
@@ -1,15 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
  // scripts/hub-watchdog.mjs — Hub 상시 감시 + 자동 재시작
3
3
  //
4
- // 10초마다 27888/status 체크. 응답 없으면 `tfx hub start` 실행.
4
+ // 10초마다 Hub /status 체크. 응답 없으면 `tfx hub start` 실행.
5
5
  // 실행: node scripts/hub-watchdog.mjs &
6
6
  // 중지: kill $(pgrep -f hub-watchdog.mjs) (또는 별도 pid 파일)
7
7
 
8
8
  import { spawn } from "node:child_process";
9
- import { existsSync, unlinkSync, writeFileSync } from "node:fs";
9
+ import { appendFileSync, existsSync, unlinkSync, writeFileSync } from "node:fs";
10
10
  import { join } from "node:path";
11
11
 
12
- const HUB_URL = "http://127.0.0.1:27888/status";
12
+ const HUB_DEFAULT_PORT = 27888;
13
+ const envPort = Number.parseInt(String(process.env.TFX_HUB_PORT ?? ""), 10);
14
+ const HUB_PORT =
15
+ Number.isFinite(envPort) && envPort > 0 ? envPort : HUB_DEFAULT_PORT;
16
+ const HUB_URL = `http://127.0.0.1:${HUB_PORT}/status`;
13
17
  const POLL_MS = 10_000;
14
18
  const START_GRACE_MS = 5_000;
15
19
  const LOG_PREFIX = "[hub-watchdog]";
@@ -24,8 +28,7 @@ function log(msg) {
24
28
  process.stdout.write(line);
25
29
  } catch {}
26
30
  try {
27
- const fs = require("node:fs");
28
- fs.appendFileSync(LOG_FILE, line);
31
+ appendFileSync(LOG_FILE, line);
29
32
  } catch {}
30
33
  }
31
34
 
@@ -44,6 +47,7 @@ function startHub() {
44
47
  log("Hub 기동: tfx hub start");
45
48
  const proc = spawn("tfx", ["hub", "start"], {
46
49
  cwd: process.cwd(),
50
+ env: { ...process.env, TFX_HUB_PORT: String(HUB_PORT) },
47
51
  stdio: "ignore",
48
52
  detached: true,
49
53
  shell: true,
@@ -81,6 +85,6 @@ try {
81
85
  writeFileSync(PID_FILE, String(process.pid));
82
86
  } catch {}
83
87
 
84
- log(`watchdog 시작 (pid=${process.pid}, poll=${POLL_MS}ms)`);
88
+ log(`watchdog 시작 (pid=${process.pid}, port=${HUB_PORT}, poll=${POLL_MS}ms)`);
85
89
  ensure();
86
90
  setInterval(ensure, POLL_MS);
@@ -6,6 +6,7 @@ import { dirname, join } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { whichCommand, whichCommandAsync } from "../../hub/platform.mjs";
8
8
 
9
+ const HUB_DEFAULT_PORT = 27888;
9
10
  const DEFAULT_STATUS_URL = "http://127.0.0.1:27888/status";
10
11
  const _sab = new Int32Array(new SharedArrayBuffer(4));
11
12
  const CLI_PROBE_CACHE = new Map();
@@ -20,7 +21,7 @@ function sleepSync(ms) {
20
21
 
21
22
  function fetchHubStatus({
22
23
  execSyncFn = execSync,
23
- statusUrl = DEFAULT_STATUS_URL,
24
+ statusUrl = resolveDefaultStatusUrl(),
24
25
  timeout = 3000,
25
26
  } = {}) {
26
27
  const response = execSyncFn(`curl -sf ${statusUrl}`, {
@@ -36,6 +37,13 @@ function fetchHubStatus({
36
37
  };
37
38
  }
38
39
 
40
+ export function resolveDefaultStatusUrl(env = process.env) {
41
+ const envPort = Number.parseInt(String(env?.TFX_HUB_PORT ?? ""), 10);
42
+ const port =
43
+ Number.isFinite(envPort) && envPort > 0 ? envPort : HUB_DEFAULT_PORT;
44
+ return `http://127.0.0.1:${port}/status`;
45
+ }
46
+
39
47
  function normalizeCliName(name) {
40
48
  return String(name ?? "").trim() || null;
41
49
  }
@@ -205,7 +213,7 @@ export function detectCodexPlan(options = {}) {
205
213
 
206
214
  export function checkHub({
207
215
  pkgRoot = DEFAULT_PKG_ROOT,
208
- statusUrl = DEFAULT_STATUS_URL,
216
+ statusUrl = resolveDefaultStatusUrl(),
209
217
  restart = true,
210
218
  requestTimeoutMs = 3000,
211
219
  pollAttempts = 8,
@@ -231,6 +239,7 @@ export function checkHub({
231
239
 
232
240
  try {
233
241
  const child = spawnFn(process.execPath, [serverPath], {
242
+ env: { ...process.env, TFX_HUB_PORT: String(new URL(statusUrl).port) },
234
243
  detached: true,
235
244
  stdio: "ignore",
236
245
  windowsHide: true,
@@ -38,6 +38,7 @@ const DEFAULT_REGISTRY = Object.freeze({
38
38
  "~/.codex/config.toml",
39
39
  "~/.claude/settings.json",
40
40
  "~/.claude/settings.local.json",
41
+ ".claude/mcp.json",
41
42
  ".mcp.json",
42
43
  ],
43
44
  },
@@ -69,6 +70,10 @@ function pathBasename(filePath) {
69
70
  return basename(filePath.replace(/\\/g, "/")).toLowerCase();
70
71
  }
71
72
 
73
+ function isClaudeProjectMcpConfig(filePath) {
74
+ return normalizeForMatch(filePath).endsWith("/.claude/mcp.json");
75
+ }
76
+
72
77
  function readJsonFile(filePath) {
73
78
  return JSON.parse(readFileSync(filePath, "utf8"));
74
79
  }
@@ -89,7 +94,8 @@ function isJsonMcpConfig(filePath) {
89
94
  return (
90
95
  name === "settings.json" ||
91
96
  name === "settings.local.json" ||
92
- name === ".mcp.json"
97
+ name === ".mcp.json" ||
98
+ isClaudeProjectMcpConfig(filePath)
93
99
  );
94
100
  }
95
101
 
@@ -116,6 +122,7 @@ function detectClient(filePath) {
116
122
  if (
117
123
  normalized.endsWith("/.claude/settings.json") ||
118
124
  normalized.endsWith("/.claude/settings.local.json") ||
125
+ normalized.endsWith("/.claude/mcp.json") ||
119
126
  normalized.endsWith("/.mcp.json")
120
127
  ) {
121
128
  return "claude";
@@ -130,6 +137,7 @@ function detectLabel(filePath) {
130
137
  if (normalized.endsWith("/.claude/settings.json")) return "Claude User";
131
138
  if (normalized.endsWith("/.claude/settings.local.json"))
132
139
  return "Claude Local";
140
+ if (normalized.endsWith("/.claude/mcp.json")) return "Claude Project MCP";
133
141
  if (normalized.endsWith("/.mcp.json")) return "Project MCP";
134
142
  return basename(filePath);
135
143
  }
@@ -139,6 +147,7 @@ function isPrimaryConfigTarget(filePath) {
139
147
  return (
140
148
  normalized.endsWith("/.gemini/settings.json") ||
141
149
  normalized.endsWith("/.codex/config.toml") ||
150
+ normalized.endsWith("/.claude/mcp.json") ||
142
151
  normalized.endsWith("/.mcp.json")
143
152
  );
144
153
  }
@@ -280,8 +289,8 @@ function buildDesiredServerRecord(name, serverConfig, filePath) {
280
289
  : normalizeUrl(serverConfig?.url || "");
281
290
  const basenameValue = pathBasename(filePath);
282
291
 
283
- if (basenameValue === ".mcp.json") {
284
- return { name, config: { type: "url", url } };
292
+ if (basenameValue === ".mcp.json" || isClaudeProjectMcpConfig(filePath)) {
293
+ return { name, config: { type: "http", url } };
285
294
  }
286
295
 
287
296
  if (isCodexConfig(filePath)) {
@@ -137,8 +137,12 @@ function walkUpForMcpJson(startDir, maxDepth = 5) {
137
137
  const found = [];
138
138
  let dir = resolve(startDir);
139
139
  for (let i = 0; i < maxDepth; i += 1) {
140
- const candidate = join(dir, ".mcp.json");
141
- if (existsSync(candidate)) found.push(candidate);
140
+ for (const candidate of [
141
+ join(dir, ".claude", "mcp.json"),
142
+ join(dir, ".mcp.json"),
143
+ ]) {
144
+ if (existsSync(candidate)) found.push(candidate);
145
+ }
142
146
  const parent = dirname(dir);
143
147
  if (parent === dir) break;
144
148
  dir = parent;
@@ -26,7 +26,11 @@ import { isProcessAlive } from "./lib/process-utils.mjs";
26
26
  const MULTI_STATE_FILE = join(tmpdir(), "tfx-multi-state.json");
27
27
  const EXPIRE_MS = 30 * 60 * 1000; // 30분
28
28
  const PID_FILE_RE = /^tfx-route-(\d+)-pids$/;
29
- const PROTECTED_ANCESTOR_NAMES = new Set(["claude.exe", "codex.exe"]);
29
+ const PROTECTED_ANCESTOR_NAMES = new Set([
30
+ "claude.exe",
31
+ "codex.exe",
32
+ "gemini.exe",
33
+ ]);
30
34
  const PID_REUSE_GRACE_MS = 1000;
31
35
 
32
36
  function normalizeName(name) {