triflux 8.11.2 → 8.12.2
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/plugin.json +1 -1
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/hooks/hooks.json +56 -51
- package/hub/pipeline/index.mjs +318 -318
- package/hub/routing/q-learning.mjs +2 -1
- package/hub/schema.sql +146 -146
- package/hub/team/cli/commands/kill.mjs +37 -37
- package/hub/team/cli/commands/start/index.mjs +3 -2
- package/hub/team/cli/commands/start/parse-args.mjs +5 -0
- package/hub/team/cli/commands/start/start-headless.mjs +2 -1
- package/hub/team/cli/commands/stop.mjs +31 -31
- package/hub/team/cli/commands/task.mjs +30 -30
- package/hub/team/cli/help.mjs +41 -40
- package/hub/team/cli/services/hub-client.mjs +208 -208
- package/hub/team/cli/services/native-control.mjs +118 -118
- package/hub/team/cli/services/runtime-mode.mjs +62 -62
- package/hub/team/cli/services/state-store.mjs +48 -48
- package/hub/team/dashboard-anchor.mjs +14 -0
- package/hub/team/dashboard.mjs +274 -274
- package/hub/team/headless.mjs +44 -19
- package/hub/team/native.mjs +649 -649
- package/hub/tools.mjs +554 -554
- package/hub/workers/delegator-mcp.mjs +21 -1
- package/package.json +1 -1
- package/scripts/__tests__/remote-spawn-transfer.test.mjs +117 -0
- package/scripts/__tests__/remote-spawn.test.mjs +78 -0
- package/scripts/cache-buildup.mjs +401 -0
- package/scripts/headless-guard.mjs +10 -3
- package/scripts/hub-ensure.mjs +120 -120
- package/scripts/lib/mcp-filter.mjs +720 -720
- package/scripts/lib/remote-spawn-transfer.mjs +196 -0
- package/scripts/mcp-check.mjs +237 -127
- package/scripts/mcp-gateway-ensure.mjs +6 -8
- package/scripts/mcp-gateway-integration-test.mjs +228 -0
- package/scripts/mcp-gateway-start.mjs +42 -16
- package/scripts/mcp-gateway-start.ps1 +17 -14
- package/scripts/mcp-gateway-verify.mjs +1 -1
- package/scripts/preflight-cache.mjs +137 -137
- package/scripts/remote-spawn.mjs +404 -40
- package/scripts/setup.mjs +18 -0
- package/scripts/tfx-route-worker.mjs +165 -165
- package/scripts/tfx-route.sh +85 -16
- package/scripts/token-snapshot.mjs +575 -575
- package/skills/tfx-analysis/SKILL.md +101 -101
- package/skills/tfx-autopilot/SKILL.md +112 -112
- package/skills/tfx-autoresearch/SKILL.md +1 -0
- package/skills/tfx-autoroute/SKILL.md +184 -184
- package/skills/tfx-codex-swarm/SKILL.md +449 -0
- package/skills/tfx-codex-swarm/evals/evals.json +26 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/benchmark.json +33 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/eval_metadata.json +42 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/grading.json +11 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/outputs/analysis.md +87 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/outputs/classification.md +35 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/outputs/commands.sh +275 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/outputs/routing.md +56 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/timing.json +5 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/grading.json +11 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/outputs/analysis.md +92 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/outputs/classification.md +71 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/outputs/commands.sh +264 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/outputs/routing.md +113 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/timing.json +5 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/eval_metadata.json +32 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/grading.json +9 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/outputs/analysis.md +96 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/outputs/classification.md +38 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/outputs/commands.sh +151 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/outputs/routing.md +51 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/timing.json +5 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/grading.json +9 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/outputs/analysis.md +127 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/outputs/classification.md +57 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/outputs/commands.sh +129 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/outputs/routing.md +84 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/timing.json +5 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/eval_metadata.json +27 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/grading.json +8 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/outputs/analysis.md +98 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/outputs/classification.md +65 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/outputs/commands.sh +123 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/outputs/routing.md +66 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/timing.json +5 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/grading.json +8 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/outputs/analysis.md +88 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/outputs/classification.md +40 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/outputs/commands.sh +130 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/outputs/routing.md +61 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/timing.json +5 -0
- package/skills/tfx-profile/SKILL.md +141 -51
- package/skills/tfx-qa/SKILL.md +117 -117
- package/skills/tfx-remote-spawn/references/hosts.json +16 -16
- package/skills/tfx-review/SKILL.md +51 -51
- package/skills/tfx-setup/SKILL.md +20 -0
- package/tui/gemini-profile.mjs +254 -0
- package/tui/setup.mjs +36 -1
- package/scripts/claude-logged.ps1 +0 -54
- package/scripts/demo-tui.mjs +0 -59
- package/skills/.omc/state/agent-replay-8f0e10a9-9693-4410-96f5-a6b07e8ed995.jsonl +0 -1
- package/skills/.omc/state/idle-notif-cooldown.json +0 -3
- package/skills/.omc/state/last-tool-error.json +0 -7
- package/skills/.omc/state/subagent-tracking.json +0 -7
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "fs";
|
|
2
|
+
import { basename, dirname, isAbsolute, resolve } from "path";
|
|
3
|
+
|
|
4
|
+
const URL_LIKE_RE = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//u;
|
|
5
|
+
const WINDOWS_ABS_RE = /^[a-zA-Z]:[\\/]/u;
|
|
6
|
+
|
|
7
|
+
function normalizeMarkdownTarget(raw) {
|
|
8
|
+
const trimmed = String(raw || "").trim();
|
|
9
|
+
const match = /^<?([^>\s]+)>?(?:\s+["'][^"']*["'])?$/u.exec(trimmed);
|
|
10
|
+
return match ? match[1] : trimmed;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function sanitizeToken(raw) {
|
|
14
|
+
let token = String(raw || "").trim();
|
|
15
|
+
if (!token) return "";
|
|
16
|
+
token = token.replace(/^[<(]+/u, "").replace(/[)>.,;:!?]+$/u, "").trim();
|
|
17
|
+
return token;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isLikelyPathToken(token) {
|
|
21
|
+
if (!token || URL_LIKE_RE.test(token)) return false;
|
|
22
|
+
if (token.startsWith("app://") || token.startsWith("plugin://") || token.startsWith("mention://")) return false;
|
|
23
|
+
if (token === "~") return false;
|
|
24
|
+
|
|
25
|
+
const hasPathSeparator = token.includes("/") || token.includes("\\");
|
|
26
|
+
const hasDotPrefix = token.startsWith("./") || token.startsWith("../") || token.startsWith(".");
|
|
27
|
+
const isHomeRelative = token.startsWith("~/") || token.startsWith("~\\");
|
|
28
|
+
const isWindowsAbs = WINDOWS_ABS_RE.test(token);
|
|
29
|
+
const hasFileLikeSuffix = /\.[a-zA-Z0-9]{1,16}$/u.test(token);
|
|
30
|
+
|
|
31
|
+
return hasPathSeparator || hasDotPrefix || isHomeRelative || isWindowsAbs || hasFileLikeSuffix;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function extractExplicitFileTokens(text) {
|
|
35
|
+
const content = String(text || "");
|
|
36
|
+
const candidates = [];
|
|
37
|
+
|
|
38
|
+
for (const match of content.matchAll(/\[[^\]]+\]\(([^)\n]+)\)/gu)) {
|
|
39
|
+
candidates.push(normalizeMarkdownTarget(match[1]));
|
|
40
|
+
}
|
|
41
|
+
for (const match of content.matchAll(/`([^`\n]+)`/gu)) {
|
|
42
|
+
candidates.push(match[1]);
|
|
43
|
+
}
|
|
44
|
+
for (const match of content.matchAll(/"([^"\n]+)"/gu)) {
|
|
45
|
+
candidates.push(match[1]);
|
|
46
|
+
}
|
|
47
|
+
for (const match of content.matchAll(/'([^'\n]+)'/gu)) {
|
|
48
|
+
candidates.push(match[1]);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const unique = new Set();
|
|
52
|
+
for (const raw of candidates) {
|
|
53
|
+
const token = sanitizeToken(raw);
|
|
54
|
+
if (!isLikelyPathToken(token)) continue;
|
|
55
|
+
unique.add(token);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return Array.from(unique);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function validateTransferFile(filePath, maxBytes) {
|
|
62
|
+
if (!existsSync(filePath)) {
|
|
63
|
+
throw new Error(`referenced file not found: ${filePath}`);
|
|
64
|
+
}
|
|
65
|
+
const stats = statSync(filePath);
|
|
66
|
+
if (!stats.isFile()) {
|
|
67
|
+
throw new Error(`referenced path is not a file: ${filePath}`);
|
|
68
|
+
}
|
|
69
|
+
if (stats.size > maxBytes) {
|
|
70
|
+
throw new Error(`referenced file too large: ${stats.size} bytes (max ${maxBytes}) for ${filePath}`);
|
|
71
|
+
}
|
|
72
|
+
return stats.size;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function resolveReferencePath(token, handoffAbsPath, cwd) {
|
|
76
|
+
if (WINDOWS_ABS_RE.test(token) || isAbsolute(token)) {
|
|
77
|
+
return resolve(token);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (token.startsWith("~/") || token.startsWith("~\\")) {
|
|
81
|
+
if (!process.env.HOME && !process.env.USERPROFILE) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
85
|
+
return resolve(homeDir, token.slice(2));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const handoffDir = dirname(handoffAbsPath);
|
|
89
|
+
const primary = resolve(handoffDir, token);
|
|
90
|
+
if (existsSync(primary)) {
|
|
91
|
+
return primary;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const fallback = resolve(cwd, token);
|
|
95
|
+
return existsSync(fallback) ? fallback : primary;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function toRemotePath(stageRoot, relativePath) {
|
|
99
|
+
const root = String(stageRoot).replace(/\\/gu, "/").replace(/\/+$/u, "");
|
|
100
|
+
const rel = String(relativePath).replace(/\\/gu, "/").replace(/^\/+/, "");
|
|
101
|
+
return `${root}/${rel}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function buildRemoteTransferPlan(options = {}) {
|
|
105
|
+
const {
|
|
106
|
+
cwd = process.cwd(),
|
|
107
|
+
handoffPath = null,
|
|
108
|
+
maxBytes,
|
|
109
|
+
remoteStageRoot,
|
|
110
|
+
userPrompt = null,
|
|
111
|
+
maxReferenceFiles = 32,
|
|
112
|
+
} = options;
|
|
113
|
+
|
|
114
|
+
if (!handoffPath) {
|
|
115
|
+
return {
|
|
116
|
+
prompt: userPrompt || "",
|
|
117
|
+
replacements: [],
|
|
118
|
+
stagedHandoffPath: null,
|
|
119
|
+
transfers: [],
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!remoteStageRoot) {
|
|
124
|
+
throw new Error("remoteStageRoot is required when handoffPath is provided");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!Number.isFinite(maxBytes) || maxBytes <= 0) {
|
|
128
|
+
throw new Error("maxBytes must be a positive number");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const absoluteHandoffPath = resolve(cwd, handoffPath);
|
|
132
|
+
validateTransferFile(absoluteHandoffPath, maxBytes);
|
|
133
|
+
const handoffContent = readFileSync(absoluteHandoffPath, "utf8").trim();
|
|
134
|
+
|
|
135
|
+
const stagedHandoffPath = toRemotePath(remoteStageRoot, `handoff/${basename(absoluteHandoffPath)}`);
|
|
136
|
+
const transfers = [{
|
|
137
|
+
localPath: absoluteHandoffPath,
|
|
138
|
+
remotePath: stagedHandoffPath,
|
|
139
|
+
type: "handoff",
|
|
140
|
+
}];
|
|
141
|
+
|
|
142
|
+
const tokens = extractExplicitFileTokens(handoffContent);
|
|
143
|
+
if (tokens.length > maxReferenceFiles) {
|
|
144
|
+
throw new Error(`too many referenced files: ${tokens.length} (max ${maxReferenceFiles})`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const stagedByLocalPath = new Map();
|
|
148
|
+
const replacements = [];
|
|
149
|
+
let fileIndex = 0;
|
|
150
|
+
|
|
151
|
+
for (const token of tokens) {
|
|
152
|
+
const resolvedPath = resolveReferencePath(token, absoluteHandoffPath, cwd);
|
|
153
|
+
validateTransferFile(resolvedPath, maxBytes);
|
|
154
|
+
|
|
155
|
+
if (!stagedByLocalPath.has(resolvedPath)) {
|
|
156
|
+
fileIndex += 1;
|
|
157
|
+
const stagedPath = toRemotePath(
|
|
158
|
+
remoteStageRoot,
|
|
159
|
+
`refs/${String(fileIndex).padStart(2, "0")}-${basename(resolvedPath)}`,
|
|
160
|
+
);
|
|
161
|
+
stagedByLocalPath.set(resolvedPath, stagedPath);
|
|
162
|
+
transfers.push({
|
|
163
|
+
localPath: resolvedPath,
|
|
164
|
+
remotePath: stagedPath,
|
|
165
|
+
type: "reference",
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
replacements.push({ token, stagedPath: stagedByLocalPath.get(resolvedPath) });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const replacementEntries = Array.from(
|
|
173
|
+
replacements
|
|
174
|
+
.reduce((map, entry) => map.set(entry.token, entry.stagedPath), new Map())
|
|
175
|
+
.entries(),
|
|
176
|
+
).sort((a, b) => b[0].length - a[0].length);
|
|
177
|
+
|
|
178
|
+
let rewrittenHandoff = handoffContent;
|
|
179
|
+
for (const [token, stagedPath] of replacementEntries) {
|
|
180
|
+
rewrittenHandoff = rewrittenHandoff.split(token).join(stagedPath);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const prefix = `Staged handoff file: ${stagedHandoffPath}`;
|
|
184
|
+
let prompt = rewrittenHandoff ? `${prefix}\n\n${rewrittenHandoff}` : prefix;
|
|
185
|
+
|
|
186
|
+
if (userPrompt) {
|
|
187
|
+
prompt = `${prompt}\n\n---\n\n${userPrompt}`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
prompt,
|
|
192
|
+
replacements: replacementEntries.map(([token, stagedPath]) => ({ stagedPath, token })),
|
|
193
|
+
stagedHandoffPath,
|
|
194
|
+
transfers,
|
|
195
|
+
};
|
|
196
|
+
}
|
package/scripts/mcp-check.mjs
CHANGED
|
@@ -1,127 +1,237 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// MCP inventory cache for dynamic MCP filtering.
|
|
3
|
-
|
|
4
|
-
import { execSync } from 'node:child_process';
|
|
5
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
6
|
-
import { homedir } from 'node:os';
|
|
7
|
-
import { join } from 'node:path';
|
|
8
|
-
import { fileURLToPath } from 'node:url';
|
|
9
|
-
|
|
10
|
-
import { normalizeServerMetadata } from './lib/mcp-server-catalog.mjs';
|
|
11
|
-
|
|
12
|
-
const CACHE_DIR = join(homedir(), '.claude', 'cache');
|
|
13
|
-
const CACHE_FILE = join(CACHE_DIR, 'mcp-inventory.json');
|
|
14
|
-
|
|
15
|
-
function countConfiguredTools(config = {}, fallbackToolCount = 0) {
|
|
16
|
-
const directKeys = ['tools', 'toolNames', 'allowedTools', 'includeTools'];
|
|
17
|
-
for (const key of directKeys) {
|
|
18
|
-
if (Array.isArray(config[key])) return config[key].length;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
if (Array.isArray(config.excludeTools)) {
|
|
22
|
-
return Math.max(0, fallbackToolCount - config.excludeTools.length);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
return fallbackToolCount;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function createServerRecord(name, status, config = {}) {
|
|
29
|
-
const normalizedName = typeof name === 'string' ? name.trim() : '';
|
|
30
|
-
const fallback = normalizeServerMetadata(normalizedName, {});
|
|
31
|
-
const toolCount = countConfiguredTools(config, fallback.tool_count);
|
|
32
|
-
const domainTags = Array.isArray(config.domain_tags)
|
|
33
|
-
? config.domain_tags
|
|
34
|
-
: Array.isArray(config.domainTags)
|
|
35
|
-
? config.domainTags
|
|
36
|
-
: [];
|
|
37
|
-
|
|
38
|
-
const metadata = normalizeServerMetadata(normalizedName, {
|
|
39
|
-
...config,
|
|
40
|
-
tool_count: toolCount,
|
|
41
|
-
domain_tags: domainTags,
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
return {
|
|
45
|
-
name: normalizedName,
|
|
46
|
-
status,
|
|
47
|
-
tool_count: metadata.tool_count,
|
|
48
|
-
domain_tags: metadata.domain_tags,
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function getCodexMcp() {
|
|
53
|
-
try {
|
|
54
|
-
const output = execSync('codex mcp list', {
|
|
55
|
-
encoding: 'utf8',
|
|
56
|
-
timeout: 15000,
|
|
57
|
-
stdio: ['pipe', 'pipe', 'ignore'],
|
|
58
|
-
windowsHide: true,
|
|
59
|
-
});
|
|
60
|
-
const lines = output.trim().split(/\r?\n/).filter((line) => line.trim());
|
|
61
|
-
if (lines.length < 2) return [];
|
|
62
|
-
|
|
63
|
-
const servers = [];
|
|
64
|
-
for (let i = 1; i < lines.length; i += 1) {
|
|
65
|
-
const cols = lines[i].split(/\s{2,}/);
|
|
66
|
-
if (cols.length < 2) continue;
|
|
67
|
-
|
|
68
|
-
const name = cols[0].trim();
|
|
69
|
-
const statusMatch = lines[i].match(/\b(enabled|disabled)\b/i);
|
|
70
|
-
const status = statusMatch ? statusMatch[1].toLowerCase() : 'unknown';
|
|
71
|
-
if (!name || name.startsWith('-')) continue;
|
|
72
|
-
servers.push(createServerRecord(name, status));
|
|
73
|
-
}
|
|
74
|
-
return servers;
|
|
75
|
-
} catch {
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export function getGeminiMcp() {
|
|
81
|
-
try {
|
|
82
|
-
const settingsPath = join(homedir(), '.gemini', 'settings.json');
|
|
83
|
-
if (!existsSync(settingsPath)) return null;
|
|
84
|
-
|
|
85
|
-
const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
|
|
86
|
-
const mcpServers = settings.mcpServers || {};
|
|
87
|
-
return Object.entries(mcpServers).map(([name, config]) => createServerRecord(name, 'configured', config || {}));
|
|
88
|
-
} catch {
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (!
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// MCP inventory cache for dynamic MCP filtering.
|
|
3
|
+
|
|
4
|
+
import { execSync } from 'node:child_process';
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { homedir } from 'node:os';
|
|
7
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
|
|
10
|
+
import { MCP_SERVER_TOOL_CATALOG, normalizeServerMetadata } from './lib/mcp-server-catalog.mjs';
|
|
11
|
+
|
|
12
|
+
const CACHE_DIR = join(homedir(), '.claude', 'cache');
|
|
13
|
+
const CACHE_FILE = join(CACHE_DIR, 'mcp-inventory.json');
|
|
14
|
+
|
|
15
|
+
function countConfiguredTools(config = {}, fallbackToolCount = 0) {
|
|
16
|
+
const directKeys = ['tools', 'toolNames', 'allowedTools', 'includeTools'];
|
|
17
|
+
for (const key of directKeys) {
|
|
18
|
+
if (Array.isArray(config[key])) return config[key].length;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (Array.isArray(config.excludeTools)) {
|
|
22
|
+
return Math.max(0, fallbackToolCount - config.excludeTools.length);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return fallbackToolCount;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function createServerRecord(name, status, config = {}) {
|
|
29
|
+
const normalizedName = typeof name === 'string' ? name.trim() : '';
|
|
30
|
+
const fallback = normalizeServerMetadata(normalizedName, {});
|
|
31
|
+
const toolCount = countConfiguredTools(config, fallback.tool_count);
|
|
32
|
+
const domainTags = Array.isArray(config.domain_tags)
|
|
33
|
+
? config.domain_tags
|
|
34
|
+
: Array.isArray(config.domainTags)
|
|
35
|
+
? config.domainTags
|
|
36
|
+
: [];
|
|
37
|
+
|
|
38
|
+
const metadata = normalizeServerMetadata(normalizedName, {
|
|
39
|
+
...config,
|
|
40
|
+
tool_count: toolCount,
|
|
41
|
+
domain_tags: domainTags,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
name: normalizedName,
|
|
46
|
+
status,
|
|
47
|
+
tool_count: metadata.tool_count,
|
|
48
|
+
domain_tags: metadata.domain_tags,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function getCodexMcp() {
|
|
53
|
+
try {
|
|
54
|
+
const output = execSync('codex mcp list', {
|
|
55
|
+
encoding: 'utf8',
|
|
56
|
+
timeout: 15000,
|
|
57
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
58
|
+
windowsHide: true,
|
|
59
|
+
});
|
|
60
|
+
const lines = output.trim().split(/\r?\n/).filter((line) => line.trim());
|
|
61
|
+
if (lines.length < 2) return [];
|
|
62
|
+
|
|
63
|
+
const servers = [];
|
|
64
|
+
for (let i = 1; i < lines.length; i += 1) {
|
|
65
|
+
const cols = lines[i].split(/\s{2,}/);
|
|
66
|
+
if (cols.length < 2) continue;
|
|
67
|
+
|
|
68
|
+
const name = cols[0].trim();
|
|
69
|
+
const statusMatch = lines[i].match(/\b(enabled|disabled)\b/i);
|
|
70
|
+
const status = statusMatch ? statusMatch[1].toLowerCase() : 'unknown';
|
|
71
|
+
if (!name || name.startsWith('-')) continue;
|
|
72
|
+
servers.push(createServerRecord(name, status));
|
|
73
|
+
}
|
|
74
|
+
return servers;
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function getGeminiMcp() {
|
|
81
|
+
try {
|
|
82
|
+
const settingsPath = join(homedir(), '.gemini', 'settings.json');
|
|
83
|
+
if (!existsSync(settingsPath)) return null;
|
|
84
|
+
|
|
85
|
+
const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
|
|
86
|
+
const mcpServers = settings.mcpServers || {};
|
|
87
|
+
return Object.entries(mcpServers).map(([name, config]) => createServerRecord(name, 'configured', config || {}));
|
|
88
|
+
} catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── Claude MCP 서버 발견 ──
|
|
94
|
+
|
|
95
|
+
const CLAUDE_DIR = join(homedir(), '.claude');
|
|
96
|
+
|
|
97
|
+
// Anthropic 클라우드 호스팅 MCP 서버 — Claude Code 런타임에서 항상 가용.
|
|
98
|
+
// mcp-server-catalog.mjs의 MCP_SERVER_TOOL_CATALOG에 정의된 서버 중
|
|
99
|
+
// 로컬 설치가 불필요한 클라우드 제공 서버.
|
|
100
|
+
const WELL_KNOWN_CLOUD_SERVERS = Object.freeze([
|
|
101
|
+
'brave-search',
|
|
102
|
+
'exa',
|
|
103
|
+
'context7',
|
|
104
|
+
]);
|
|
105
|
+
|
|
106
|
+
function readJsonSafe(filePath) {
|
|
107
|
+
try {
|
|
108
|
+
return JSON.parse(readFileSync(filePath, 'utf8'));
|
|
109
|
+
} catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function extractMcpServerNames(mcpJsonPath) {
|
|
115
|
+
const data = readJsonSafe(mcpJsonPath);
|
|
116
|
+
if (!data?.mcpServers || typeof data.mcpServers !== 'object') return [];
|
|
117
|
+
return Object.entries(data.mcpServers)
|
|
118
|
+
.filter(([name]) => typeof name === 'string' && name.trim())
|
|
119
|
+
.map(([name, config]) => ({ name: name.trim(), config: config || {} }));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function walkUpForMcpJson(startDir, maxDepth = 5) {
|
|
123
|
+
const found = [];
|
|
124
|
+
let dir = resolve(startDir);
|
|
125
|
+
for (let i = 0; i < maxDepth; i += 1) {
|
|
126
|
+
const candidate = join(dir, '.mcp.json');
|
|
127
|
+
if (existsSync(candidate)) found.push(candidate);
|
|
128
|
+
const parent = dirname(dir);
|
|
129
|
+
if (parent === dir) break;
|
|
130
|
+
dir = parent;
|
|
131
|
+
}
|
|
132
|
+
return found;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function scanPluginsMcpJson() {
|
|
136
|
+
const pluginsDir = join(CLAUDE_DIR, 'plugins');
|
|
137
|
+
if (!existsSync(pluginsDir)) return [];
|
|
138
|
+
const found = [];
|
|
139
|
+
const walk = (dir, depth = 0) => {
|
|
140
|
+
if (depth > 4) return;
|
|
141
|
+
try {
|
|
142
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
143
|
+
if (entry.name === 'node_modules' || entry.name === '.git') continue;
|
|
144
|
+
const full = join(dir, entry.name);
|
|
145
|
+
if (entry.isFile() && entry.name === '.mcp.json') {
|
|
146
|
+
found.push(full);
|
|
147
|
+
} else if (entry.isDirectory()) {
|
|
148
|
+
walk(full, depth + 1);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
} catch { /* permission errors */ }
|
|
152
|
+
};
|
|
153
|
+
walk(pluginsDir);
|
|
154
|
+
return found;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function getClaudeMcp(cwd = process.cwd()) {
|
|
158
|
+
const serverMap = new Map();
|
|
159
|
+
|
|
160
|
+
// 1) .mcp.json 파일 트리 스캔 (CWD → root)
|
|
161
|
+
for (const mcpJson of walkUpForMcpJson(cwd)) {
|
|
162
|
+
for (const { name, config } of extractMcpServerNames(mcpJson)) {
|
|
163
|
+
if (!serverMap.has(name)) {
|
|
164
|
+
serverMap.set(name, createServerRecord(name, 'configured', config));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 2) ~/.claude/plugins/ 스캔
|
|
170
|
+
for (const mcpJson of scanPluginsMcpJson()) {
|
|
171
|
+
for (const { name, config } of extractMcpServerNames(mcpJson)) {
|
|
172
|
+
if (!serverMap.has(name)) {
|
|
173
|
+
serverMap.set(name, createServerRecord(name, 'configured', config));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 3) ~/.claude/settings.json + settings.local.json의 mcpServers
|
|
179
|
+
for (const settingsFile of ['settings.json', 'settings.local.json']) {
|
|
180
|
+
const data = readJsonSafe(join(CLAUDE_DIR, settingsFile));
|
|
181
|
+
if (!data?.mcpServers) continue;
|
|
182
|
+
for (const [name, config] of Object.entries(data.mcpServers)) {
|
|
183
|
+
if (typeof name !== 'string' || !name.trim()) continue;
|
|
184
|
+
if (!serverMap.has(name.trim())) {
|
|
185
|
+
serverMap.set(name.trim(), createServerRecord(name.trim(), 'configured', config || {}));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// 4) Well-known Anthropic 클라우드 MCP 서버 (카탈로그에 정의된 것만)
|
|
191
|
+
for (const name of WELL_KNOWN_CLOUD_SERVERS) {
|
|
192
|
+
if (serverMap.has(name)) continue;
|
|
193
|
+
if (!MCP_SERVER_TOOL_CATALOG[name]) continue;
|
|
194
|
+
serverMap.set(name, createServerRecord(name, 'available'));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return [...serverMap.values()];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function buildInventory(cwd = process.cwd()) {
|
|
201
|
+
const inventory = {
|
|
202
|
+
timestamp: new Date().toISOString(),
|
|
203
|
+
codex: { available: false, servers: [] },
|
|
204
|
+
gemini: { available: false, servers: [] },
|
|
205
|
+
claude: { available: true, servers: [] },
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const codexServers = getCodexMcp();
|
|
209
|
+
if (codexServers !== null) {
|
|
210
|
+
inventory.codex.available = true;
|
|
211
|
+
inventory.codex.servers = codexServers;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const geminiServers = getGeminiMcp();
|
|
215
|
+
if (geminiServers !== null) {
|
|
216
|
+
inventory.gemini.available = true;
|
|
217
|
+
inventory.gemini.servers = geminiServers;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
inventory.claude.servers = getClaudeMcp(cwd);
|
|
221
|
+
|
|
222
|
+
return inventory;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function writeInventoryCache(inventory = buildInventory()) {
|
|
226
|
+
if (!existsSync(CACHE_DIR)) mkdirSync(CACHE_DIR, { recursive: true });
|
|
227
|
+
writeFileSync(CACHE_FILE, JSON.stringify(inventory, null, 2));
|
|
228
|
+
return inventory;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function main() {
|
|
232
|
+
writeInventoryCache();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
236
|
+
main();
|
|
237
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { existsSync, readFileSync } from 'node:fs';
|
|
6
6
|
import { join, dirname } from 'node:path';
|
|
7
|
-
import {
|
|
7
|
+
import { execSync } from 'node:child_process';
|
|
8
8
|
import { tmpdir } from 'node:os';
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
10
10
|
|
|
@@ -35,18 +35,16 @@ function hasManifest() {
|
|
|
35
35
|
return existsSync(PID_FILE);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
/** mcp-gateway-start.mjs를
|
|
38
|
+
/** mcp-gateway-start.mjs를 독립 프로세스로 기동 */
|
|
39
39
|
function startGateway() {
|
|
40
40
|
const scriptPath = join(PLUGIN_ROOT, 'scripts', 'mcp-gateway-start.mjs');
|
|
41
41
|
if (!existsSync(scriptPath)) return false;
|
|
42
42
|
|
|
43
43
|
try {
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
});
|
|
49
|
-
child.unref();
|
|
44
|
+
// PowerShell Start-Process: Windows Job Object에서 벗어나 부모 종료 후 생존
|
|
45
|
+
execSync(
|
|
46
|
+
`powershell -NoProfile -Command "Start-Process -WindowStyle Hidden -FilePath '${process.execPath}' -ArgumentList '${scriptPath.replaceAll("'", "''")}'"`
|
|
47
|
+
, { stdio: 'ignore', timeout: 10000 });
|
|
50
48
|
return true;
|
|
51
49
|
} catch {
|
|
52
50
|
return false;
|