@nerviq/cli 1.20.0 → 1.21.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/LICENSE +23 -23
- package/README.md +2 -2
- package/bin/cli.js +1 -0
- package/package.json +2 -1
- package/src/activity.js +1039 -1039
- package/src/adoption-advisor.js +299 -299
- package/src/aider/config-parser.js +166 -166
- package/src/aider/context.js +6 -2
- package/src/aider/deep-review.js +316 -316
- package/src/aider/domain-packs.js +303 -303
- package/src/aider/freshness.js +93 -93
- package/src/aider/governance.js +253 -253
- package/src/aider/interactive.js +334 -334
- package/src/aider/mcp-packs.js +329 -329
- package/src/aider/patch.js +214 -214
- package/src/aider/plans.js +186 -186
- package/src/aider/premium.js +360 -360
- package/src/aider/setup.js +404 -404
- package/src/aider/techniques.js +312 -67
- package/src/analyze.js +951 -951
- package/src/anti-patterns.js +485 -485
- package/src/audit/instruction-files.js +180 -180
- package/src/audit/recommendations.js +577 -577
- package/src/auto-suggest.js +154 -154
- package/src/badge.js +13 -13
- package/src/behavioral-drift.js +801 -801
- package/src/benchmark.js +67 -67
- package/src/catalog.js +103 -103
- package/src/certification.js +128 -128
- package/src/codex/config-parser.js +183 -183
- package/src/codex/context.js +223 -223
- package/src/codex/deep-review.js +493 -493
- package/src/codex/domain-packs.js +394 -394
- package/src/codex/freshness.js +84 -84
- package/src/codex/governance.js +192 -192
- package/src/codex/interactive.js +618 -618
- package/src/codex/mcp-packs.js +914 -914
- package/src/codex/patch.js +209 -209
- package/src/codex/plans.js +251 -251
- package/src/codex/premium.js +614 -614
- package/src/codex/setup.js +591 -591
- package/src/context.js +10 -4
- package/src/continuous-ops.js +681 -681
- package/src/copilot/activity.js +309 -309
- package/src/copilot/deep-review.js +346 -346
- package/src/copilot/domain-packs.js +372 -372
- package/src/copilot/freshness.js +57 -57
- package/src/copilot/governance.js +222 -222
- package/src/copilot/interactive.js +406 -406
- package/src/copilot/mcp-packs.js +826 -826
- package/src/copilot/plans.js +253 -253
- package/src/copilot/premium.js +451 -451
- package/src/copilot/setup.js +488 -488
- package/src/cost-tracking.js +61 -61
- package/src/cursor/activity.js +301 -301
- package/src/cursor/config-parser.js +265 -265
- package/src/cursor/context.js +256 -256
- package/src/cursor/deep-review.js +334 -334
- package/src/cursor/domain-packs.js +368 -368
- package/src/cursor/freshness.js +65 -65
- package/src/cursor/governance.js +229 -229
- package/src/cursor/interactive.js +391 -391
- package/src/cursor/mcp-packs.js +828 -828
- package/src/cursor/plans.js +254 -254
- package/src/cursor/premium.js +469 -469
- package/src/cursor/setup.js +488 -488
- package/src/dashboard.js +493 -493
- package/src/deep-review.js +428 -428
- package/src/deprecation.js +98 -98
- package/src/diff-only.js +280 -280
- package/src/doctor.js +119 -119
- package/src/domain-pack-expansion.js +1033 -1033
- package/src/domain-packs.js +387 -387
- package/src/feedback.js +178 -178
- package/src/fix-engine.js +783 -783
- package/src/fix-prompts.js +122 -122
- package/src/formatters/sarif.js +115 -115
- package/src/freshness.js +74 -74
- package/src/gemini/config-parser.js +275 -275
- package/src/gemini/deep-review.js +559 -559
- package/src/gemini/domain-packs.js +393 -393
- package/src/gemini/freshness.js +66 -66
- package/src/gemini/governance.js +201 -201
- package/src/gemini/interactive.js +860 -860
- package/src/gemini/mcp-packs.js +915 -915
- package/src/gemini/plans.js +269 -269
- package/src/gemini/premium.js +760 -760
- package/src/gemini/setup.js +692 -692
- package/src/governance.js +72 -72
- package/src/harmony/add.js +68 -68
- package/src/harmony/advisor.js +333 -333
- package/src/harmony/canon.js +565 -565
- package/src/harmony/cli.js +591 -591
- package/src/harmony/drift.js +401 -401
- package/src/harmony/governance.js +313 -313
- package/src/harmony/memory.js +239 -239
- package/src/harmony/sync.js +475 -475
- package/src/harmony/watch.js +370 -370
- package/src/hook-validation.js +342 -342
- package/src/index.js +271 -271
- package/src/init.js +184 -184
- package/src/instruction-surfaces.js +185 -185
- package/src/integrations.js +144 -144
- package/src/interactive.js +118 -118
- package/src/locales/en.json +1 -1
- package/src/locales/es.json +1 -1
- package/src/mcp-packs.js +830 -830
- package/src/mcp-server.js +726 -726
- package/src/mcp-validation.js +337 -337
- package/src/nerviq-sync.json +7 -7
- package/src/opencode/config-parser.js +109 -109
- package/src/opencode/context.js +247 -247
- package/src/opencode/deep-review.js +313 -313
- package/src/opencode/domain-packs.js +262 -262
- package/src/opencode/freshness.js +66 -66
- package/src/opencode/governance.js +159 -159
- package/src/opencode/interactive.js +392 -392
- package/src/opencode/mcp-packs.js +705 -705
- package/src/opencode/patch.js +184 -184
- package/src/opencode/plans.js +231 -231
- package/src/opencode/premium.js +413 -413
- package/src/opencode/setup.js +449 -449
- package/src/opencode/techniques.js +27 -27
- package/src/operating-profile.js +574 -574
- package/src/org.js +152 -152
- package/src/permission-rules.js +218 -218
- package/src/plans.js +839 -839
- package/src/platform-change-manifest.js +86 -86
- package/src/plugins.js +110 -110
- package/src/policy-layers.js +210 -210
- package/src/profiles.js +124 -124
- package/src/prompt-injection.js +74 -74
- package/src/public-api.js +173 -173
- package/src/recommendation-rules.js +84 -84
- package/src/repo-archetype.js +386 -386
- package/src/secret-patterns.js +39 -39
- package/src/server.js +527 -527
- package/src/setup/analysis.js +607 -607
- package/src/setup/runtime.js +172 -172
- package/src/setup.js +677 -677
- package/src/shared/capabilities.js +194 -194
- package/src/source-urls.js +132 -132
- package/src/stack-checks.js +565 -565
- package/src/supplemental-checks.js +13 -13
- package/src/synergy/adaptive.js +261 -261
- package/src/synergy/compensation.js +137 -137
- package/src/synergy/evidence.js +193 -193
- package/src/synergy/learning.js +199 -199
- package/src/synergy/patterns.js +227 -227
- package/src/synergy/ranking.js +83 -83
- package/src/synergy/report.js +165 -165
- package/src/synergy/routing.js +146 -146
- package/src/techniques/api.js +407 -407
- package/src/techniques/automation.js +316 -316
- package/src/techniques/compliance.js +257 -257
- package/src/techniques/hygiene.js +294 -294
- package/src/techniques/instructions.js +243 -243
- package/src/techniques/observability.js +226 -226
- package/src/techniques/optimization.js +142 -142
- package/src/techniques/quality.js +318 -318
- package/src/techniques/security.js +237 -237
- package/src/techniques/shared.js +443 -443
- package/src/techniques/stacks.js +2294 -2294
- package/src/techniques/tools.js +106 -106
- package/src/techniques/workflow.js +413 -413
- package/src/techniques.js +81 -81
- package/src/terminology.js +73 -73
- package/src/token-estimate.js +35 -35
- package/src/usage-patterns.js +99 -99
- package/src/verification-metadata.js +145 -145
- package/src/watch.js +247 -247
- package/src/windsurf/activity.js +302 -302
- package/src/windsurf/config-parser.js +267 -267
- package/src/windsurf/context.js +120 -10
- package/src/windsurf/deep-review.js +337 -337
- package/src/windsurf/domain-packs.js +370 -370
- package/src/windsurf/freshness.js +36 -36
- package/src/windsurf/governance.js +231 -231
- package/src/windsurf/interactive.js +388 -388
- package/src/windsurf/mcp-packs.js +792 -792
- package/src/windsurf/plans.js +247 -247
- package/src/windsurf/premium.js +468 -468
- package/src/windsurf/setup.js +471 -471
- package/src/windsurf/techniques.js +155 -33
- package/src/workspace.js +375 -375
package/src/mcp-validation.js
CHANGED
|
@@ -1,337 +1,337 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const os = require('os');
|
|
5
|
-
const path = require('path');
|
|
6
|
-
const { spawnSync } = require('child_process');
|
|
7
|
-
const { ProjectContext } = require('./context');
|
|
8
|
-
const { CodexProjectContext } = require('./codex/context');
|
|
9
|
-
const { GeminiProjectContext } = require('./gemini/context');
|
|
10
|
-
const { CopilotProjectContext } = require('./copilot/context');
|
|
11
|
-
const { CursorProjectContext } = require('./cursor/context');
|
|
12
|
-
const { WindsurfProjectContext } = require('./windsurf/context');
|
|
13
|
-
const { OpenCodeProjectContext } = require('./opencode/context');
|
|
14
|
-
|
|
15
|
-
function readJsonFile(filePath) {
|
|
16
|
-
try {
|
|
17
|
-
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
18
|
-
} catch {
|
|
19
|
-
return null;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function normalizeSourcePath(filePath) {
|
|
24
|
-
if (!filePath) return null;
|
|
25
|
-
const homeDir = os.homedir();
|
|
26
|
-
if (filePath.startsWith(homeDir)) {
|
|
27
|
-
return filePath.replace(homeDir, '~');
|
|
28
|
-
}
|
|
29
|
-
return filePath;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function normalizeServerConfig(rawConfig) {
|
|
33
|
-
const raw = rawConfig && typeof rawConfig === 'object' ? rawConfig : {};
|
|
34
|
-
let command = raw.command || null;
|
|
35
|
-
let args = Array.isArray(raw.args) ? raw.args.map((item) => `${item}`) : [];
|
|
36
|
-
|
|
37
|
-
if (Array.isArray(command)) {
|
|
38
|
-
const commandParts = command.map((item) => `${item}`);
|
|
39
|
-
command = commandParts[0] || null;
|
|
40
|
-
args = [...commandParts.slice(1), ...args];
|
|
41
|
-
} else if (typeof command !== 'string') {
|
|
42
|
-
command = null;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const env = raw.env && typeof raw.env === 'object'
|
|
46
|
-
? raw.env
|
|
47
|
-
: raw.environment && typeof raw.environment === 'object'
|
|
48
|
-
? raw.environment
|
|
49
|
-
: {};
|
|
50
|
-
|
|
51
|
-
const urlCandidates = [raw.url, raw.endpoint, raw.serverUrl, raw.sseUrl, raw.httpUrl];
|
|
52
|
-
const url = urlCandidates.find((value) => typeof value === 'string' && value.trim()) || null;
|
|
53
|
-
const transport = typeof raw.transport === 'string'
|
|
54
|
-
? raw.transport.toLowerCase()
|
|
55
|
-
: (url ? 'remote' : 'stdio');
|
|
56
|
-
|
|
57
|
-
return {
|
|
58
|
-
command: typeof command === 'string' ? command.trim() : null,
|
|
59
|
-
args,
|
|
60
|
-
env,
|
|
61
|
-
url: typeof url === 'string' ? url.trim() : null,
|
|
62
|
-
transport,
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function extractEnvReferences(config) {
|
|
67
|
-
const references = new Set();
|
|
68
|
-
const pattern = /\$\{(?:env:)?([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
69
|
-
|
|
70
|
-
const scan = (value) => {
|
|
71
|
-
if (typeof value !== 'string') return;
|
|
72
|
-
for (const match of value.matchAll(pattern)) {
|
|
73
|
-
references.add(match[1]);
|
|
74
|
-
}
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
scan(config.command);
|
|
78
|
-
scan(config.url);
|
|
79
|
-
for (const arg of config.args || []) scan(arg);
|
|
80
|
-
for (const value of Object.values(config.env || {})) scan(value);
|
|
81
|
-
|
|
82
|
-
return [...references];
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function resolveExecutable(command, dir) {
|
|
86
|
-
if (!command) {
|
|
87
|
-
return { found: false, resolved: null };
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const trimmed = command.trim();
|
|
91
|
-
const isPathLike = /^[A-Za-z]:[\\/]/.test(trimmed) ||
|
|
92
|
-
trimmed.startsWith('.') ||
|
|
93
|
-
trimmed.includes('/') ||
|
|
94
|
-
trimmed.includes('\\');
|
|
95
|
-
|
|
96
|
-
if (isPathLike) {
|
|
97
|
-
const resolved = path.isAbsolute(trimmed) ? trimmed : path.resolve(dir, trimmed);
|
|
98
|
-
return { found: fs.existsSync(resolved), resolved };
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const lookup = process.platform === 'win32'
|
|
102
|
-
? spawnSync('where.exe', [trimmed], { encoding: 'utf8' })
|
|
103
|
-
: spawnSync('which', [trimmed], { encoding: 'utf8' });
|
|
104
|
-
const output = `${lookup.stdout || ''}`.trim().split(/\r?\n/).filter(Boolean)[0] || null;
|
|
105
|
-
|
|
106
|
-
return {
|
|
107
|
-
found: lookup.status === 0 && Boolean(output),
|
|
108
|
-
resolved: output,
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
async function probeRemoteUrl(targetUrl) {
|
|
113
|
-
try {
|
|
114
|
-
const parsed = new URL(targetUrl);
|
|
115
|
-
if (!/^https?:$/.test(parsed.protocol)) {
|
|
116
|
-
return { status: 'fail', detail: `unsupported protocol ${parsed.protocol}` };
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const controller = new AbortController();
|
|
120
|
-
const timer = setTimeout(() => controller.abort(), 2000);
|
|
121
|
-
let response;
|
|
122
|
-
try {
|
|
123
|
-
response = await fetch(targetUrl, { method: 'GET', signal: controller.signal });
|
|
124
|
-
} finally {
|
|
125
|
-
clearTimeout(timer);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (response.status >= 500) {
|
|
129
|
-
return { status: 'warn', detail: `HTTP ${response.status} ${response.statusText}` };
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return { status: 'pass', detail: `HTTP ${response.status} ${response.statusText}` };
|
|
133
|
-
} catch (error) {
|
|
134
|
-
if (error && error.name === 'AbortError') {
|
|
135
|
-
return { status: 'warn', detail: 'timed out after 2000ms' };
|
|
136
|
-
}
|
|
137
|
-
return { status: 'warn', detail: error && error.message ? error.message : 'request failed' };
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function pushDeclarations(target, platform, scope, source, servers, note = null) {
|
|
142
|
-
if (!servers || typeof servers !== 'object') return;
|
|
143
|
-
|
|
144
|
-
for (const [serverName, rawConfig] of Object.entries(servers)) {
|
|
145
|
-
target.push({
|
|
146
|
-
platform,
|
|
147
|
-
scope,
|
|
148
|
-
source: normalizeSourcePath(source),
|
|
149
|
-
serverName,
|
|
150
|
-
note,
|
|
151
|
-
config: normalizeServerConfig(rawConfig),
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function collectDeclaredMcpServers(dir, detectedPlatforms = []) {
|
|
157
|
-
const declarations = [];
|
|
158
|
-
const platformSet = new Set(detectedPlatforms);
|
|
159
|
-
|
|
160
|
-
if (platformSet.has('claude')) {
|
|
161
|
-
const claudeCtx = new ProjectContext(dir);
|
|
162
|
-
const projectConfig = claudeCtx.jsonFile('.mcp.json');
|
|
163
|
-
if (projectConfig && projectConfig.mcpServers) {
|
|
164
|
-
pushDeclarations(declarations, 'claude', 'project', '.mcp.json', projectConfig.mcpServers);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const globalConfigPath = path.join(os.homedir(), '.claude.json');
|
|
168
|
-
const globalConfig = readJsonFile(globalConfigPath);
|
|
169
|
-
if (globalConfig && globalConfig.mcpServers) {
|
|
170
|
-
pushDeclarations(declarations, 'claude', 'global', globalConfigPath, globalConfig.mcpServers);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
if (platformSet.has('codex')) {
|
|
175
|
-
const ctx = new CodexProjectContext(dir);
|
|
176
|
-
const projectConfig = ctx.configToml();
|
|
177
|
-
if (projectConfig.ok && projectConfig.data && projectConfig.data.mcp_servers) {
|
|
178
|
-
pushDeclarations(declarations, 'codex', 'project', projectConfig.source, projectConfig.data.mcp_servers);
|
|
179
|
-
}
|
|
180
|
-
const globalConfig = ctx.globalConfigToml();
|
|
181
|
-
if (globalConfig.ok && globalConfig.data && globalConfig.data.mcp_servers) {
|
|
182
|
-
pushDeclarations(declarations, 'codex', 'global', globalConfig.source, globalConfig.data.mcp_servers);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if (platformSet.has('gemini')) {
|
|
187
|
-
const ctx = new GeminiProjectContext(dir);
|
|
188
|
-
const projectConfig = ctx.settingsJson();
|
|
189
|
-
if (projectConfig.ok && projectConfig.data && projectConfig.data.mcpServers) {
|
|
190
|
-
pushDeclarations(declarations, 'gemini', 'project', projectConfig.source, projectConfig.data.mcpServers);
|
|
191
|
-
}
|
|
192
|
-
const globalConfig = ctx.globalSettingsJson();
|
|
193
|
-
if (globalConfig.ok && globalConfig.data && globalConfig.data.mcpServers) {
|
|
194
|
-
pushDeclarations(declarations, 'gemini', 'global', globalConfig.source, globalConfig.data.mcpServers);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
if (platformSet.has('copilot')) {
|
|
199
|
-
const ctx = new CopilotProjectContext(dir);
|
|
200
|
-
const projectConfig = ctx.mcpConfig();
|
|
201
|
-
if (projectConfig.ok && projectConfig.data) {
|
|
202
|
-
pushDeclarations(
|
|
203
|
-
declarations,
|
|
204
|
-
'copilot',
|
|
205
|
-
'project',
|
|
206
|
-
projectConfig.source,
|
|
207
|
-
projectConfig.data.servers || projectConfig.data.mcpServers || {}
|
|
208
|
-
);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
if (platformSet.has('cursor')) {
|
|
213
|
-
const ctx = new CursorProjectContext(dir);
|
|
214
|
-
const projectConfig = ctx.mcpConfig();
|
|
215
|
-
if (projectConfig.ok && projectConfig.data) {
|
|
216
|
-
pushDeclarations(declarations, 'cursor', 'project', projectConfig.source, projectConfig.data.mcpServers || {});
|
|
217
|
-
}
|
|
218
|
-
const globalConfig = ctx.globalMcpConfig();
|
|
219
|
-
if (globalConfig.ok && globalConfig.data) {
|
|
220
|
-
pushDeclarations(declarations, 'cursor', 'global', globalConfig.source, globalConfig.data.mcpServers || {});
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
if (platformSet.has('windsurf')) {
|
|
225
|
-
const ctx = new WindsurfProjectContext(dir);
|
|
226
|
-
const projectConfig = ctx.mcpConfig();
|
|
227
|
-
if (projectConfig.ok && projectConfig.data) {
|
|
228
|
-
pushDeclarations(
|
|
229
|
-
declarations,
|
|
230
|
-
'windsurf',
|
|
231
|
-
'project',
|
|
232
|
-
projectConfig.source,
|
|
233
|
-
projectConfig.data.mcpServers || {},
|
|
234
|
-
'Current Windsurf runtime may still rely on global MCP config.'
|
|
235
|
-
);
|
|
236
|
-
}
|
|
237
|
-
const globalConfig = ctx.globalMcpConfig();
|
|
238
|
-
if (globalConfig.ok && globalConfig.data) {
|
|
239
|
-
pushDeclarations(declarations, 'windsurf', 'global', globalConfig.source, globalConfig.data.mcpServers || {});
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
if (platformSet.has('opencode')) {
|
|
244
|
-
const ctx = new OpenCodeProjectContext(dir);
|
|
245
|
-
const projectConfig = ctx.configJson();
|
|
246
|
-
if (projectConfig.ok && projectConfig.data && projectConfig.data.mcp) {
|
|
247
|
-
pushDeclarations(declarations, 'opencode', 'project', projectConfig.source, projectConfig.data.mcp);
|
|
248
|
-
}
|
|
249
|
-
const globalConfig = ctx.globalConfigJson();
|
|
250
|
-
if (globalConfig.ok && globalConfig.data && globalConfig.data.mcp) {
|
|
251
|
-
pushDeclarations(declarations, 'opencode', 'global', globalConfig.source, globalConfig.data.mcp);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
return declarations;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
async function validateDeclaredMcpServers({ dir, detectedPlatforms = [] }) {
|
|
259
|
-
const declarations = collectDeclaredMcpServers(dir, detectedPlatforms);
|
|
260
|
-
const checks = [];
|
|
261
|
-
|
|
262
|
-
for (const declaration of declarations) {
|
|
263
|
-
const missingEnv = extractEnvReferences(declaration.config)
|
|
264
|
-
.filter((name) => !Object.prototype.hasOwnProperty.call(process.env, name));
|
|
265
|
-
const entry = {
|
|
266
|
-
platform: declaration.platform,
|
|
267
|
-
scope: declaration.scope,
|
|
268
|
-
source: declaration.source,
|
|
269
|
-
serverName: declaration.serverName,
|
|
270
|
-
transport: declaration.config.transport,
|
|
271
|
-
command: declaration.config.command,
|
|
272
|
-
args: declaration.config.args,
|
|
273
|
-
url: declaration.config.url,
|
|
274
|
-
missingEnv,
|
|
275
|
-
status: 'fail',
|
|
276
|
-
mode: 'unknown',
|
|
277
|
-
detail: '',
|
|
278
|
-
fix: null,
|
|
279
|
-
};
|
|
280
|
-
|
|
281
|
-
if (declaration.config.command) {
|
|
282
|
-
entry.mode = 'command';
|
|
283
|
-
const resolution = resolveExecutable(declaration.config.command, dir);
|
|
284
|
-
if (!resolution.found) {
|
|
285
|
-
entry.status = 'fail';
|
|
286
|
-
entry.detail = `Command '${declaration.config.command}' was not found in PATH or at the declared path.`;
|
|
287
|
-
entry.fix = 'Install the MCP server command or correct the configured command path.';
|
|
288
|
-
} else if (missingEnv.length > 0) {
|
|
289
|
-
entry.status = 'warn';
|
|
290
|
-
entry.detail = `Command '${declaration.config.command}' resolves to ${resolution.resolved}, but referenced env vars are missing: ${missingEnv.join(', ')}.`;
|
|
291
|
-
entry.fix = `Set the missing env vars before starting this MCP server: ${missingEnv.join(', ')}.`;
|
|
292
|
-
} else {
|
|
293
|
-
entry.status = 'pass';
|
|
294
|
-
entry.detail = `Command '${declaration.config.command}' resolves to ${resolution.resolved}.`;
|
|
295
|
-
}
|
|
296
|
-
} else if (declaration.config.url) {
|
|
297
|
-
entry.mode = 'remote';
|
|
298
|
-
const probe = await probeRemoteUrl(declaration.config.url);
|
|
299
|
-
entry.status = probe.status;
|
|
300
|
-
entry.detail = `URL ${declaration.config.url} ${probe.status === 'pass' ? 'responded' : 'was not confirmed'} (${probe.detail}).`;
|
|
301
|
-
entry.fix = probe.status === 'pass'
|
|
302
|
-
? null
|
|
303
|
-
: 'Start the remote MCP endpoint locally or update the configured URL.';
|
|
304
|
-
if (missingEnv.length > 0) {
|
|
305
|
-
entry.status = entry.status === 'fail' ? 'fail' : 'warn';
|
|
306
|
-
entry.detail += ` Missing env vars: ${missingEnv.join(', ')}.`;
|
|
307
|
-
if (!entry.fix) {
|
|
308
|
-
entry.fix = `Set the missing env vars before starting this MCP server: ${missingEnv.join(', ')}.`;
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
} else {
|
|
312
|
-
entry.mode = 'unknown';
|
|
313
|
-
entry.status = 'fail';
|
|
314
|
-
entry.detail = 'Declared MCP server has neither a command nor a URL to validate.';
|
|
315
|
-
entry.fix = 'Add a valid command/args pair or a reachable URL-based transport definition.';
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
if (declaration.note) {
|
|
319
|
-
entry.detail += ` ${declaration.note}`;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
checks.push(entry);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
return {
|
|
326
|
-
checks,
|
|
327
|
-
declared: checks.length,
|
|
328
|
-
pass: checks.filter((item) => item.status === 'pass').length,
|
|
329
|
-
warn: checks.filter((item) => item.status === 'warn').length,
|
|
330
|
-
fail: checks.filter((item) => item.status === 'fail').length,
|
|
331
|
-
};
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
module.exports = {
|
|
335
|
-
collectDeclaredMcpServers,
|
|
336
|
-
validateDeclaredMcpServers,
|
|
337
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { spawnSync } = require('child_process');
|
|
7
|
+
const { ProjectContext } = require('./context');
|
|
8
|
+
const { CodexProjectContext } = require('./codex/context');
|
|
9
|
+
const { GeminiProjectContext } = require('./gemini/context');
|
|
10
|
+
const { CopilotProjectContext } = require('./copilot/context');
|
|
11
|
+
const { CursorProjectContext } = require('./cursor/context');
|
|
12
|
+
const { WindsurfProjectContext } = require('./windsurf/context');
|
|
13
|
+
const { OpenCodeProjectContext } = require('./opencode/context');
|
|
14
|
+
|
|
15
|
+
function readJsonFile(filePath) {
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function normalizeSourcePath(filePath) {
|
|
24
|
+
if (!filePath) return null;
|
|
25
|
+
const homeDir = os.homedir();
|
|
26
|
+
if (filePath.startsWith(homeDir)) {
|
|
27
|
+
return filePath.replace(homeDir, '~');
|
|
28
|
+
}
|
|
29
|
+
return filePath;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizeServerConfig(rawConfig) {
|
|
33
|
+
const raw = rawConfig && typeof rawConfig === 'object' ? rawConfig : {};
|
|
34
|
+
let command = raw.command || null;
|
|
35
|
+
let args = Array.isArray(raw.args) ? raw.args.map((item) => `${item}`) : [];
|
|
36
|
+
|
|
37
|
+
if (Array.isArray(command)) {
|
|
38
|
+
const commandParts = command.map((item) => `${item}`);
|
|
39
|
+
command = commandParts[0] || null;
|
|
40
|
+
args = [...commandParts.slice(1), ...args];
|
|
41
|
+
} else if (typeof command !== 'string') {
|
|
42
|
+
command = null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const env = raw.env && typeof raw.env === 'object'
|
|
46
|
+
? raw.env
|
|
47
|
+
: raw.environment && typeof raw.environment === 'object'
|
|
48
|
+
? raw.environment
|
|
49
|
+
: {};
|
|
50
|
+
|
|
51
|
+
const urlCandidates = [raw.url, raw.endpoint, raw.serverUrl, raw.sseUrl, raw.httpUrl];
|
|
52
|
+
const url = urlCandidates.find((value) => typeof value === 'string' && value.trim()) || null;
|
|
53
|
+
const transport = typeof raw.transport === 'string'
|
|
54
|
+
? raw.transport.toLowerCase()
|
|
55
|
+
: (url ? 'remote' : 'stdio');
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
command: typeof command === 'string' ? command.trim() : null,
|
|
59
|
+
args,
|
|
60
|
+
env,
|
|
61
|
+
url: typeof url === 'string' ? url.trim() : null,
|
|
62
|
+
transport,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function extractEnvReferences(config) {
|
|
67
|
+
const references = new Set();
|
|
68
|
+
const pattern = /\$\{(?:env:)?([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
69
|
+
|
|
70
|
+
const scan = (value) => {
|
|
71
|
+
if (typeof value !== 'string') return;
|
|
72
|
+
for (const match of value.matchAll(pattern)) {
|
|
73
|
+
references.add(match[1]);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
scan(config.command);
|
|
78
|
+
scan(config.url);
|
|
79
|
+
for (const arg of config.args || []) scan(arg);
|
|
80
|
+
for (const value of Object.values(config.env || {})) scan(value);
|
|
81
|
+
|
|
82
|
+
return [...references];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function resolveExecutable(command, dir) {
|
|
86
|
+
if (!command) {
|
|
87
|
+
return { found: false, resolved: null };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const trimmed = command.trim();
|
|
91
|
+
const isPathLike = /^[A-Za-z]:[\\/]/.test(trimmed) ||
|
|
92
|
+
trimmed.startsWith('.') ||
|
|
93
|
+
trimmed.includes('/') ||
|
|
94
|
+
trimmed.includes('\\');
|
|
95
|
+
|
|
96
|
+
if (isPathLike) {
|
|
97
|
+
const resolved = path.isAbsolute(trimmed) ? trimmed : path.resolve(dir, trimmed);
|
|
98
|
+
return { found: fs.existsSync(resolved), resolved };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const lookup = process.platform === 'win32'
|
|
102
|
+
? spawnSync('where.exe', [trimmed], { encoding: 'utf8' })
|
|
103
|
+
: spawnSync('which', [trimmed], { encoding: 'utf8' });
|
|
104
|
+
const output = `${lookup.stdout || ''}`.trim().split(/\r?\n/).filter(Boolean)[0] || null;
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
found: lookup.status === 0 && Boolean(output),
|
|
108
|
+
resolved: output,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function probeRemoteUrl(targetUrl) {
|
|
113
|
+
try {
|
|
114
|
+
const parsed = new URL(targetUrl);
|
|
115
|
+
if (!/^https?:$/.test(parsed.protocol)) {
|
|
116
|
+
return { status: 'fail', detail: `unsupported protocol ${parsed.protocol}` };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const controller = new AbortController();
|
|
120
|
+
const timer = setTimeout(() => controller.abort(), 2000);
|
|
121
|
+
let response;
|
|
122
|
+
try {
|
|
123
|
+
response = await fetch(targetUrl, { method: 'GET', signal: controller.signal });
|
|
124
|
+
} finally {
|
|
125
|
+
clearTimeout(timer);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (response.status >= 500) {
|
|
129
|
+
return { status: 'warn', detail: `HTTP ${response.status} ${response.statusText}` };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { status: 'pass', detail: `HTTP ${response.status} ${response.statusText}` };
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if (error && error.name === 'AbortError') {
|
|
135
|
+
return { status: 'warn', detail: 'timed out after 2000ms' };
|
|
136
|
+
}
|
|
137
|
+
return { status: 'warn', detail: error && error.message ? error.message : 'request failed' };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function pushDeclarations(target, platform, scope, source, servers, note = null) {
|
|
142
|
+
if (!servers || typeof servers !== 'object') return;
|
|
143
|
+
|
|
144
|
+
for (const [serverName, rawConfig] of Object.entries(servers)) {
|
|
145
|
+
target.push({
|
|
146
|
+
platform,
|
|
147
|
+
scope,
|
|
148
|
+
source: normalizeSourcePath(source),
|
|
149
|
+
serverName,
|
|
150
|
+
note,
|
|
151
|
+
config: normalizeServerConfig(rawConfig),
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function collectDeclaredMcpServers(dir, detectedPlatforms = []) {
|
|
157
|
+
const declarations = [];
|
|
158
|
+
const platformSet = new Set(detectedPlatforms);
|
|
159
|
+
|
|
160
|
+
if (platformSet.has('claude')) {
|
|
161
|
+
const claudeCtx = new ProjectContext(dir);
|
|
162
|
+
const projectConfig = claudeCtx.jsonFile('.mcp.json');
|
|
163
|
+
if (projectConfig && projectConfig.mcpServers) {
|
|
164
|
+
pushDeclarations(declarations, 'claude', 'project', '.mcp.json', projectConfig.mcpServers);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const globalConfigPath = path.join(os.homedir(), '.claude.json');
|
|
168
|
+
const globalConfig = readJsonFile(globalConfigPath);
|
|
169
|
+
if (globalConfig && globalConfig.mcpServers) {
|
|
170
|
+
pushDeclarations(declarations, 'claude', 'global', globalConfigPath, globalConfig.mcpServers);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (platformSet.has('codex')) {
|
|
175
|
+
const ctx = new CodexProjectContext(dir);
|
|
176
|
+
const projectConfig = ctx.configToml();
|
|
177
|
+
if (projectConfig.ok && projectConfig.data && projectConfig.data.mcp_servers) {
|
|
178
|
+
pushDeclarations(declarations, 'codex', 'project', projectConfig.source, projectConfig.data.mcp_servers);
|
|
179
|
+
}
|
|
180
|
+
const globalConfig = ctx.globalConfigToml();
|
|
181
|
+
if (globalConfig.ok && globalConfig.data && globalConfig.data.mcp_servers) {
|
|
182
|
+
pushDeclarations(declarations, 'codex', 'global', globalConfig.source, globalConfig.data.mcp_servers);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (platformSet.has('gemini')) {
|
|
187
|
+
const ctx = new GeminiProjectContext(dir);
|
|
188
|
+
const projectConfig = ctx.settingsJson();
|
|
189
|
+
if (projectConfig.ok && projectConfig.data && projectConfig.data.mcpServers) {
|
|
190
|
+
pushDeclarations(declarations, 'gemini', 'project', projectConfig.source, projectConfig.data.mcpServers);
|
|
191
|
+
}
|
|
192
|
+
const globalConfig = ctx.globalSettingsJson();
|
|
193
|
+
if (globalConfig.ok && globalConfig.data && globalConfig.data.mcpServers) {
|
|
194
|
+
pushDeclarations(declarations, 'gemini', 'global', globalConfig.source, globalConfig.data.mcpServers);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (platformSet.has('copilot')) {
|
|
199
|
+
const ctx = new CopilotProjectContext(dir);
|
|
200
|
+
const projectConfig = ctx.mcpConfig();
|
|
201
|
+
if (projectConfig.ok && projectConfig.data) {
|
|
202
|
+
pushDeclarations(
|
|
203
|
+
declarations,
|
|
204
|
+
'copilot',
|
|
205
|
+
'project',
|
|
206
|
+
projectConfig.source,
|
|
207
|
+
projectConfig.data.servers || projectConfig.data.mcpServers || {}
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (platformSet.has('cursor')) {
|
|
213
|
+
const ctx = new CursorProjectContext(dir);
|
|
214
|
+
const projectConfig = ctx.mcpConfig();
|
|
215
|
+
if (projectConfig.ok && projectConfig.data) {
|
|
216
|
+
pushDeclarations(declarations, 'cursor', 'project', projectConfig.source, projectConfig.data.mcpServers || {});
|
|
217
|
+
}
|
|
218
|
+
const globalConfig = ctx.globalMcpConfig();
|
|
219
|
+
if (globalConfig.ok && globalConfig.data) {
|
|
220
|
+
pushDeclarations(declarations, 'cursor', 'global', globalConfig.source, globalConfig.data.mcpServers || {});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (platformSet.has('windsurf')) {
|
|
225
|
+
const ctx = new WindsurfProjectContext(dir);
|
|
226
|
+
const projectConfig = ctx.mcpConfig();
|
|
227
|
+
if (projectConfig.ok && projectConfig.data) {
|
|
228
|
+
pushDeclarations(
|
|
229
|
+
declarations,
|
|
230
|
+
'windsurf',
|
|
231
|
+
'project',
|
|
232
|
+
projectConfig.source,
|
|
233
|
+
projectConfig.data.mcpServers || {},
|
|
234
|
+
'Current Windsurf runtime may still rely on global MCP config.'
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
const globalConfig = ctx.globalMcpConfig();
|
|
238
|
+
if (globalConfig.ok && globalConfig.data) {
|
|
239
|
+
pushDeclarations(declarations, 'windsurf', 'global', globalConfig.source, globalConfig.data.mcpServers || {});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (platformSet.has('opencode')) {
|
|
244
|
+
const ctx = new OpenCodeProjectContext(dir);
|
|
245
|
+
const projectConfig = ctx.configJson();
|
|
246
|
+
if (projectConfig.ok && projectConfig.data && projectConfig.data.mcp) {
|
|
247
|
+
pushDeclarations(declarations, 'opencode', 'project', projectConfig.source, projectConfig.data.mcp);
|
|
248
|
+
}
|
|
249
|
+
const globalConfig = ctx.globalConfigJson();
|
|
250
|
+
if (globalConfig.ok && globalConfig.data && globalConfig.data.mcp) {
|
|
251
|
+
pushDeclarations(declarations, 'opencode', 'global', globalConfig.source, globalConfig.data.mcp);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return declarations;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function validateDeclaredMcpServers({ dir, detectedPlatforms = [] }) {
|
|
259
|
+
const declarations = collectDeclaredMcpServers(dir, detectedPlatforms);
|
|
260
|
+
const checks = [];
|
|
261
|
+
|
|
262
|
+
for (const declaration of declarations) {
|
|
263
|
+
const missingEnv = extractEnvReferences(declaration.config)
|
|
264
|
+
.filter((name) => !Object.prototype.hasOwnProperty.call(process.env, name));
|
|
265
|
+
const entry = {
|
|
266
|
+
platform: declaration.platform,
|
|
267
|
+
scope: declaration.scope,
|
|
268
|
+
source: declaration.source,
|
|
269
|
+
serverName: declaration.serverName,
|
|
270
|
+
transport: declaration.config.transport,
|
|
271
|
+
command: declaration.config.command,
|
|
272
|
+
args: declaration.config.args,
|
|
273
|
+
url: declaration.config.url,
|
|
274
|
+
missingEnv,
|
|
275
|
+
status: 'fail',
|
|
276
|
+
mode: 'unknown',
|
|
277
|
+
detail: '',
|
|
278
|
+
fix: null,
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
if (declaration.config.command) {
|
|
282
|
+
entry.mode = 'command';
|
|
283
|
+
const resolution = resolveExecutable(declaration.config.command, dir);
|
|
284
|
+
if (!resolution.found) {
|
|
285
|
+
entry.status = 'fail';
|
|
286
|
+
entry.detail = `Command '${declaration.config.command}' was not found in PATH or at the declared path.`;
|
|
287
|
+
entry.fix = 'Install the MCP server command or correct the configured command path.';
|
|
288
|
+
} else if (missingEnv.length > 0) {
|
|
289
|
+
entry.status = 'warn';
|
|
290
|
+
entry.detail = `Command '${declaration.config.command}' resolves to ${resolution.resolved}, but referenced env vars are missing: ${missingEnv.join(', ')}.`;
|
|
291
|
+
entry.fix = `Set the missing env vars before starting this MCP server: ${missingEnv.join(', ')}.`;
|
|
292
|
+
} else {
|
|
293
|
+
entry.status = 'pass';
|
|
294
|
+
entry.detail = `Command '${declaration.config.command}' resolves to ${resolution.resolved}.`;
|
|
295
|
+
}
|
|
296
|
+
} else if (declaration.config.url) {
|
|
297
|
+
entry.mode = 'remote';
|
|
298
|
+
const probe = await probeRemoteUrl(declaration.config.url);
|
|
299
|
+
entry.status = probe.status;
|
|
300
|
+
entry.detail = `URL ${declaration.config.url} ${probe.status === 'pass' ? 'responded' : 'was not confirmed'} (${probe.detail}).`;
|
|
301
|
+
entry.fix = probe.status === 'pass'
|
|
302
|
+
? null
|
|
303
|
+
: 'Start the remote MCP endpoint locally or update the configured URL.';
|
|
304
|
+
if (missingEnv.length > 0) {
|
|
305
|
+
entry.status = entry.status === 'fail' ? 'fail' : 'warn';
|
|
306
|
+
entry.detail += ` Missing env vars: ${missingEnv.join(', ')}.`;
|
|
307
|
+
if (!entry.fix) {
|
|
308
|
+
entry.fix = `Set the missing env vars before starting this MCP server: ${missingEnv.join(', ')}.`;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
} else {
|
|
312
|
+
entry.mode = 'unknown';
|
|
313
|
+
entry.status = 'fail';
|
|
314
|
+
entry.detail = 'Declared MCP server has neither a command nor a URL to validate.';
|
|
315
|
+
entry.fix = 'Add a valid command/args pair or a reachable URL-based transport definition.';
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (declaration.note) {
|
|
319
|
+
entry.detail += ` ${declaration.note}`;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
checks.push(entry);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
checks,
|
|
327
|
+
declared: checks.length,
|
|
328
|
+
pass: checks.filter((item) => item.status === 'pass').length,
|
|
329
|
+
warn: checks.filter((item) => item.status === 'warn').length,
|
|
330
|
+
fail: checks.filter((item) => item.status === 'fail').length,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
module.exports = {
|
|
335
|
+
collectDeclaredMcpServers,
|
|
336
|
+
validateDeclaredMcpServers,
|
|
337
|
+
};
|