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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/bin/triflux.mjs +42 -11
- package/config/mcp-registry.json +1 -0
- package/hub/bridge.mjs +18 -8
- package/hub/lib/process-utils.mjs +9 -4
- package/hub/server.mjs +0 -1
- package/hub/team/cli/services/hub-client.mjs +2 -2
- package/package.json +1 -1
- package/scripts/__tests__/mcp-guard-engine.test.mjs +42 -2
- package/scripts/config-audit.mjs +1 -0
- package/scripts/hub-watchdog.mjs +10 -6
- package/scripts/lib/env-probe.mjs +11 -2
- package/scripts/lib/mcp-guard-engine.mjs +12 -3
- package/scripts/mcp-check.mjs +6 -2
- package/scripts/session-stale-cleanup.mjs +5 -1
|
@@ -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.
|
|
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.
|
|
33
|
+
"version": "10.17.4"
|
|
34
34
|
}
|
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
|
-
|
|
5162
|
-
|
|
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(
|
|
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
|
-
|
|
5186
|
-
|
|
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(
|
|
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
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
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
|
-
|
|
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] :
|
|
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)) {
|
package/config/mcp-registry.json
CHANGED
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
|
-
|
|
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
|
-
|
|
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([
|
|
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
|
@@ -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) ||
|
|
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
|
@@ -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,
|
|
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);
|
package/scripts/config-audit.mjs
CHANGED
package/scripts/hub-watchdog.mjs
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// scripts/hub-watchdog.mjs — Hub 상시 감시 + 자동 재시작
|
|
3
3
|
//
|
|
4
|
-
// 10초마다
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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: "
|
|
292
|
+
if (basenameValue === ".mcp.json" || isClaudeProjectMcpConfig(filePath)) {
|
|
293
|
+
return { name, config: { type: "http", url } };
|
|
285
294
|
}
|
|
286
295
|
|
|
287
296
|
if (isCodexConfig(filePath)) {
|
package/scripts/mcp-check.mjs
CHANGED
|
@@ -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
|
|
141
|
-
|
|
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([
|
|
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) {
|