@geraldmaron/construct 1.4.0 → 1.4.1
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/lib/hooks/guard-bash.mjs
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Runs as PreToolUse on Bash. Scans the command against a blocklist of destructive patterns (rm -rf, force push to main, etc.) and exits 2 to block matches.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
7
|
+
* Budget covers the role-fence path below — doctorRoot lookup, last-agent file reads, and the lazy manifest/fence/approval imports — which the original 5ms target predated; the nightly bench (variance-heavy CI lane) measured ~17-19ms marginal, so the budget reflects that real cost with the ×2 gate tolerance still catching a true regression.
|
|
8
|
+
*
|
|
9
|
+
* @p95ms 15
|
|
8
10
|
* @maxBlockingScope PreToolUse
|
|
9
11
|
*
|
|
10
12
|
* @lifecycle PreToolUse
|
package/lib/mcp/server.mjs
CHANGED
|
@@ -1334,7 +1334,7 @@ const ALL_TOOL_DEFS = [
|
|
|
1334
1334
|
// host/model; the long tail is reachable, just not front-loaded.
|
|
1335
1335
|
|
|
1336
1336
|
const CORE_TOOL_NAMES = new Set([
|
|
1337
|
-
'orchestration_policy', 'get_skill', 'get_template', 'search_skills', 'knowledge_search',
|
|
1337
|
+
'orchestration_policy', 'orchestration_run', 'get_skill', 'get_template', 'search_skills', 'knowledge_search',
|
|
1338
1338
|
'memory_search', 'project_context', 'summarize_diff', 'find_tool',
|
|
1339
1339
|
'author_artifact', 'document_export', 'publish_run', 'artifact_workflow',
|
|
1340
1340
|
'workflow_invoke', 'triage_recommend', 'orchestration_readiness',
|
|
@@ -14,6 +14,24 @@
|
|
|
14
14
|
const COMMUNITY_HOSTS = ['reddit.com', 'stackoverflow.com', 'news.ycombinator.com', 'discord.com', 'github.com'];
|
|
15
15
|
const HIGH_CONFIDENCE_GRADES = new Set(['A1', 'A2', 'B1']);
|
|
16
16
|
|
|
17
|
+
// Recency enforcement (rules/common/research.md §1, research-execution-policy
|
|
18
|
+
// recencyRule). The researcher is told to search most-recent-first and treat
|
|
19
|
+
// sources older than 12 months as stale, but the provider returns relevance- and
|
|
20
|
+
// authority-ranked results with the date dropped, so older established sources
|
|
21
|
+
// surface over newer ones. Normalize a date from whatever field the provider
|
|
22
|
+
// supplies, flag staleness against the 12-month window, and reorder
|
|
23
|
+
// most-recent-first. Admiralty-derived `confidence` is left intact (ADR-0017);
|
|
24
|
+
// recency rides alongside it as `date` + `stale` rather than folded into it.
|
|
25
|
+
|
|
26
|
+
const STALE_AFTER_DAYS = 365;
|
|
27
|
+
|
|
28
|
+
function normalizeDate(it) {
|
|
29
|
+
const raw = it.date || it.publishedAt || it.published || it.published_at || it.datePublished || null;
|
|
30
|
+
if (!raw) return null;
|
|
31
|
+
const t = Date.parse(raw);
|
|
32
|
+
return Number.isNaN(t) ? null : new Date(t).toISOString().slice(0, 10);
|
|
33
|
+
}
|
|
34
|
+
|
|
17
35
|
function providerConfig(env) {
|
|
18
36
|
const url = env.WEB_SEARCH_URL;
|
|
19
37
|
if (!url) return null;
|
|
@@ -49,7 +67,7 @@ function degraded(reason, note) {
|
|
|
49
67
|
return { source: 'web', degraded: true, degradationReason: reason, results: [], note };
|
|
50
68
|
}
|
|
51
69
|
|
|
52
|
-
export async function webSearch(args = {}, { env = process.env, fetchImpl = globalThis.fetch } = {}) {
|
|
70
|
+
export async function webSearch(args = {}, { env = process.env, fetchImpl = globalThis.fetch, now = Date.now() } = {}) {
|
|
53
71
|
const { query, claim, recency = null } = args;
|
|
54
72
|
if (!query || typeof query !== 'string') {
|
|
55
73
|
return { error: { code: 'INVALID_INPUT', message: 'query (string) is required' } };
|
|
@@ -78,6 +96,8 @@ export async function webSearch(args = {}, { env = process.env, fetchImpl = glob
|
|
|
78
96
|
.filter((it) => it && typeof it.url === 'string' && /^https?:\/\//.test(it.url))
|
|
79
97
|
.map((it) => {
|
|
80
98
|
const admiralty = typeof it.admiralty === 'string' && /^[A-F][1-6]$/.test(it.admiralty) ? it.admiralty : 'C3';
|
|
99
|
+
const date = normalizeDate(it);
|
|
100
|
+
const stale = date != null && (now - Date.parse(date)) > STALE_AFTER_DAYS * 86_400_000;
|
|
81
101
|
return {
|
|
82
102
|
source: 'web',
|
|
83
103
|
url: it.url,
|
|
@@ -86,9 +106,22 @@ export async function webSearch(args = {}, { env = process.env, fetchImpl = glob
|
|
|
86
106
|
class: classifyResult(it.url),
|
|
87
107
|
admiralty,
|
|
88
108
|
confidence: confidenceFromGrade(admiralty),
|
|
109
|
+
date,
|
|
110
|
+
stale,
|
|
89
111
|
needsGrading: !(typeof it.admiralty === 'string' && /^[A-F][1-6]$/.test(it.admiralty)),
|
|
90
112
|
};
|
|
91
113
|
});
|
|
92
114
|
|
|
115
|
+
// Order most-recent-first: fresh dated results, then undated (provider order
|
|
116
|
+
// kept by the stable sort), then stale ones; newest first within a tier.
|
|
117
|
+
|
|
118
|
+
const tier = (r) => (r.date && !r.stale ? 0 : r.date ? 2 : 1);
|
|
119
|
+
results.sort((a, b) => {
|
|
120
|
+
const t = tier(a) - tier(b);
|
|
121
|
+
if (t !== 0) return t;
|
|
122
|
+
if (a.date && b.date) return Date.parse(b.date) - Date.parse(a.date);
|
|
123
|
+
return 0;
|
|
124
|
+
});
|
|
125
|
+
|
|
93
126
|
return { source: 'web', degraded: false, query, claim, results, dropped: items.length - results.length };
|
|
94
127
|
}
|
|
@@ -183,6 +183,38 @@ export function classifyResearchShape(request = '') {
|
|
|
183
183
|
return null;
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
// Live web access is distinct from research-shape: a bare "connect to the
|
|
187
|
+
// internet" or "fetch example.com" carries none of the comparative/landscape
|
|
188
|
+
// vocabulary, so without this it falls to the immediate track and the
|
|
189
|
+
// orchestrator — which holds no network tools — refuses instead of dispatching
|
|
190
|
+
// the web-capable cx-researcher. A literal URL, an explicit online/internet
|
|
191
|
+
// phrase, or a fetch/scrape verb paired with a web object is the signal.
|
|
192
|
+
|
|
193
|
+
const WEB_VERB = '(?:fetch|download|open|load|read|scrape|crawl|retrieve|pull|grab|access|visit|hit|reach)';
|
|
194
|
+
const WEB_OBJECT = '(?:url|link|web\\s?page|web\\s?site|site|page|online|internet|web|endpoint|api)';
|
|
195
|
+
const LIVE_WEB_ACCESS_PATTERNS = [
|
|
196
|
+
/https?:\/\/\S+/i,
|
|
197
|
+
/\bwww\.[a-z0-9-]+\.[a-z]{2,}/i,
|
|
198
|
+
/\b(?:connect|connecting|connection)\s+to\s+the\s+(?:internet|web|network)\b/i,
|
|
199
|
+
/\binternet\s+(?:access|connection|connectivity)\b/i,
|
|
200
|
+
/\b(?:go|get|getting|going)\s+online\b/i,
|
|
201
|
+
/\b(?:browse|browsing|surf)\b[\s\S]{0,20}\b(?:web|internet|site|page|url)\b/i,
|
|
202
|
+
/\b(?:curl|wget|ping)\b/i,
|
|
203
|
+
/\b(?:is|are)\b[\s\S]{0,40}\b(?:online|offline|reachable|unreachable|responding)\b/i,
|
|
204
|
+
new RegExp(`\\b${WEB_VERB}\\b[\\s\\S]{0,40}\\b${WEB_OBJECT}\\b`, 'i'),
|
|
205
|
+
new RegExp(`\\b${WEB_VERB}\\b[\\s\\S]{0,40}\\b[a-z0-9-]+\\.(?:com|org|net|io|dev|ai|gov|edu|co|app|info|xyz)\\b`, 'i'),
|
|
206
|
+
];
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Whether a request needs live external web/network access the orchestrator
|
|
210
|
+
* cannot perform itself (it is a read-only router with no network tools), so it
|
|
211
|
+
* must be routed to cx-researcher rather than answered immediately.
|
|
212
|
+
*/
|
|
213
|
+
export function requiresLiveWebAccess(request = '') {
|
|
214
|
+
const text = String(request);
|
|
215
|
+
return LIVE_WEB_ACCESS_PATTERNS.some((pattern) => pattern.test(text));
|
|
216
|
+
}
|
|
217
|
+
|
|
186
218
|
/**
|
|
187
219
|
* Returns whether external research is required before scaffolding, with the
|
|
188
220
|
* reason. Triggered by named entities not in the project glossary, by
|
|
@@ -193,6 +225,9 @@ export function requiresExternalResearch({ request = '', workCategory, riskFlags
|
|
|
193
225
|
const entities = extractNamedEntities(request);
|
|
194
226
|
const category = workCategory ?? classifyWorkCategory(request);
|
|
195
227
|
const flags = riskFlags ?? detectRiskFlags(request);
|
|
228
|
+
if (requiresLiveWebAccess(request)) {
|
|
229
|
+
return { required: true, reason: 'web-access' };
|
|
230
|
+
}
|
|
196
231
|
if (entities.length > 0) {
|
|
197
232
|
return { required: true, reason: 'named-entities', entities };
|
|
198
233
|
}
|
|
@@ -507,6 +542,7 @@ export function determineExecutionTrack({
|
|
|
507
542
|
|| isOperationsPlanningRequest(request)
|
|
508
543
|
|| isRdLeadRequest(request)
|
|
509
544
|
|| isExplorerRequest(request)
|
|
545
|
+
|| requiresLiveWebAccess(request)
|
|
510
546
|
) {
|
|
511
547
|
return EXECUTION_TRACKS.focused;
|
|
512
548
|
}
|
package/package.json
CHANGED
|
@@ -448,6 +448,13 @@ const standardConstructTools = [
|
|
|
448
448
|
"cx_trace",
|
|
449
449
|
"cx_score",
|
|
450
450
|
].join(",");
|
|
451
|
+
|
|
452
|
+
// The orchestrator's atomic contract is classify-then-dispatch: orchestration_policy
|
|
453
|
+
// then orchestration_run. On allowlist hosts (Claude) these must be named in the
|
|
454
|
+
// agent's tools list or the call is blocked, leaving the orchestrator unable to route.
|
|
455
|
+
// Single source of truth so every host's orchestrator grant stays in parity.
|
|
456
|
+
|
|
457
|
+
const ORCHESTRATOR_DISPATCH_TOOLS = ["orchestration_policy", "orchestration_run"];
|
|
451
458
|
const managedStart = `# BEGIN ${systemName.toUpperCase()} AGENTS`;
|
|
452
459
|
const managedEnd = `# END ${systemName.toUpperCase()} AGENTS`;
|
|
453
460
|
const mdManagedStart = `<!-- BEGIN ${systemName.toUpperCase()} AGENTS -->`;
|
|
@@ -765,7 +772,8 @@ function orchestrationMicroPrompt(platform) {
|
|
|
765
772
|
`You are the primary orchestrator. Before any non-trivial answer, call \`${policyTool}\` with the user's \`request\`. Do not guess agent names or workflow types.\n\n` +
|
|
766
773
|
`Example — the user says "add rate limiting to the API". Your first action is a tool call, not prose:\n` +
|
|
767
774
|
` call ${policyTool} { "request": "add rate limiting to the API" }\n` +
|
|
768
|
-
`If the route is focused/orchestrated specialist work, call \`${runTool}\` with the same request. If the route suggests a workflow such as \`research-synthesis\`, pass it as \`workflow_type\`. Do not narrate completed research unless \`${runTool}\` or evidence tools actually ran
|
|
775
|
+
`If the route is focused/orchestrated specialist work, call \`${runTool}\` with the same request. If the route suggests a workflow such as \`research-synthesis\`, pass it as \`workflow_type\`. Do not narrate completed research unless \`${runTool}\` or evidence tools actually ran.\n\n` +
|
|
776
|
+
`If a request needs a capability you lack — live web/network access, external data, code execution — do not refuse or tell the user to run it themselves. Route it via \`${runTool}\` to the specialist that holds the capability (web access lives with the researcher), or ask one clarifying question when the target is ambiguous.`
|
|
769
777
|
);
|
|
770
778
|
}
|
|
771
779
|
|
|
@@ -984,6 +992,13 @@ function claudeAgentMarkdown(entry, allEntries) {
|
|
|
984
992
|
...baseTools.split(",").map((t) => t.trim()),
|
|
985
993
|
...standardConstructTools.split(","),
|
|
986
994
|
]);
|
|
995
|
+
|
|
996
|
+
// Claude's tools list is a hard allowlist; the orchestrator can only route if its
|
|
997
|
+
// dispatch tools are named here.
|
|
998
|
+
|
|
999
|
+
if (entry.isOrchestrator) {
|
|
1000
|
+
for (const tool of ORCHESTRATOR_DISPATCH_TOOLS) toolSet.add(tool);
|
|
1001
|
+
}
|
|
987
1002
|
const tools = Array.from(toolSet).filter(Boolean).join(",");
|
|
988
1003
|
|
|
989
1004
|
return `---
|
|
@@ -1346,6 +1361,40 @@ When using this prompt, stay within the role above and adapt to the current repo
|
|
|
1346
1361
|
`;
|
|
1347
1362
|
}
|
|
1348
1363
|
|
|
1364
|
+
// VS Code reads custom agents (the renamed successor to chat modes) from
|
|
1365
|
+
// .github/agents/<name>.agent.md, and tool grants must use its namespaced ids:
|
|
1366
|
+
// <server>/* for an MCP server's tools, web/fetch for outbound web, search/read
|
|
1367
|
+
// for repo awareness. The Claude-format tools in .claude/agents/*.md are not
|
|
1368
|
+
// recognized here, so the orchestrator needs its own VS Code agent or it lists
|
|
1369
|
+
// in the picker with no usable tools.
|
|
1370
|
+
|
|
1371
|
+
const COPILOT_AGENT_TOOLS = [
|
|
1372
|
+
"construct-mcp/*",
|
|
1373
|
+
"web/fetch",
|
|
1374
|
+
"web/githubRepo",
|
|
1375
|
+
"search/codebase",
|
|
1376
|
+
"search/usages",
|
|
1377
|
+
"search/fileSearch",
|
|
1378
|
+
"read/problems",
|
|
1379
|
+
"edit/editFiles",
|
|
1380
|
+
];
|
|
1381
|
+
|
|
1382
|
+
function copilotAgentFile(entry, allEntries) {
|
|
1383
|
+
const name = adapterName(entry);
|
|
1384
|
+
return `---
|
|
1385
|
+
description: ${entry.description}
|
|
1386
|
+
name: ${name}
|
|
1387
|
+
tools: ${JSON.stringify(COPILOT_AGENT_TOOLS)}
|
|
1388
|
+
---
|
|
1389
|
+
|
|
1390
|
+
${generatedMarkdownNote}
|
|
1391
|
+
|
|
1392
|
+
# ${name}
|
|
1393
|
+
|
|
1394
|
+
${buildPrompt(entry, allEntries, "copilot")}
|
|
1395
|
+
`;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1349
1398
|
function syncCopilot(entries, targetDir = null, wants = true) {
|
|
1350
1399
|
const promptsDir = targetDir
|
|
1351
1400
|
? path.join(targetDir, ".github", "prompts")
|
|
@@ -1362,15 +1411,20 @@ function syncCopilot(entries, targetDir = null, wants = true) {
|
|
|
1362
1411
|
sweepLegacyPrefixedFiles(promptsDir, ".prompt.md", writeEntries.map((e) => `${adapterName(e)}.prompt.md`));
|
|
1363
1412
|
}
|
|
1364
1413
|
|
|
1365
|
-
// VS Code
|
|
1366
|
-
//
|
|
1367
|
-
//
|
|
1368
|
-
//
|
|
1414
|
+
// VS Code reads custom agents from `.github/agents/*.agent.md`. The Claude
|
|
1415
|
+
// tool names in `.claude/agents/*.md` are not recognized there, so the front
|
|
1416
|
+
// door ships as a VS Code agent with namespaced tool grants (construct-mcp/*,
|
|
1417
|
+
// web/fetch, search/read) — selecting it in the dropdown then scopes those
|
|
1418
|
+
// tools in. The Claude-format set stays for Claude Code.
|
|
1369
1419
|
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1420
|
+
const agentsDir = targetDir
|
|
1421
|
+
? path.join(targetDir, ".github", "agents")
|
|
1422
|
+
: path.join(home, ".github", "agents");
|
|
1423
|
+
if (!DRY_RUN && wants) mkdirp(agentsDir);
|
|
1424
|
+
for (const entry of writeEntries) {
|
|
1425
|
+
writeFile(path.join(agentsDir, `${adapterName(entry)}.agent.md`), copilotAgentFile(entry, entries), { stamp: false });
|
|
1373
1426
|
}
|
|
1427
|
+
if (fs.existsSync(agentsDir)) removeStaleAdapters(agentsDir, ".agent.md", writeEntries);
|
|
1374
1428
|
|
|
1375
1429
|
const instructionsPath = targetDir
|
|
1376
1430
|
? path.join(targetDir, ".github", "copilot-instructions.md")
|
|
@@ -1387,7 +1441,7 @@ function syncCopilot(entries, targetDir = null, wants = true) {
|
|
|
1387
1441
|
const list = listEntries.map((e) => `- \`${adapterName(e)}\`: use \`${promptPathPrefix}/${adapterName(e)}.prompt.md\`.`).join("\n");
|
|
1388
1442
|
const note = `## ${systemName.charAt(0).toUpperCase() + systemName.slice(1)} Agent Prompts
|
|
1389
1443
|
|
|
1390
|
-
|
|
1444
|
+
Select \`${systemName}\` from the chat mode dropdown to enter the orchestrator: describe an outcome and it classifies the request and dispatches the right specialists through the \`construct-mcp\` tools (\`orchestration_policy\` then \`orchestration_run\`). You ask for outcomes, not specialists — it routes internally. Requires the \`construct-mcp\` server (wired in \`.vscode/mcp.json\`); if its tools are unavailable, the mode cannot route and will say so rather than guess.
|
|
1391
1445
|
|
|
1392
1446
|
${list || "(no front-door prompts to surface)"}`;
|
|
1393
1447
|
|
|
@@ -1432,6 +1486,45 @@ function getVSCodeUserMcpPaths() {
|
|
|
1432
1486
|
.filter((file) => fs.existsSync(file));
|
|
1433
1487
|
}
|
|
1434
1488
|
|
|
1489
|
+
// A merged mcp.json preserves existing entries so user customizations survive a
|
|
1490
|
+
// re-sync. A construct-owned server path is a fully-resolved, non-placeholder
|
|
1491
|
+
// path, so the preserve rule keeps it even when it points at a different toolkit
|
|
1492
|
+
// root than the current one — and VS Code then launches a server that may not
|
|
1493
|
+
// exist. Treat a construct toolkit path outside the current root as stale so the
|
|
1494
|
+
// sync refreshes it; user-owned servers carry no lib/mcp toolkit path and stay.
|
|
1495
|
+
|
|
1496
|
+
export function mcpEntryPointsOutsideToolkit(entry, root) {
|
|
1497
|
+
const args = Array.isArray(entry?.args) ? entry.args : [];
|
|
1498
|
+
return args.some(
|
|
1499
|
+
(arg) => typeof arg === "string"
|
|
1500
|
+
&& /\/lib\/mcp\/[a-z0-9-]+\.mjs$/.test(arg)
|
|
1501
|
+
&& !arg.startsWith(`${root}/`),
|
|
1502
|
+
);
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
// VS Code scans both `.github/agents` and `.claude/agents`, so the orchestrator
|
|
1506
|
+
// would list twice — once with VS Code tools (.github/agents) and once with
|
|
1507
|
+
// Claude tool names VS Code ignores (.claude/agents). `chat.agentFilesLocations`
|
|
1508
|
+
// is the documented lever to pin the scan; its power over the built-in
|
|
1509
|
+
// `.claude/agents` compatibility scan is version-dependent, so this is a
|
|
1510
|
+
// best-effort hint, not a guarantee. Merge only into a strictly-parseable file
|
|
1511
|
+
// and never overwrite an existing choice, so a commented (JSONC) or
|
|
1512
|
+
// user-customized settings.json is left untouched.
|
|
1513
|
+
|
|
1514
|
+
export function pinVscodeAgentLocations(targetDir) {
|
|
1515
|
+
if (DRY_RUN) return;
|
|
1516
|
+
const settingsPath = path.join(targetDir, ".vscode", "settings.json");
|
|
1517
|
+
let settings = {};
|
|
1518
|
+
if (fs.existsSync(settingsPath)) {
|
|
1519
|
+
try { settings = JSON.parse(fs.readFileSync(settingsPath, "utf8")) || {}; }
|
|
1520
|
+
catch { return; }
|
|
1521
|
+
}
|
|
1522
|
+
if (settings["chat.agentFilesLocations"]) return;
|
|
1523
|
+
settings["chat.agentFilesLocations"] = { ".github/agents": true, ".claude/agents": false };
|
|
1524
|
+
mkdirp(path.dirname(settingsPath));
|
|
1525
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1435
1528
|
function syncVSCode(targetDir = null, wants = true) {
|
|
1436
1529
|
const registryMcp = scopedManagedMcpDefs({ projectScope: Boolean(targetDir) });
|
|
1437
1530
|
if (Object.keys(registryMcp).length === 0) return false;
|
|
@@ -1480,13 +1573,15 @@ function syncVSCode(targetDir = null, wants = true) {
|
|
|
1480
1573
|
const registryWantsCommand = !mcpDef.type && Array.isArray(mcpDef.args);
|
|
1481
1574
|
const existingIsRemote = existingEntry && (existingEntry.type === 'http' || existingEntry.type === 'remote');
|
|
1482
1575
|
const transportMismatch = registryWantsCommand && existingIsRemote;
|
|
1483
|
-
|
|
1576
|
+
const staleToolkitPath = mcpEntryPointsOutsideToolkit(existingEntry, root);
|
|
1577
|
+
if (existingEntry && !hasPlaceholder && !transportMismatch && !staleToolkitPath) continue;
|
|
1484
1578
|
config.servers[id] = buildClaudeMcpEntry(id, mcpDef, process.env, { host: "vscode" });
|
|
1485
1579
|
}
|
|
1486
1580
|
if (!DRY_RUN) {
|
|
1487
1581
|
mkdirp(path.dirname(mcpPath));
|
|
1488
1582
|
fs.writeFileSync(mcpPath, JSON.stringify(config, null, 2) + "\n");
|
|
1489
1583
|
}
|
|
1584
|
+
pinVscodeAgentLocations(targetDir);
|
|
1490
1585
|
return true;
|
|
1491
1586
|
}
|
|
1492
1587
|
|
|
@@ -1504,7 +1599,8 @@ function syncVSCode(targetDir = null, wants = true) {
|
|
|
1504
1599
|
const registryWantsCommand = !mcpDef.type && Array.isArray(mcpDef.args);
|
|
1505
1600
|
const existingIsRemote = existingEntry && (existingEntry.type === 'http' || existingEntry.type === 'remote');
|
|
1506
1601
|
const transportMismatch = registryWantsCommand && existingIsRemote;
|
|
1507
|
-
|
|
1602
|
+
const staleToolkitPath = mcpEntryPointsOutsideToolkit(existingEntry, root);
|
|
1603
|
+
if (existingEntry && !hasPlaceholder && !transportMismatch && !staleToolkitPath) continue;
|
|
1508
1604
|
config.servers[id] = buildClaudeMcpEntry(id, mcpDef, process.env, { host: "vscode" });
|
|
1509
1605
|
}
|
|
1510
1606
|
if (!DRY_RUN) fs.writeFileSync(mcpPath, JSON.stringify(config, null, 2) + "\n");
|