claudex-setup 1.7.0 → 1.8.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.
- package/CHANGELOG.md +14 -0
- package/LICENSE +21 -0
- package/README.md +34 -12
- package/bin/cli.js +35 -4
- package/package.json +3 -2
- package/src/analyze.js +26 -0
- package/src/audit.js +2 -2
- package/src/benchmark.js +81 -7
- package/src/context.js +3 -2
- package/src/domain-packs.js +160 -0
- package/src/governance.js +156 -0
- package/src/index.js +8 -0
- package/src/mcp-packs.js +85 -0
- package/src/plans.js +329 -59
- package/src/setup.js +60 -36
- package/src/techniques.js +19 -6
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
const DOMAIN_PACKS = [
|
|
2
|
+
{
|
|
3
|
+
key: 'baseline-general',
|
|
4
|
+
label: 'Baseline General',
|
|
5
|
+
useWhen: 'General repos that need a pragmatic Claude baseline without domain-specific assumptions.',
|
|
6
|
+
recommendedModules: ['CLAUDE.md baseline', 'verification', 'safe-write profile'],
|
|
7
|
+
recommendedMcpPacks: ['context7-docs'],
|
|
8
|
+
benchmarkFocus: ['discover next actions', 'starter-safe improvement', 'governed rollout'],
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
key: 'backend-api',
|
|
12
|
+
label: 'Backend API',
|
|
13
|
+
useWhen: 'Service, API, or backend-heavy repos with routes, services, jobs, or data access.',
|
|
14
|
+
recommendedModules: ['verification', 'security workflow', 'commands', 'rules'],
|
|
15
|
+
recommendedMcpPacks: ['context7-docs'],
|
|
16
|
+
benchmarkFocus: ['test + build verification', 'security review workflow', 'safe apply on existing config'],
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
key: 'frontend-ui',
|
|
20
|
+
label: 'Frontend UI',
|
|
21
|
+
useWhen: 'React, Next.js, Vue, Angular, or Svelte repos with components and UI-heavy workflows.',
|
|
22
|
+
recommendedModules: ['frontend rules', 'design guidance', 'commands', 'benchmark'],
|
|
23
|
+
recommendedMcpPacks: ['context7-docs', 'next-devtools'],
|
|
24
|
+
benchmarkFocus: ['build checks', 'component workflow quality', 'framework-aware starter output'],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
key: 'data-pipeline',
|
|
28
|
+
label: 'Data Pipeline',
|
|
29
|
+
useWhen: 'Repos with workers, DAGs, marts, ETL jobs, migrations, or analytics-heavy workflows.',
|
|
30
|
+
recommendedModules: ['verification', 'rules', 'agents', 'benchmark'],
|
|
31
|
+
recommendedMcpPacks: ['context7-docs'],
|
|
32
|
+
benchmarkFocus: ['pipeline safety', 'repeatable task flows', 'state-aware review artifacts'],
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
key: 'infra-platform',
|
|
36
|
+
label: 'Infra Platform',
|
|
37
|
+
useWhen: 'Terraform, Docker, Kubernetes, Wrangler, or deployment-oriented repos.',
|
|
38
|
+
recommendedModules: ['ci-devops', 'commands', 'governance', 'benchmark'],
|
|
39
|
+
recommendedMcpPacks: ['context7-docs'],
|
|
40
|
+
benchmarkFocus: ['release safety', 'policy-controlled rollout', 'infra verification loops'],
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
key: 'oss-library',
|
|
44
|
+
label: 'OSS Library',
|
|
45
|
+
useWhen: 'Public packages or contributor-heavy repos that need a lighter governance footprint.',
|
|
46
|
+
recommendedModules: ['suggest-only profile', 'light rules', 'commands', 'README-aligned CLAUDE.md'],
|
|
47
|
+
recommendedMcpPacks: ['context7-docs'],
|
|
48
|
+
benchmarkFocus: ['low-footprint adoption', 'manual review friendliness', 'contributor-safe defaults'],
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
key: 'enterprise-governed',
|
|
52
|
+
label: 'Enterprise Governed',
|
|
53
|
+
useWhen: 'Repos with CI, permissions, hooks, and a need for auditable change controls.',
|
|
54
|
+
recommendedModules: ['governance', 'activity artifacts', 'rollback manifests', 'benchmark evidence'],
|
|
55
|
+
recommendedMcpPacks: ['context7-docs'],
|
|
56
|
+
benchmarkFocus: ['policy-aware rollout', 'approval flow readiness', 'benchmark export quality'],
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
function uniqueByKey(items) {
|
|
61
|
+
const seen = new Set();
|
|
62
|
+
const result = [];
|
|
63
|
+
for (const item of items) {
|
|
64
|
+
if (seen.has(item.key)) continue;
|
|
65
|
+
seen.add(item.key);
|
|
66
|
+
result.push(item);
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function detectDomainPacks(ctx, stacks, assets = null) {
|
|
72
|
+
const stackKeys = new Set((stacks || []).map(stack => stack.key));
|
|
73
|
+
const pkg = ctx.jsonFile('package.json') || {};
|
|
74
|
+
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
75
|
+
const matches = [];
|
|
76
|
+
|
|
77
|
+
function addMatch(key, reasons) {
|
|
78
|
+
const pack = DOMAIN_PACKS.find(item => item.key === key);
|
|
79
|
+
if (!pack) return;
|
|
80
|
+
matches.push({
|
|
81
|
+
...pack,
|
|
82
|
+
matchReasons: reasons.filter(Boolean).slice(0, 3),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const hasFrontend = stackKeys.has('react') || stackKeys.has('nextjs') || stackKeys.has('vue') ||
|
|
87
|
+
stackKeys.has('angular') || stackKeys.has('svelte') || ctx.hasDir('components') || ctx.hasDir('app') || ctx.hasDir('pages');
|
|
88
|
+
const hasBackend = stackKeys.has('node') || stackKeys.has('python') || stackKeys.has('django') ||
|
|
89
|
+
stackKeys.has('fastapi') || stackKeys.has('go') || stackKeys.has('rust') || stackKeys.has('java') ||
|
|
90
|
+
ctx.hasDir('api') || ctx.hasDir('routes') || ctx.hasDir('services') || ctx.hasDir('controllers');
|
|
91
|
+
const hasData = ctx.hasDir('dags') || ctx.hasDir('jobs') || ctx.hasDir('workers') ||
|
|
92
|
+
ctx.hasDir('models') || ctx.hasDir('migrations') || ctx.hasDir('db') ||
|
|
93
|
+
deps.dbt || deps['apache-airflow'] || deps.pandas || deps.polars || deps.duckdb;
|
|
94
|
+
const hasInfra = stackKeys.has('docker') || stackKeys.has('terraform') || stackKeys.has('kubernetes') ||
|
|
95
|
+
ctx.files.includes('wrangler.toml') || ctx.files.includes('serverless.yml') || ctx.files.includes('serverless.yaml') ||
|
|
96
|
+
ctx.files.includes('cdk.json') || ctx.hasDir('infra') || ctx.hasDir('deploy') || ctx.hasDir('helm');
|
|
97
|
+
const isOss = !!ctx.fileContent('LICENSE') && !!ctx.fileContent('CONTRIBUTING.md') && pkg.private !== true;
|
|
98
|
+
const isEnterpriseGoverned = !!(assets && assets.permissions && assets.permissions.hasDenyRules) &&
|
|
99
|
+
!!(assets && assets.files && assets.files.settings) && ctx.hasDir('.github/workflows');
|
|
100
|
+
|
|
101
|
+
if (hasBackend) {
|
|
102
|
+
addMatch('backend-api', [
|
|
103
|
+
'Detected backend stack or service directories.',
|
|
104
|
+
ctx.hasDir('api') ? 'API-facing structure detected.' : null,
|
|
105
|
+
ctx.hasDir('services') ? 'Service-layer directories detected.' : null,
|
|
106
|
+
]);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (hasFrontend) {
|
|
110
|
+
addMatch('frontend-ui', [
|
|
111
|
+
'Detected frontend stack or UI directories.',
|
|
112
|
+
ctx.hasDir('components') ? 'Component directories detected.' : null,
|
|
113
|
+
stackKeys.has('nextjs') ? 'Next.js stack detected.' : null,
|
|
114
|
+
]);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (hasData) {
|
|
118
|
+
addMatch('data-pipeline', [
|
|
119
|
+
'Detected worker, jobs, models, or analytics-style structure.',
|
|
120
|
+
ctx.hasDir('jobs') ? 'Job/pipeline directories detected.' : null,
|
|
121
|
+
ctx.hasDir('migrations') ? 'Migration flow detected.' : null,
|
|
122
|
+
]);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (hasInfra) {
|
|
126
|
+
addMatch('infra-platform', [
|
|
127
|
+
'Detected deployment or infrastructure signals.',
|
|
128
|
+
ctx.files.includes('wrangler.toml') ? 'Wrangler deployment config detected.' : null,
|
|
129
|
+
ctx.hasDir('deploy') ? 'Deployment directory detected.' : null,
|
|
130
|
+
]);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (isOss) {
|
|
134
|
+
addMatch('oss-library', [
|
|
135
|
+
'License and contribution guidance suggest an open-source repo.',
|
|
136
|
+
pkg.private === false ? 'package.json is not marked private.' : null,
|
|
137
|
+
]);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (isEnterpriseGoverned) {
|
|
141
|
+
addMatch('enterprise-governed', [
|
|
142
|
+
'Settings, deny rules, and CI indicate a governed team workflow.',
|
|
143
|
+
'Repo already has policy-aware Claude assets.',
|
|
144
|
+
]);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const deduped = uniqueByKey(matches);
|
|
148
|
+
if (deduped.length === 0) {
|
|
149
|
+
return [{
|
|
150
|
+
...DOMAIN_PACKS.find(item => item.key === 'baseline-general'),
|
|
151
|
+
matchReasons: ['No stronger domain signal detected yet.'],
|
|
152
|
+
}];
|
|
153
|
+
}
|
|
154
|
+
return deduped;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
module.exports = {
|
|
158
|
+
DOMAIN_PACKS,
|
|
159
|
+
detectDomainPacks,
|
|
160
|
+
};
|
package/src/governance.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
const { DOMAIN_PACKS } = require('./domain-packs');
|
|
2
|
+
const { MCP_PACKS, mergeMcpServers, normalizeMcpPackKeys } = require('./mcp-packs');
|
|
3
|
+
|
|
1
4
|
const PERMISSION_PROFILES = [
|
|
2
5
|
{
|
|
3
6
|
key: 'read-only',
|
|
@@ -137,11 +140,147 @@ const PILOT_ROLLOUT_KIT = {
|
|
|
137
140
|
],
|
|
138
141
|
};
|
|
139
142
|
|
|
143
|
+
function clone(value) {
|
|
144
|
+
return JSON.parse(JSON.stringify(value));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function mergeUnique(existing = [], additions = []) {
|
|
148
|
+
return [...new Set([...(Array.isArray(existing) ? existing : []), ...additions])];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function mergeHooks(existingHooks = {}, nextHooks = {}) {
|
|
152
|
+
const merged = clone(existingHooks || {});
|
|
153
|
+
|
|
154
|
+
for (const [stage, blocks] of Object.entries(nextHooks)) {
|
|
155
|
+
const targetBlocks = Array.isArray(merged[stage]) ? clone(merged[stage]) : [];
|
|
156
|
+
for (const incoming of blocks) {
|
|
157
|
+
const index = targetBlocks.findIndex(block => block.matcher === incoming.matcher);
|
|
158
|
+
if (index === -1) {
|
|
159
|
+
targetBlocks.push(clone(incoming));
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const current = targetBlocks[index];
|
|
164
|
+
const existingCommands = new Set((current.hooks || []).map(hook => `${hook.type}:${hook.command}:${hook.timeout || ''}`));
|
|
165
|
+
const mergedHooks = [...(current.hooks || [])];
|
|
166
|
+
for (const hook of incoming.hooks || []) {
|
|
167
|
+
const signature = `${hook.type}:${hook.command}:${hook.timeout || ''}`;
|
|
168
|
+
if (!existingCommands.has(signature)) {
|
|
169
|
+
mergedHooks.push(clone(hook));
|
|
170
|
+
existingCommands.add(signature);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
targetBlocks[index] = { ...current, hooks: mergedHooks };
|
|
174
|
+
}
|
|
175
|
+
merged[stage] = targetBlocks;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return merged;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function getPermissionProfile(key = 'safe-write') {
|
|
182
|
+
return PERMISSION_PROFILES.find(profile => profile.key === key) ||
|
|
183
|
+
PERMISSION_PROFILES.find(profile => profile.key === 'safe-write');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function isWritableProfile(key = 'safe-write') {
|
|
187
|
+
return ['safe-write', 'power-user', 'internal-research'].includes(getPermissionProfile(key).key);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function ensureWritableProfile(key = 'safe-write', commandName = 'apply', dryRun = false) {
|
|
191
|
+
const profile = getPermissionProfile(key);
|
|
192
|
+
if (!dryRun && !isWritableProfile(profile.key)) {
|
|
193
|
+
throw new Error(`${commandName} requires a writable profile. Use --profile safe-write or --dry-run.`);
|
|
194
|
+
}
|
|
195
|
+
return profile;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function buildHookConfig(hookFiles, profileKey) {
|
|
199
|
+
const profile = getPermissionProfile(profileKey);
|
|
200
|
+
if (!isWritableProfile(profile.key)) {
|
|
201
|
+
return {};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const uniqueFiles = [...new Set(hookFiles)].sort();
|
|
205
|
+
if (uniqueFiles.length === 0) {
|
|
206
|
+
return {};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const hookConfig = {
|
|
210
|
+
PostToolUse: [{
|
|
211
|
+
matcher: 'Write|Edit',
|
|
212
|
+
hooks: uniqueFiles
|
|
213
|
+
.filter(file => file !== 'protect-secrets.sh' && file !== 'session-start.sh')
|
|
214
|
+
.map(file => ({
|
|
215
|
+
type: 'command',
|
|
216
|
+
command: `bash .claude/hooks/${file}`,
|
|
217
|
+
timeout: 10,
|
|
218
|
+
})),
|
|
219
|
+
}],
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
if (uniqueFiles.includes('protect-secrets.sh')) {
|
|
223
|
+
hookConfig.PreToolUse = [{
|
|
224
|
+
matcher: 'Read|Write|Edit',
|
|
225
|
+
hooks: [{
|
|
226
|
+
type: 'command',
|
|
227
|
+
command: 'bash .claude/hooks/protect-secrets.sh',
|
|
228
|
+
timeout: 5,
|
|
229
|
+
}],
|
|
230
|
+
}];
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (uniqueFiles.includes('session-start.sh')) {
|
|
234
|
+
hookConfig.SessionStart = [{
|
|
235
|
+
matcher: '*',
|
|
236
|
+
hooks: [{
|
|
237
|
+
type: 'command',
|
|
238
|
+
command: 'bash .claude/hooks/session-start.sh',
|
|
239
|
+
timeout: 5,
|
|
240
|
+
}],
|
|
241
|
+
}];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if ((hookConfig.PostToolUse[0].hooks || []).length === 0) {
|
|
245
|
+
delete hookConfig.PostToolUse;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return hookConfig;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function buildSettingsForProfile({ profileKey = 'safe-write', hookFiles = [], existingSettings = null, mcpPackKeys = [] } = {}) {
|
|
252
|
+
const profile = getPermissionProfile(profileKey);
|
|
253
|
+
const base = existingSettings ? clone(existingSettings) : {};
|
|
254
|
+
const selectedMcpPacks = normalizeMcpPackKeys(mcpPackKeys);
|
|
255
|
+
base.permissions = base.permissions || {};
|
|
256
|
+
base.permissions.defaultMode = profile.defaultMode;
|
|
257
|
+
base.permissions.deny = mergeUnique(base.permissions.deny, profile.deny);
|
|
258
|
+
|
|
259
|
+
const hookConfig = buildHookConfig(hookFiles, profile.key);
|
|
260
|
+
if (Object.keys(hookConfig).length > 0) {
|
|
261
|
+
base.hooks = mergeHooks(base.hooks, hookConfig);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (selectedMcpPacks.length > 0) {
|
|
265
|
+
base.mcpServers = mergeMcpServers(base.mcpServers, selectedMcpPacks);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
base.claudexSetup = {
|
|
269
|
+
...(base.claudexSetup || {}),
|
|
270
|
+
profile: profile.key,
|
|
271
|
+
mcpPacks: selectedMcpPacks,
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
return base;
|
|
275
|
+
}
|
|
276
|
+
|
|
140
277
|
function getGovernanceSummary() {
|
|
141
278
|
return {
|
|
142
279
|
permissionProfiles: PERMISSION_PROFILES,
|
|
143
280
|
hookRegistry: HOOK_REGISTRY,
|
|
144
281
|
policyPacks: POLICY_PACKS,
|
|
282
|
+
domainPacks: DOMAIN_PACKS,
|
|
283
|
+
mcpPacks: MCP_PACKS,
|
|
145
284
|
pilotRolloutKit: PILOT_ROLLOUT_KIT,
|
|
146
285
|
};
|
|
147
286
|
}
|
|
@@ -179,6 +318,18 @@ function printGovernanceSummary(summary, options = {}) {
|
|
|
179
318
|
}
|
|
180
319
|
console.log('');
|
|
181
320
|
|
|
321
|
+
console.log(' Domain Packs');
|
|
322
|
+
for (const pack of summary.domainPacks) {
|
|
323
|
+
console.log(` - ${pack.label}: ${pack.useWhen}`);
|
|
324
|
+
}
|
|
325
|
+
console.log('');
|
|
326
|
+
|
|
327
|
+
console.log(' MCP Packs');
|
|
328
|
+
for (const pack of summary.mcpPacks) {
|
|
329
|
+
console.log(` - ${pack.label}: ${Object.keys(pack.servers).join(', ')}`);
|
|
330
|
+
}
|
|
331
|
+
console.log('');
|
|
332
|
+
|
|
182
333
|
console.log(' Pilot Rollout Kit');
|
|
183
334
|
for (const item of summary.pilotRolloutKit.recommendedScope) {
|
|
184
335
|
console.log(` - ${item}`);
|
|
@@ -187,6 +338,11 @@ function printGovernanceSummary(summary, options = {}) {
|
|
|
187
338
|
}
|
|
188
339
|
|
|
189
340
|
module.exports = {
|
|
341
|
+
PERMISSION_PROFILES,
|
|
342
|
+
getPermissionProfile,
|
|
343
|
+
isWritableProfile,
|
|
344
|
+
ensureWritableProfile,
|
|
345
|
+
buildSettingsForProfile,
|
|
190
346
|
getGovernanceSummary,
|
|
191
347
|
printGovernanceSummary,
|
|
192
348
|
};
|
package/src/index.js
CHANGED
|
@@ -4,6 +4,8 @@ const { analyzeProject } = require('./analyze');
|
|
|
4
4
|
const { buildProposalBundle, applyProposalBundle } = require('./plans');
|
|
5
5
|
const { getGovernanceSummary } = require('./governance');
|
|
6
6
|
const { runBenchmark } = require('./benchmark');
|
|
7
|
+
const { DOMAIN_PACKS, detectDomainPacks } = require('./domain-packs');
|
|
8
|
+
const { MCP_PACKS, getMcpPack, mergeMcpServers, recommendMcpPacks } = require('./mcp-packs');
|
|
7
9
|
|
|
8
10
|
module.exports = {
|
|
9
11
|
audit,
|
|
@@ -13,4 +15,10 @@ module.exports = {
|
|
|
13
15
|
applyProposalBundle,
|
|
14
16
|
getGovernanceSummary,
|
|
15
17
|
runBenchmark,
|
|
18
|
+
DOMAIN_PACKS,
|
|
19
|
+
detectDomainPacks,
|
|
20
|
+
MCP_PACKS,
|
|
21
|
+
getMcpPack,
|
|
22
|
+
mergeMcpServers,
|
|
23
|
+
recommendMcpPacks,
|
|
16
24
|
};
|
package/src/mcp-packs.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const MCP_PACKS = [
|
|
2
|
+
{
|
|
3
|
+
key: 'context7-docs',
|
|
4
|
+
label: 'Context7 Docs',
|
|
5
|
+
useWhen: 'Repos that benefit from live, current framework and library documentation during Claude sessions.',
|
|
6
|
+
adoption: 'Safe default docs pack for most application repos.',
|
|
7
|
+
servers: {
|
|
8
|
+
context7: {
|
|
9
|
+
command: 'npx',
|
|
10
|
+
args: ['-y', '@upstash/context7-mcp@latest'],
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
key: 'next-devtools',
|
|
16
|
+
label: 'Next.js Devtools',
|
|
17
|
+
useWhen: 'Next.js repos that need runtime-aware debugging and framework-specific tooling.',
|
|
18
|
+
adoption: 'Useful companion pack for frontend-ui repos running Next.js.',
|
|
19
|
+
servers: {
|
|
20
|
+
'next-devtools': {
|
|
21
|
+
command: 'npx',
|
|
22
|
+
args: ['-y', 'next-devtools-mcp@latest'],
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
function clone(value) {
|
|
29
|
+
return JSON.parse(JSON.stringify(value));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getMcpPack(key) {
|
|
33
|
+
return MCP_PACKS.find(pack => pack.key === key) || null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function normalizeMcpPackKeys(keys = []) {
|
|
37
|
+
return [...new Set((Array.isArray(keys) ? keys : [])
|
|
38
|
+
.map(key => `${key}`.trim())
|
|
39
|
+
.filter(Boolean))]
|
|
40
|
+
.filter(key => !!getMcpPack(key));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function mergeMcpServers(existing = {}, packKeys = []) {
|
|
44
|
+
const merged = clone(existing || {});
|
|
45
|
+
for (const key of normalizeMcpPackKeys(packKeys)) {
|
|
46
|
+
const pack = getMcpPack(key);
|
|
47
|
+
if (!pack) continue;
|
|
48
|
+
for (const [serverName, serverConfig] of Object.entries(pack.servers || {})) {
|
|
49
|
+
if (!merged[serverName]) {
|
|
50
|
+
merged[serverName] = clone(serverConfig);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return merged;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function recommendMcpPacks(stacks = [], domainPacks = []) {
|
|
58
|
+
const recommended = new Set();
|
|
59
|
+
const stackKeys = new Set(stacks.map(stack => stack.key));
|
|
60
|
+
|
|
61
|
+
for (const pack of domainPacks) {
|
|
62
|
+
for (const key of pack.recommendedMcpPacks || []) {
|
|
63
|
+
recommended.add(key);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (stackKeys.has('nextjs')) {
|
|
68
|
+
recommended.add('next-devtools');
|
|
69
|
+
}
|
|
70
|
+
if (stackKeys.size > 0) {
|
|
71
|
+
recommended.add('context7-docs');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return MCP_PACKS
|
|
75
|
+
.filter(pack => recommended.has(pack.key))
|
|
76
|
+
.map(pack => clone(pack));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = {
|
|
80
|
+
MCP_PACKS,
|
|
81
|
+
getMcpPack,
|
|
82
|
+
normalizeMcpPackKeys,
|
|
83
|
+
mergeMcpServers,
|
|
84
|
+
recommendMcpPacks,
|
|
85
|
+
};
|