@fragments-sdk/mcp 0.9.0 → 0.10.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/README.md +31 -17
- package/dist/bin.js +6 -22
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-YSNIGHNU.js → chunk-KGFM5SCE.js} +1140 -2537
- package/dist/chunk-KGFM5SCE.js.map +1 -0
- package/dist/{chunk-VV2PJ75X.js → chunk-WDQPNHZ2.js} +37 -6
- package/dist/chunk-WDQPNHZ2.js.map +1 -0
- package/dist/{dist-BDWAHJ4K.js → dist-LVC53MY5.js} +764 -22
- package/dist/dist-LVC53MY5.js.map +1 -0
- package/dist/index.js +77 -14
- package/dist/index.js.map +1 -1
- package/dist/init.js +34 -0
- package/dist/init.js.map +1 -1
- package/dist/rules-JUZ3RABB.js +8 -0
- package/dist/server.js +1 -3
- package/package.json +6 -5
- package/dist/chunk-4SVS3AA3.js +0 -78
- package/dist/chunk-4SVS3AA3.js.map +0 -1
- package/dist/chunk-VV2PJ75X.js.map +0 -1
- package/dist/chunk-YSNIGHNU.js.map +0 -1
- package/dist/chunk-YSRGQDEB.js +0 -93
- package/dist/chunk-YSRGQDEB.js.map +0 -1
- package/dist/constants-BLN4SSNH.js +0 -10
- package/dist/dist-BDWAHJ4K.js.map +0 -1
- package/dist/rules-TN4KHFFG.js +0 -9
- package/dist/rules-TN4KHFFG.js.map +0 -1
- /package/dist/{constants-BLN4SSNH.js.map → rules-JUZ3RABB.js.map} +0 -0
|
@@ -1,28 +1,19 @@
|
|
|
1
|
-
import {
|
|
2
|
-
BRAND,
|
|
3
|
-
DEFAULTS
|
|
4
|
-
} from "./chunk-4SVS3AA3.js";
|
|
5
|
-
import {
|
|
6
|
-
componentNames,
|
|
7
|
-
findComponent,
|
|
8
|
-
findComponentByName,
|
|
9
|
-
getGuidanceWhen,
|
|
10
|
-
getGuidanceWhenNot,
|
|
11
|
-
listBlocks,
|
|
12
|
-
listComponents
|
|
13
|
-
} from "./chunk-YSRGQDEB.js";
|
|
14
|
-
|
|
15
1
|
// src/server.ts
|
|
16
2
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
17
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
18
4
|
import {
|
|
19
5
|
CallToolRequestSchema,
|
|
20
|
-
|
|
6
|
+
ErrorCode,
|
|
7
|
+
ListResourcesRequestSchema,
|
|
8
|
+
ListToolsRequestSchema,
|
|
9
|
+
McpError,
|
|
10
|
+
ReadResourceRequestSchema
|
|
21
11
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
22
|
-
import { existsSync as
|
|
12
|
+
import { existsSync as existsSync8 } from "fs";
|
|
23
13
|
import { readFileSync as readFileSync6 } from "fs";
|
|
24
|
-
import { join as
|
|
14
|
+
import { join as join7 } from "path";
|
|
25
15
|
import { fileURLToPath } from "url";
|
|
16
|
+
import { BRAND as BRAND4 } from "@fragments-sdk/core";
|
|
26
17
|
|
|
27
18
|
// src/config.ts
|
|
28
19
|
import { readFileSync, existsSync } from "fs";
|
|
@@ -30,2372 +21,294 @@ import { join } from "path";
|
|
|
30
21
|
function loadConfigFile(projectRoot) {
|
|
31
22
|
const configPath = join(projectRoot, "ds-mcp.config.json");
|
|
32
23
|
if (existsSync(configPath)) {
|
|
33
|
-
try {
|
|
34
|
-
const content = readFileSync(configPath, "utf-8");
|
|
35
|
-
return JSON.parse(content);
|
|
36
|
-
} catch (e) {
|
|
37
|
-
throw new Error(`Failed to parse ${configPath}: ${e instanceof Error ? e.message : String(e)}`);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
const pkgPath = join(projectRoot, "package.json");
|
|
41
|
-
if (existsSync(pkgPath)) {
|
|
42
|
-
try {
|
|
43
|
-
const content = readFileSync(pkgPath, "utf-8");
|
|
44
|
-
const pkg = JSON.parse(content);
|
|
45
|
-
if (pkg.dsMcp) return pkg.dsMcp;
|
|
46
|
-
} catch {
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
return null;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// src/server.ts
|
|
53
|
-
import { buildMcpTools, buildToolNames, MCP_TOOL_DEFINITIONS } from "@fragments-sdk/context/mcp-tools";
|
|
54
|
-
|
|
55
|
-
// src/orama-index.ts
|
|
56
|
-
import { create, insertMultiple, search } from "@orama/orama";
|
|
57
|
-
var SYNONYM_MAP = {
|
|
58
|
-
"form": ["input", "field", "submit", "validation"],
|
|
59
|
-
"input": ["form", "field", "text", "entry"],
|
|
60
|
-
"button": ["action", "click", "submit", "trigger"],
|
|
61
|
-
"action": ["button", "click", "trigger"],
|
|
62
|
-
"submit": ["button", "form", "action", "send"],
|
|
63
|
-
"alert": ["notification", "message", "warning", "error", "feedback"],
|
|
64
|
-
"notification": ["alert", "message", "toast"],
|
|
65
|
-
"feedback": ["form", "comment", "review", "rating"],
|
|
66
|
-
"card": ["container", "panel", "box", "content"],
|
|
67
|
-
"toggle": ["switch", "checkbox", "boolean", "on/off"],
|
|
68
|
-
"switch": ["toggle", "checkbox", "boolean"],
|
|
69
|
-
"badge": ["tag", "label", "status", "indicator"],
|
|
70
|
-
"status": ["badge", "indicator", "state"],
|
|
71
|
-
"login": ["auth", "signin", "authentication", "form"],
|
|
72
|
-
"auth": ["login", "signin", "authentication"],
|
|
73
|
-
"chat": ["message", "conversation", "ai"],
|
|
74
|
-
"table": ["data", "grid", "list", "rows"],
|
|
75
|
-
"textarea": ["text", "input", "multiline", "area", "comment"],
|
|
76
|
-
"area": ["textarea", "multiline", "text"],
|
|
77
|
-
"landing": ["page", "hero", "marketing", "section", "layout"],
|
|
78
|
-
"hero": ["landing", "marketing", "banner", "headline", "section"],
|
|
79
|
-
"marketing": ["landing", "hero", "pricing", "testimonial", "cta"],
|
|
80
|
-
"cta": ["marketing", "banner", "action", "button"],
|
|
81
|
-
"testimonial": ["marketing", "review", "quote", "feedback"],
|
|
82
|
-
"layout": ["stack", "grid", "box", "container", "page"],
|
|
83
|
-
"page": ["layout", "landing", "section", "container"],
|
|
84
|
-
"section": ["hero", "feature", "testimonial", "cta", "faq"],
|
|
85
|
-
"pricing": ["card", "plan", "tier", "marketing"],
|
|
86
|
-
"plan": ["pricing", "card", "tier", "subscription"],
|
|
87
|
-
"dashboard": ["metrics", "stats", "chart", "card", "grid"],
|
|
88
|
-
"metrics": ["dashboard", "stats", "progress", "number"],
|
|
89
|
-
"stats": ["metrics", "dashboard", "progress", "badge"],
|
|
90
|
-
"chart": ["dashboard", "metrics", "data", "graph"]
|
|
91
|
-
};
|
|
92
|
-
function expandQuery(query) {
|
|
93
|
-
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
94
|
-
const expanded = new Set(terms);
|
|
95
|
-
for (const term of terms) {
|
|
96
|
-
const synonyms = SYNONYM_MAP[term];
|
|
97
|
-
if (synonyms) {
|
|
98
|
-
for (const syn of synonyms) expanded.add(syn);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
return Array.from(expanded).join(" ");
|
|
102
|
-
}
|
|
103
|
-
function twoPassSearch(config) {
|
|
104
|
-
const { index, query, properties, boost, limit, kind } = config;
|
|
105
|
-
const baseConfig = {
|
|
106
|
-
mode: "fulltext",
|
|
107
|
-
properties,
|
|
108
|
-
boost,
|
|
109
|
-
limit
|
|
110
|
-
};
|
|
111
|
-
const originalTermsQuery = query.toLowerCase().split(/\s+/).filter(Boolean).join(" ");
|
|
112
|
-
const expandedQuery = expandQuery(query);
|
|
113
|
-
const originalResults = search(index, { term: originalTermsQuery, ...baseConfig, threshold: 0.8 });
|
|
114
|
-
const expandedResults = search(index, { term: expandedQuery, ...baseConfig, threshold: 1 });
|
|
115
|
-
const origHits = originalResults.hits;
|
|
116
|
-
const expHits = expandedResults.hits;
|
|
117
|
-
const scoreMap = /* @__PURE__ */ new Map();
|
|
118
|
-
for (const hit of origHits) {
|
|
119
|
-
scoreMap.set(hit.document.name, (hit.score || 0) * 2);
|
|
120
|
-
}
|
|
121
|
-
for (const hit of expHits) {
|
|
122
|
-
const name = hit.document.name;
|
|
123
|
-
const existing = scoreMap.get(name) ?? 0;
|
|
124
|
-
scoreMap.set(name, existing + (hit.score || 0));
|
|
125
|
-
}
|
|
126
|
-
const scored = [];
|
|
127
|
-
for (const [name, score] of scoreMap) {
|
|
128
|
-
if (score > 0) {
|
|
129
|
-
scored.push({ name, kind, rank: scored.length, score });
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
scored.sort((a, b) => b.score - a.score);
|
|
133
|
-
scored.forEach((s, i) => {
|
|
134
|
-
s.rank = i;
|
|
135
|
-
});
|
|
136
|
-
return scored;
|
|
137
|
-
}
|
|
138
|
-
var componentSchema = {
|
|
139
|
-
name: "string",
|
|
140
|
-
description: "string",
|
|
141
|
-
category: "string",
|
|
142
|
-
tags: "string",
|
|
143
|
-
whenUsed: "string",
|
|
144
|
-
patterns: "string",
|
|
145
|
-
variants: "string",
|
|
146
|
-
status: "string"
|
|
147
|
-
};
|
|
148
|
-
function isCompiledFragment(value) {
|
|
149
|
-
return "meta" in value;
|
|
150
|
-
}
|
|
151
|
-
function normalizeComponent(value) {
|
|
152
|
-
if (!isCompiledFragment(value)) return value;
|
|
153
|
-
return {
|
|
154
|
-
id: value.filePath ?? value.meta.name,
|
|
155
|
-
name: value.meta.name,
|
|
156
|
-
description: value.meta.description ?? "",
|
|
157
|
-
category: value.meta.category ?? "uncategorized",
|
|
158
|
-
status: value.meta.status ?? "stable",
|
|
159
|
-
tags: value.meta.tags ?? [],
|
|
160
|
-
props: {},
|
|
161
|
-
propsSummary: value.propsSummary ?? value.contract?.propsSummary ?? Object.entries(value.props ?? {}).map(
|
|
162
|
-
([propName, prop]) => `${propName}${prop.required ? " (required)" : ""}: ${prop.type}`
|
|
163
|
-
),
|
|
164
|
-
examples: (value.variants ?? []).map((variant) => ({
|
|
165
|
-
name: variant.name,
|
|
166
|
-
description: variant.description,
|
|
167
|
-
code: variant.code
|
|
168
|
-
})),
|
|
169
|
-
relations: (value.relations ?? []).map((relation) => ({
|
|
170
|
-
componentName: relation.component,
|
|
171
|
-
relationship: relation.relationship,
|
|
172
|
-
note: relation.note
|
|
173
|
-
})),
|
|
174
|
-
compoundChildren: [],
|
|
175
|
-
guidance: {
|
|
176
|
-
when: value.usage?.when ?? [],
|
|
177
|
-
whenNot: value.usage?.whenNot ?? [],
|
|
178
|
-
guidelines: value.usage?.guidelines ?? [],
|
|
179
|
-
accessibility: value.usage?.accessibility ?? [],
|
|
180
|
-
dos: value.usage?.when ?? [],
|
|
181
|
-
donts: value.usage?.whenNot ?? [],
|
|
182
|
-
patterns: []
|
|
183
|
-
},
|
|
184
|
-
sourceType: "fragments-json",
|
|
185
|
-
sourcePath: value.sourcePath ?? value.filePath,
|
|
186
|
-
metadata: {
|
|
187
|
-
a11yRules: value.contract?.a11yRules ?? [],
|
|
188
|
-
scenarioTags: value.contract?.scenarioTags ?? []
|
|
189
|
-
}
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
function buildComponentIndex(fragments) {
|
|
193
|
-
const db = create({ schema: componentSchema, language: "english" });
|
|
194
|
-
const normalized = fragments.map(normalizeComponent);
|
|
195
|
-
const docs = normalized.map((f) => ({
|
|
196
|
-
name: f.name,
|
|
197
|
-
description: f.description ?? "",
|
|
198
|
-
category: f.category ?? "",
|
|
199
|
-
tags: (f.tags ?? []).join(" "),
|
|
200
|
-
whenUsed: (f.guidance.when ?? []).join(" "),
|
|
201
|
-
patterns: (f.guidance.patterns ?? []).map(
|
|
202
|
-
(pattern) => `${pattern.name} ${pattern.description || ""}`
|
|
203
|
-
).join(" "),
|
|
204
|
-
variants: f.examples.map(
|
|
205
|
-
(example) => `${example.name} ${example.description || ""}`
|
|
206
|
-
).join(" "),
|
|
207
|
-
status: f.status ?? "stable"
|
|
208
|
-
}));
|
|
209
|
-
insertMultiple(db, docs);
|
|
210
|
-
return db;
|
|
211
|
-
}
|
|
212
|
-
function searchComponents(query, index, fragments, limit = 50) {
|
|
213
|
-
const normalized = fragments.map(normalizeComponent);
|
|
214
|
-
const boostConfig = {
|
|
215
|
-
mode: "fulltext",
|
|
216
|
-
properties: ["name", "whenUsed", "description", "patterns", "category", "tags", "variants"],
|
|
217
|
-
boost: {
|
|
218
|
-
name: 3,
|
|
219
|
-
whenUsed: 2.5,
|
|
220
|
-
description: 2,
|
|
221
|
-
patterns: 1.5,
|
|
222
|
-
category: 1.5,
|
|
223
|
-
tags: 1.5,
|
|
224
|
-
variants: 1
|
|
225
|
-
},
|
|
226
|
-
limit
|
|
227
|
-
};
|
|
228
|
-
const originalTermsList = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
229
|
-
const originalTermsQuery = originalTermsList.join(" ");
|
|
230
|
-
const expandedQuery = expandQuery(query);
|
|
231
|
-
const originalResults = search(index, { term: originalTermsQuery, ...boostConfig, threshold: 0.8 });
|
|
232
|
-
const expandedResults = search(index, { term: expandedQuery, ...boostConfig, threshold: 1 });
|
|
233
|
-
const origHits = originalResults.hits;
|
|
234
|
-
const expHits = expandedResults.hits;
|
|
235
|
-
const scoreMap = /* @__PURE__ */ new Map();
|
|
236
|
-
for (const hit of origHits) {
|
|
237
|
-
scoreMap.set(hit.document.name, (hit.score || 0) * 2);
|
|
238
|
-
}
|
|
239
|
-
for (const hit of expHits) {
|
|
240
|
-
const name = hit.document.name;
|
|
241
|
-
const existing = scoreMap.get(name) ?? 0;
|
|
242
|
-
scoreMap.set(name, existing + (hit.score || 0));
|
|
243
|
-
}
|
|
244
|
-
const fragmentMap = /* @__PURE__ */ new Map();
|
|
245
|
-
for (const f of normalized) {
|
|
246
|
-
fragmentMap.set(f.name.toLowerCase(), f);
|
|
247
|
-
}
|
|
248
|
-
const originalTermsSet = new Set(originalTermsList);
|
|
249
|
-
const scored = [];
|
|
250
|
-
for (const [name, rawScore] of scoreMap) {
|
|
251
|
-
let score = rawScore;
|
|
252
|
-
const nameLower = name.toLowerCase();
|
|
253
|
-
const fragment = fragmentMap.get(nameLower);
|
|
254
|
-
if (originalTermsSet.has(nameLower)) {
|
|
255
|
-
score += 25;
|
|
256
|
-
}
|
|
257
|
-
if (fragment) {
|
|
258
|
-
if (fragment.status === "stable") score += 5;
|
|
259
|
-
else if (fragment.status === "beta") score += 2;
|
|
260
|
-
if (fragment.status === "deprecated") score -= 25;
|
|
261
|
-
}
|
|
262
|
-
if (score > 0) {
|
|
263
|
-
scored.push({ name, kind: "component", rank: scored.length, score });
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
scored.sort((a, b) => b.score - a.score);
|
|
267
|
-
scored.forEach((s, i) => {
|
|
268
|
-
s.rank = i;
|
|
269
|
-
});
|
|
270
|
-
return scored;
|
|
271
|
-
}
|
|
272
|
-
var blockSchema = {
|
|
273
|
-
name: "string",
|
|
274
|
-
description: "string",
|
|
275
|
-
category: "string",
|
|
276
|
-
tags: "string",
|
|
277
|
-
components: "string"
|
|
278
|
-
};
|
|
279
|
-
function normalizeBlock(value) {
|
|
280
|
-
if ("id" in value) return value;
|
|
281
|
-
return {
|
|
282
|
-
id: value.filePath ?? value.name,
|
|
283
|
-
name: value.name,
|
|
284
|
-
description: value.description ?? "",
|
|
285
|
-
category: value.category ?? "uncategorized",
|
|
286
|
-
components: value.components ?? [],
|
|
287
|
-
tags: value.tags ?? [],
|
|
288
|
-
code: value.code ?? ""
|
|
289
|
-
};
|
|
290
|
-
}
|
|
291
|
-
function buildBlockIndex(blocks) {
|
|
292
|
-
const db = create({ schema: blockSchema, language: "english" });
|
|
293
|
-
const normalized = blocks.map(normalizeBlock);
|
|
294
|
-
const docs = normalized.map((b) => ({
|
|
295
|
-
name: b.name,
|
|
296
|
-
description: b.description ?? "",
|
|
297
|
-
category: b.category ?? "",
|
|
298
|
-
tags: (b.tags ?? []).join(" "),
|
|
299
|
-
components: b.components.join(" ")
|
|
300
|
-
}));
|
|
301
|
-
insertMultiple(db, docs);
|
|
302
|
-
return db;
|
|
303
|
-
}
|
|
304
|
-
function searchBlocks(query, index, limit = 50) {
|
|
305
|
-
return twoPassSearch({
|
|
306
|
-
index,
|
|
307
|
-
query,
|
|
308
|
-
properties: ["name", "description", "components", "tags", "category"],
|
|
309
|
-
boost: {
|
|
310
|
-
name: 3,
|
|
311
|
-
description: 2,
|
|
312
|
-
components: 1.5,
|
|
313
|
-
tags: 1.5,
|
|
314
|
-
category: 1.5
|
|
315
|
-
},
|
|
316
|
-
limit,
|
|
317
|
-
kind: "block"
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
var tokenSchema = {
|
|
321
|
-
name: "string",
|
|
322
|
-
category: "string",
|
|
323
|
-
description: "string"
|
|
324
|
-
};
|
|
325
|
-
function normalizeTokenData(tokenData) {
|
|
326
|
-
if ("flat" in tokenData) return tokenData;
|
|
327
|
-
const categories = Object.fromEntries(
|
|
328
|
-
Object.entries(tokenData.categories).map(([category, entries]) => [
|
|
329
|
-
category,
|
|
330
|
-
entries.map((entry) => ({
|
|
331
|
-
name: entry.name,
|
|
332
|
-
category,
|
|
333
|
-
value: typeof entry.value === "string" ? entry.value : void 0,
|
|
334
|
-
description: entry.description
|
|
335
|
-
}))
|
|
336
|
-
])
|
|
337
|
-
);
|
|
338
|
-
return {
|
|
339
|
-
prefix: tokenData.prefix,
|
|
340
|
-
total: tokenData.total,
|
|
341
|
-
categories,
|
|
342
|
-
flat: Object.values(categories).flat()
|
|
343
|
-
};
|
|
344
|
-
}
|
|
345
|
-
function buildTokenIndex(tokenData) {
|
|
346
|
-
const db = create({ schema: tokenSchema, language: "english" });
|
|
347
|
-
const normalizedData = normalizeTokenData(tokenData);
|
|
348
|
-
const docs = [];
|
|
349
|
-
for (const [cat, tokens] of Object.entries(normalizedData.categories)) {
|
|
350
|
-
for (const token of tokens) {
|
|
351
|
-
docs.push({
|
|
352
|
-
name: token.name,
|
|
353
|
-
category: cat,
|
|
354
|
-
description: token.description ?? ""
|
|
355
|
-
});
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
insertMultiple(db, docs);
|
|
359
|
-
return db;
|
|
360
|
-
}
|
|
361
|
-
function searchTokens(query, index, limit = 50) {
|
|
362
|
-
return twoPassSearch({
|
|
363
|
-
index,
|
|
364
|
-
query,
|
|
365
|
-
properties: ["name", "category", "description"],
|
|
366
|
-
boost: {
|
|
367
|
-
name: 2.5,
|
|
368
|
-
category: 2,
|
|
369
|
-
description: 1.5
|
|
370
|
-
},
|
|
371
|
-
limit,
|
|
372
|
-
kind: "token"
|
|
373
|
-
});
|
|
374
|
-
}
|
|
375
|
-
var USE_CASE_TOKEN_CATEGORIES = {
|
|
376
|
-
"table": ["spacing", "borders", "surfaces", "text"],
|
|
377
|
-
"data": ["spacing", "borders", "surfaces"],
|
|
378
|
-
"grid": ["spacing", "layout"],
|
|
379
|
-
"form": ["spacing", "borders", "radius", "focus"],
|
|
380
|
-
"input": ["spacing", "borders", "radius", "focus"],
|
|
381
|
-
"card": ["surfaces", "shadows", "radius", "borders", "spacing"],
|
|
382
|
-
"button": ["colors", "radius", "spacing", "focus"],
|
|
383
|
-
"layout": ["spacing", "layout", "surfaces"],
|
|
384
|
-
"dashboard": ["spacing", "surfaces", "borders", "shadows"],
|
|
385
|
-
"chat": ["spacing", "surfaces", "radius", "shadows"],
|
|
386
|
-
"modal": ["shadows", "surfaces", "radius", "spacing"],
|
|
387
|
-
"dialog": ["shadows", "surfaces", "radius", "spacing"],
|
|
388
|
-
"navigation": ["spacing", "surfaces", "borders"],
|
|
389
|
-
"sidebar": ["spacing", "surfaces", "borders"],
|
|
390
|
-
"hero": ["spacing", "typography", "colors"],
|
|
391
|
-
"landing": ["spacing", "typography", "colors"],
|
|
392
|
-
"pricing": ["spacing", "surfaces", "borders", "radius"],
|
|
393
|
-
"auth": ["spacing", "borders", "radius", "focus"],
|
|
394
|
-
"login": ["spacing", "borders", "radius", "focus"],
|
|
395
|
-
"dark": ["colors", "surfaces"],
|
|
396
|
-
"theme": ["colors", "surfaces", "text"]
|
|
397
|
-
};
|
|
398
|
-
function extractTokenCategories(query) {
|
|
399
|
-
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
400
|
-
const categories = /* @__PURE__ */ new Set();
|
|
401
|
-
for (const term of terms) {
|
|
402
|
-
const cats = USE_CASE_TOKEN_CATEGORIES[term];
|
|
403
|
-
if (cats) {
|
|
404
|
-
for (const cat of cats) categories.add(cat);
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
if (categories.size === 0) {
|
|
408
|
-
return ["spacing", "colors", "surfaces"];
|
|
409
|
-
}
|
|
410
|
-
return Array.from(categories);
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
// src/version.ts
|
|
414
|
-
import { readFileSync as readFileSync2 } from "fs";
|
|
415
|
-
function readPackageVersion() {
|
|
416
|
-
try {
|
|
417
|
-
const raw = readFileSync2(new URL("../package.json", import.meta.url), "utf-8");
|
|
418
|
-
const pkg = JSON.parse(raw);
|
|
419
|
-
return pkg.version ?? "0.0.0";
|
|
420
|
-
} catch {
|
|
421
|
-
return "0.0.0";
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
var MCP_SERVER_VERSION = readPackageVersion();
|
|
425
|
-
|
|
426
|
-
// src/search.ts
|
|
427
|
-
var CONVEX_SEARCH_URL = "https://combative-jay-834.convex.site/search";
|
|
428
|
-
var CONVEX_TIMEOUT_MS = 3e3;
|
|
429
|
-
async function searchConvex(query, apiKey, limit = 10, kind) {
|
|
430
|
-
try {
|
|
431
|
-
const controller = new AbortController();
|
|
432
|
-
const timeout = setTimeout(() => controller.abort(), CONVEX_TIMEOUT_MS);
|
|
433
|
-
const response = await fetch(CONVEX_SEARCH_URL, {
|
|
434
|
-
method: "POST",
|
|
435
|
-
headers: {
|
|
436
|
-
"Content-Type": "application/json",
|
|
437
|
-
"Authorization": `Bearer ${apiKey}`
|
|
438
|
-
},
|
|
439
|
-
body: JSON.stringify({ query, limit, ...kind && { kind } }),
|
|
440
|
-
signal: controller.signal
|
|
441
|
-
});
|
|
442
|
-
clearTimeout(timeout);
|
|
443
|
-
if (!response.ok) {
|
|
444
|
-
return [];
|
|
445
|
-
}
|
|
446
|
-
const data = await response.json();
|
|
447
|
-
return data.results.map((r, i) => ({
|
|
448
|
-
name: r.name,
|
|
449
|
-
kind: r.kind ?? "component",
|
|
450
|
-
rank: i,
|
|
451
|
-
score: r.score
|
|
452
|
-
}));
|
|
453
|
-
} catch {
|
|
454
|
-
return [];
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
function keywordScoreComponents(query, fragments, componentIndex) {
|
|
458
|
-
const index = componentIndex ?? buildComponentIndex(fragments);
|
|
459
|
-
return searchComponents(query, index, fragments);
|
|
460
|
-
}
|
|
461
|
-
function keywordScoreBlocks(query, blocks, blockIndex) {
|
|
462
|
-
const index = blockIndex ?? buildBlockIndex(blocks);
|
|
463
|
-
return searchBlocks(query, index);
|
|
464
|
-
}
|
|
465
|
-
function keywordScoreTokens(query, tokenData, tokenIndex) {
|
|
466
|
-
const index = tokenIndex ?? buildTokenIndex(tokenData);
|
|
467
|
-
return searchTokens(query, index);
|
|
468
|
-
}
|
|
469
|
-
function reciprocalRankFusion(resultSets, k = 60) {
|
|
470
|
-
const scoreMap = /* @__PURE__ */ new Map();
|
|
471
|
-
for (const { results } of resultSets) {
|
|
472
|
-
for (let rank = 0; rank < results.length; rank++) {
|
|
473
|
-
const result2 = results[rank];
|
|
474
|
-
const key = `${result2.kind}:${result2.name}`;
|
|
475
|
-
const rrfScore = 1 / (k + rank + 1);
|
|
476
|
-
const existing = scoreMap.get(key);
|
|
477
|
-
if (existing) {
|
|
478
|
-
existing.score += rrfScore;
|
|
479
|
-
} else {
|
|
480
|
-
scoreMap.set(key, { score: rrfScore, kind: result2.kind, name: result2.name });
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
const fused = [];
|
|
485
|
-
for (const [, { score, kind, name }] of scoreMap) {
|
|
486
|
-
fused.push({ name, kind, rank: 0, score });
|
|
487
|
-
}
|
|
488
|
-
fused.sort((a, b) => b.score - a.score);
|
|
489
|
-
fused.forEach((r, i) => {
|
|
490
|
-
r.rank = i;
|
|
491
|
-
});
|
|
492
|
-
return fused;
|
|
493
|
-
}
|
|
494
|
-
async function hybridSearch(query, data, limit = 10, kind, apiKey) {
|
|
495
|
-
const keywordResults = [];
|
|
496
|
-
if (!kind || kind === "component") {
|
|
497
|
-
keywordResults.push(...keywordScoreComponents(query, data.fragments, data.componentIndex));
|
|
498
|
-
}
|
|
499
|
-
if ((!kind || kind === "block") && data.blocks) {
|
|
500
|
-
keywordResults.push(...keywordScoreBlocks(query, data.blocks, data.blockIndex));
|
|
501
|
-
}
|
|
502
|
-
if ((!kind || kind === "token") && data.tokenData) {
|
|
503
|
-
keywordResults.push(...keywordScoreTokens(query, data.tokenData, data.tokenIndex));
|
|
504
|
-
}
|
|
505
|
-
keywordResults.sort((a, b) => b.score - a.score);
|
|
506
|
-
keywordResults.forEach((r, i) => {
|
|
507
|
-
r.rank = i;
|
|
508
|
-
});
|
|
509
|
-
if (!apiKey) {
|
|
510
|
-
return keywordResults.slice(0, limit);
|
|
511
|
-
}
|
|
512
|
-
const vectorResults = await searchConvex(query, apiKey, limit, kind);
|
|
513
|
-
if (vectorResults.length === 0) {
|
|
514
|
-
return keywordResults.slice(0, limit);
|
|
515
|
-
}
|
|
516
|
-
const graphBoostResults = [];
|
|
517
|
-
if (data.graph) {
|
|
518
|
-
try {
|
|
519
|
-
const { ComponentGraphEngine: ComponentGraphEngine2, deserializeGraph: deserializeGraph2 } = await import("@fragments-sdk/context/graph");
|
|
520
|
-
const graph = deserializeGraph2(data.graph);
|
|
521
|
-
const engine = new ComponentGraphEngine2(graph);
|
|
522
|
-
const topComponents = [...keywordResults, ...vectorResults].filter((r) => r.kind === "component").slice(0, 5);
|
|
523
|
-
const neighborSet = /* @__PURE__ */ new Set();
|
|
524
|
-
for (const result2 of topComponents) {
|
|
525
|
-
const neighbors = engine.neighbors(result2.name, 1);
|
|
526
|
-
for (const n of neighbors.neighbors) {
|
|
527
|
-
if (!neighborSet.has(n.component)) {
|
|
528
|
-
neighborSet.add(n.component);
|
|
529
|
-
graphBoostResults.push({
|
|
530
|
-
name: n.component,
|
|
531
|
-
kind: "component",
|
|
532
|
-
rank: graphBoostResults.length,
|
|
533
|
-
score: 1
|
|
534
|
-
// Will be normalized through RRF
|
|
535
|
-
});
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
} catch {
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
const resultSets = [
|
|
543
|
-
{ label: "vector", results: vectorResults },
|
|
544
|
-
{ label: "keyword", results: keywordResults }
|
|
545
|
-
];
|
|
546
|
-
if (graphBoostResults.length > 0) {
|
|
547
|
-
resultSets.push({ label: "graph", results: graphBoostResults });
|
|
548
|
-
}
|
|
549
|
-
const fused = reciprocalRankFusion(resultSets);
|
|
550
|
-
return fused.slice(0, limit);
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
// src/scoring.ts
|
|
554
|
-
var MINIMUM_SCORE_THRESHOLD = 5;
|
|
555
|
-
function assignConfidence(score, maxScore) {
|
|
556
|
-
if (maxScore <= 0) return "low";
|
|
557
|
-
const ratio = score / maxScore;
|
|
558
|
-
if (ratio >= 0.7) return "high";
|
|
559
|
-
if (ratio >= 0.4) return "medium";
|
|
560
|
-
return "low";
|
|
561
|
-
}
|
|
562
|
-
function meetsMinimumThreshold(maxScore) {
|
|
563
|
-
return maxScore >= MINIMUM_SCORE_THRESHOLD;
|
|
564
|
-
}
|
|
565
|
-
function levenshtein(a, b) {
|
|
566
|
-
const la = a.length;
|
|
567
|
-
const lb = b.length;
|
|
568
|
-
const dp = Array.from({ length: lb + 1 }, (_, i) => i);
|
|
569
|
-
for (let i = 1; i <= la; i++) {
|
|
570
|
-
let prev = i - 1;
|
|
571
|
-
dp[0] = i;
|
|
572
|
-
for (let j = 1; j <= lb; j++) {
|
|
573
|
-
const temp = dp[j];
|
|
574
|
-
dp[j] = a[i - 1] === b[j - 1] ? prev : 1 + Math.min(prev, dp[j], dp[j - 1]);
|
|
575
|
-
prev = temp;
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
return dp[lb];
|
|
579
|
-
}
|
|
580
|
-
function findClosestMatch(input, candidates, maxDistance = 3) {
|
|
581
|
-
const inputLower = input.toLowerCase();
|
|
582
|
-
let bestMatch = null;
|
|
583
|
-
let bestDist = maxDistance + 1;
|
|
584
|
-
for (const candidate of candidates) {
|
|
585
|
-
const candidateLower = candidate.toLowerCase();
|
|
586
|
-
const dist = levenshtein(inputLower, candidateLower);
|
|
587
|
-
if (dist < bestDist) {
|
|
588
|
-
bestDist = dist;
|
|
589
|
-
bestMatch = candidate;
|
|
590
|
-
} else if (dist === bestDist && bestMatch) {
|
|
591
|
-
const currentLenDiff = Math.abs(bestMatch.length - input.length);
|
|
592
|
-
const newLenDiff = Math.abs(candidate.length - input.length);
|
|
593
|
-
if (newLenDiff < currentLenDiff) {
|
|
594
|
-
bestMatch = candidate;
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
return bestDist <= maxDistance ? bestMatch : null;
|
|
599
|
-
}
|
|
600
|
-
var BLOCK_BOOST_PER_OCCURRENCE = 5;
|
|
601
|
-
function buildBlockComponentFrequency(blocks) {
|
|
602
|
-
const freq = /* @__PURE__ */ new Map();
|
|
603
|
-
for (const block of blocks) {
|
|
604
|
-
for (const comp of block.components) {
|
|
605
|
-
const key = comp.toLowerCase();
|
|
606
|
-
freq.set(key, (freq.get(key) ?? 0) + 1);
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
return freq;
|
|
610
|
-
}
|
|
611
|
-
function boostByBlockFrequency(results, freq) {
|
|
612
|
-
for (const result2 of results) {
|
|
613
|
-
const count = freq.get(result2.name.toLowerCase()) ?? 0;
|
|
614
|
-
if (count > 0) {
|
|
615
|
-
result2.score += count * BLOCK_BOOST_PER_OCCURRENCE;
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
results.sort((a, b) => b.score - a.score);
|
|
619
|
-
results.forEach((r, i) => {
|
|
620
|
-
r.rank = i;
|
|
621
|
-
});
|
|
622
|
-
return results;
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
// src/server-helpers.ts
|
|
626
|
-
function normalizeFilter(value) {
|
|
627
|
-
const normalized = value?.trim().toLowerCase();
|
|
628
|
-
return normalized && normalized.length > 0 ? normalized : void 0;
|
|
629
|
-
}
|
|
630
|
-
function categoryMatches(category, categoryFilter) {
|
|
631
|
-
if (!categoryFilter) return true;
|
|
632
|
-
return normalizeFilter(category) === categoryFilter;
|
|
633
|
-
}
|
|
634
|
-
function buildLocalSearchData(data, indexes) {
|
|
635
|
-
const isLegacy = "fragments" in data;
|
|
636
|
-
const allFragments = Object.values(
|
|
637
|
-
isLegacy ? data.fragments : data.components
|
|
638
|
-
);
|
|
639
|
-
const allBlocks = Object.values(
|
|
640
|
-
isLegacy ? data.blocks ?? data.recipes ?? {} : data.blocks ?? {}
|
|
641
|
-
);
|
|
642
|
-
const tokens = isLegacy ? data.tokens : data.tokens;
|
|
643
|
-
const graph = isLegacy ? data.graph : data.graph;
|
|
644
|
-
const localData = {
|
|
645
|
-
fragments: allFragments,
|
|
646
|
-
blocks: allBlocks,
|
|
647
|
-
tokenData: tokens,
|
|
648
|
-
graph,
|
|
649
|
-
componentIndex: indexes.componentIndex ?? void 0,
|
|
650
|
-
blockIndex: indexes.blockIndex ?? void 0,
|
|
651
|
-
tokenIndex: indexes.tokenIndex ?? void 0
|
|
652
|
-
};
|
|
653
|
-
return { allFragments, allBlocks, localData };
|
|
654
|
-
}
|
|
655
|
-
async function buildImportStatements(components, resolvePackageName) {
|
|
656
|
-
const grouped = /* @__PURE__ */ new Map();
|
|
657
|
-
const uniqueComponents = [...new Set(components.filter(Boolean))];
|
|
658
|
-
const resolvedPackages = await Promise.all(
|
|
659
|
-
uniqueComponents.map(async (component) => ({
|
|
660
|
-
component,
|
|
661
|
-
packageName: await resolvePackageName(component)
|
|
662
|
-
}))
|
|
663
|
-
);
|
|
664
|
-
for (const { component, packageName } of resolvedPackages) {
|
|
665
|
-
const existing = grouped.get(packageName);
|
|
666
|
-
if (!existing) {
|
|
667
|
-
grouped.set(packageName, [component]);
|
|
668
|
-
continue;
|
|
669
|
-
}
|
|
670
|
-
existing.push(component);
|
|
671
|
-
}
|
|
672
|
-
return Array.from(grouped.entries()).map(
|
|
673
|
-
([packageName, componentNames2]) => `import { ${componentNames2.join(", ")} } from '${packageName}';`
|
|
674
|
-
);
|
|
675
|
-
}
|
|
676
|
-
function limitTokensPerCategory(categories, limit) {
|
|
677
|
-
if (limit === void 0) {
|
|
678
|
-
return {
|
|
679
|
-
categories,
|
|
680
|
-
total: Object.values(categories).reduce((sum, tokens) => sum + tokens.length, 0)
|
|
681
|
-
};
|
|
682
|
-
}
|
|
683
|
-
const limited = {};
|
|
684
|
-
let total = 0;
|
|
685
|
-
for (const [category, tokens] of Object.entries(categories)) {
|
|
686
|
-
const sliced = tokens.slice(0, limit);
|
|
687
|
-
if (sliced.length === 0) continue;
|
|
688
|
-
limited[category] = sliced;
|
|
689
|
-
total += sliced.length;
|
|
690
|
-
}
|
|
691
|
-
return { categories: limited, total };
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
// src/search-helpers.ts
|
|
695
|
-
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
696
|
-
"a",
|
|
697
|
-
"an",
|
|
698
|
-
"and",
|
|
699
|
-
"bar",
|
|
700
|
-
"build",
|
|
701
|
-
"button",
|
|
702
|
-
"for",
|
|
703
|
-
"form",
|
|
704
|
-
"i",
|
|
705
|
-
"login",
|
|
706
|
-
"me",
|
|
707
|
-
"need",
|
|
708
|
-
"of",
|
|
709
|
-
"or",
|
|
710
|
-
"the",
|
|
711
|
-
"to",
|
|
712
|
-
"use",
|
|
713
|
-
"with"
|
|
714
|
-
]);
|
|
715
|
-
function normalizeTerms(value) {
|
|
716
|
-
return value.toLowerCase().split(/[^a-z0-9]+/g).filter((term) => term.length > 1 && !STOP_WORDS.has(term));
|
|
717
|
-
}
|
|
718
|
-
function hasDirectQueryOverlap(query, component) {
|
|
719
|
-
const queryTerms = normalizeTerms(query);
|
|
720
|
-
if (queryTerms.length === 0) return true;
|
|
721
|
-
const haystack = new Set(
|
|
722
|
-
normalizeTerms(
|
|
723
|
-
[
|
|
724
|
-
component.name,
|
|
725
|
-
component.description,
|
|
726
|
-
component.category,
|
|
727
|
-
component.tags.join(" "),
|
|
728
|
-
getGuidanceWhen(component).join(" "),
|
|
729
|
-
getGuidanceWhenNot(component).join(" "),
|
|
730
|
-
component.propsSummary.join(" ")
|
|
731
|
-
].join(" ")
|
|
732
|
-
)
|
|
733
|
-
);
|
|
734
|
-
return queryTerms.some((term) => haystack.has(term));
|
|
735
|
-
}
|
|
736
|
-
function getRankingBonus(component) {
|
|
737
|
-
let bonus = 0;
|
|
738
|
-
if (component.isCanonical) bonus += 30;
|
|
739
|
-
if (component.tier === "core") bonus += 20;
|
|
740
|
-
if (component.status === "stable") bonus += 5;
|
|
741
|
-
return bonus;
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
// src/tools/discover.ts
|
|
745
|
-
function renderContextMarkdown(args) {
|
|
746
|
-
const lines = ["# Design System Context", ""];
|
|
747
|
-
for (const component of args.components) {
|
|
748
|
-
lines.push(`## ${component.name}`);
|
|
749
|
-
if (component.description) {
|
|
750
|
-
lines.push(component.description);
|
|
751
|
-
}
|
|
752
|
-
lines.push(
|
|
753
|
-
`- Category: ${component.category}`,
|
|
754
|
-
`- Status: ${component.status}`
|
|
755
|
-
);
|
|
756
|
-
if (component.propsSummary.length > 0) {
|
|
757
|
-
lines.push(`- Props: ${component.propsSummary.join(", ")}`);
|
|
758
|
-
}
|
|
759
|
-
const when = getGuidanceWhen(component);
|
|
760
|
-
if (when.length > 0) {
|
|
761
|
-
lines.push(`- Use when: ${when.slice(0, args.compact ? 1 : 3).join("; ")}`);
|
|
762
|
-
}
|
|
763
|
-
const whenNot = getGuidanceWhenNot(component);
|
|
764
|
-
if (whenNot.length > 0) {
|
|
765
|
-
lines.push(
|
|
766
|
-
`- Avoid when: ${whenNot.slice(0, args.compact ? 1 : 2).join("; ")}`
|
|
767
|
-
);
|
|
768
|
-
}
|
|
769
|
-
if (args.includeRelations && component.relations.length > 0) {
|
|
770
|
-
lines.push(
|
|
771
|
-
`- Related: ${component.relations.slice(0, 5).map(
|
|
772
|
-
(relation) => `${relation.componentName} (${relation.relationship})`
|
|
773
|
-
).join(", ")}`
|
|
774
|
-
);
|
|
775
|
-
}
|
|
776
|
-
if (args.includeCode && component.examples[0]?.code) {
|
|
777
|
-
lines.push("", "```tsx", component.examples[0].code, "```");
|
|
778
|
-
}
|
|
779
|
-
lines.push("");
|
|
780
|
-
}
|
|
781
|
-
if (args.blocks.length > 0) {
|
|
782
|
-
lines.push("## Blocks", "");
|
|
783
|
-
for (const block of args.blocks) {
|
|
784
|
-
lines.push(`- ${block.name}: ${block.description}`);
|
|
785
|
-
}
|
|
786
|
-
lines.push("");
|
|
787
|
-
}
|
|
788
|
-
return lines.join("\n").trim();
|
|
789
|
-
}
|
|
790
|
-
function renderContextJson(args) {
|
|
791
|
-
return JSON.stringify({
|
|
792
|
-
components: args.components.map((component) => ({
|
|
793
|
-
name: component.name,
|
|
794
|
-
description: component.description,
|
|
795
|
-
category: component.category,
|
|
796
|
-
status: component.status,
|
|
797
|
-
propsSummary: component.propsSummary,
|
|
798
|
-
when: getGuidanceWhen(component),
|
|
799
|
-
whenNot: getGuidanceWhenNot(component),
|
|
800
|
-
...args.includeRelations && { relations: component.relations },
|
|
801
|
-
...args.includeCode && {
|
|
802
|
-
examples: component.examples.filter((example) => Boolean(example.code)).slice(0, 2)
|
|
803
|
-
}
|
|
804
|
-
})),
|
|
805
|
-
blocks: args.blocks
|
|
806
|
-
});
|
|
807
|
-
}
|
|
808
|
-
var discoverHandler = async (args, ctx) => {
|
|
809
|
-
const data = ctx.data;
|
|
810
|
-
const snapshotComponents = listComponents(data.snapshot);
|
|
811
|
-
const componentsByName = new Map(
|
|
812
|
-
snapshotComponents.map((component) => [
|
|
813
|
-
component.name.toLowerCase(),
|
|
814
|
-
component
|
|
815
|
-
])
|
|
816
|
-
);
|
|
817
|
-
const useCase = args?.useCase ?? void 0;
|
|
818
|
-
const componentForAlts = args?.component ?? void 0;
|
|
819
|
-
const category = normalizeFilter(args?.category);
|
|
820
|
-
const search2 = args?.search?.toLowerCase() ?? void 0;
|
|
821
|
-
const status = args?.status ?? void 0;
|
|
822
|
-
const format = args?.format ?? "markdown";
|
|
823
|
-
const compact = args?.compact ?? false;
|
|
824
|
-
const includeCode = args?.includeCode ?? false;
|
|
825
|
-
const includeRelations = args?.includeRelations ?? false;
|
|
826
|
-
const depth = args?.depth ?? "quick";
|
|
827
|
-
const suggestLimit = typeof args?.limit === "number" ? Math.min(Math.max(args.limit, 1), 25) : 10;
|
|
828
|
-
const listLimit = typeof args?.limit === "number" ? Math.min(Math.max(args.limit, 1), 25) : void 0;
|
|
829
|
-
const verbosity = args?.verbosity ?? (compact ? "compact" : "standard");
|
|
830
|
-
const allSnapshotComponents = listComponents(data.snapshot);
|
|
831
|
-
if (!useCase && !componentForAlts && (args?.format || includeCode || includeRelations)) {
|
|
832
|
-
let components2 = allSnapshotComponents;
|
|
833
|
-
const allBlocks = listBlocks(data.snapshot);
|
|
834
|
-
if (category) {
|
|
835
|
-
components2 = components2.filter(
|
|
836
|
-
(component) => categoryMatches(component.category, category)
|
|
837
|
-
);
|
|
838
|
-
}
|
|
839
|
-
if (search2) {
|
|
840
|
-
const scored = keywordScoreComponents(
|
|
841
|
-
search2,
|
|
842
|
-
components2,
|
|
843
|
-
ctx.indexes.componentIndex ?? void 0
|
|
844
|
-
);
|
|
845
|
-
const allowedNames = new Set(components2.map((component) => component.name));
|
|
846
|
-
const sortedNames = scored.filter((result2) => allowedNames.has(result2.name)).map((result2) => result2.name.toLowerCase());
|
|
847
|
-
components2 = components2.filter((component) => sortedNames.includes(component.name.toLowerCase())).sort(
|
|
848
|
-
(a, b) => sortedNames.indexOf(a.name.toLowerCase()) - sortedNames.indexOf(b.name.toLowerCase())
|
|
849
|
-
);
|
|
850
|
-
}
|
|
851
|
-
if (status) {
|
|
852
|
-
components2 = components2.filter((component) => component.status === status);
|
|
853
|
-
}
|
|
854
|
-
const blocks = allBlocks.map((block) => ({
|
|
855
|
-
name: block.name,
|
|
856
|
-
description: block.description
|
|
857
|
-
}));
|
|
858
|
-
const ctxContent = format === "json" ? renderContextJson({
|
|
859
|
-
components: components2,
|
|
860
|
-
blocks,
|
|
861
|
-
includeCode: includeCode || verbosity === "full",
|
|
862
|
-
includeRelations
|
|
863
|
-
}) : renderContextMarkdown({
|
|
864
|
-
components: components2,
|
|
865
|
-
blocks,
|
|
866
|
-
includeCode: includeCode || verbosity === "full",
|
|
867
|
-
includeRelations,
|
|
868
|
-
compact: verbosity === "compact"
|
|
869
|
-
});
|
|
870
|
-
return {
|
|
871
|
-
content: [{ type: "text", text: ctxContent }],
|
|
872
|
-
_meta: {
|
|
873
|
-
componentCount: components2.length,
|
|
874
|
-
blockCount: blocks.length
|
|
875
|
-
}
|
|
876
|
-
};
|
|
877
|
-
}
|
|
878
|
-
if (useCase) {
|
|
879
|
-
const { allFragments, allBlocks, localData } = buildLocalSearchData(
|
|
880
|
-
{
|
|
881
|
-
components: data.components,
|
|
882
|
-
blocks: data.blocks,
|
|
883
|
-
tokens: data.tokens,
|
|
884
|
-
graph: data.graph
|
|
885
|
-
},
|
|
886
|
-
{
|
|
887
|
-
componentIndex: ctx.indexes.componentIndex,
|
|
888
|
-
blockIndex: ctx.indexes.blockIndex,
|
|
889
|
-
tokenIndex: ctx.indexes.tokenIndex
|
|
890
|
-
}
|
|
891
|
-
);
|
|
892
|
-
const context = args?.context?.toLowerCase() ?? "";
|
|
893
|
-
const fullQuery = context ? `${useCase} ${context}` : useCase;
|
|
894
|
-
const searchResults = await hybridSearch(
|
|
895
|
-
fullQuery,
|
|
896
|
-
localData,
|
|
897
|
-
suggestLimit,
|
|
898
|
-
"component",
|
|
899
|
-
ctx.config.searchApiKey
|
|
900
|
-
);
|
|
901
|
-
const filteredSearchResults = searchResults.filter((result2) => {
|
|
902
|
-
const component = componentsByName.get(result2.name.toLowerCase());
|
|
903
|
-
if (!component) return false;
|
|
904
|
-
if (category && !categoryMatches(component.category, category)) return false;
|
|
905
|
-
if (status && component.status !== status) return false;
|
|
906
|
-
result2.score += getRankingBonus(component);
|
|
907
|
-
return true;
|
|
908
|
-
});
|
|
909
|
-
const blockMatches = keywordScoreBlocks(
|
|
910
|
-
fullQuery,
|
|
911
|
-
allBlocks,
|
|
912
|
-
ctx.indexes.blockIndex ?? void 0
|
|
913
|
-
).slice(0, 5);
|
|
914
|
-
if (blockMatches.length > 0) {
|
|
915
|
-
const matchedBlocks = blockMatches.map(
|
|
916
|
-
(match) => allBlocks.find((block) => block.name.toLowerCase() === match.name.toLowerCase())
|
|
917
|
-
).filter(Boolean);
|
|
918
|
-
const blockFreq = buildBlockComponentFrequency(matchedBlocks);
|
|
919
|
-
boostByBlockFrequency(filteredSearchResults, blockFreq);
|
|
920
|
-
}
|
|
921
|
-
const maxScore = filteredSearchResults.length > 0 ? filteredSearchResults[0].score : 0;
|
|
922
|
-
const scored = filteredSearchResults.map((result2) => {
|
|
923
|
-
const component = componentsByName.get(result2.name.toLowerCase());
|
|
924
|
-
if (!component) return null;
|
|
925
|
-
return {
|
|
926
|
-
component: component.name,
|
|
927
|
-
category: component.category,
|
|
928
|
-
description: component.description,
|
|
929
|
-
confidence: assignConfidence(result2.score, maxScore),
|
|
930
|
-
reasons: [`Matched via hybrid search (score: ${result2.score.toFixed(4)})`],
|
|
931
|
-
usage: {
|
|
932
|
-
when: getGuidanceWhen(component).slice(0, 3),
|
|
933
|
-
whenNot: getGuidanceWhenNot(component).slice(0, 2)
|
|
934
|
-
},
|
|
935
|
-
publicRef: component.publicRef,
|
|
936
|
-
componentKey: component.id,
|
|
937
|
-
tier: component.tier,
|
|
938
|
-
isCanonical: component.isCanonical ?? false,
|
|
939
|
-
sourcePath: component.sourcePath,
|
|
940
|
-
exampleCount: component.examples.length,
|
|
941
|
-
status: component.status
|
|
942
|
-
};
|
|
943
|
-
}).filter(Boolean);
|
|
944
|
-
const suggestions = [];
|
|
945
|
-
const categoryCount = {};
|
|
946
|
-
for (const item of scored) {
|
|
947
|
-
if (!item) continue;
|
|
948
|
-
const cat = item.category || "uncategorized";
|
|
949
|
-
const count = categoryCount[cat] || 0;
|
|
950
|
-
if (count < 2 || suggestions.length < 3) {
|
|
951
|
-
suggestions.push(item);
|
|
952
|
-
categoryCount[cat] = count + 1;
|
|
953
|
-
if (suggestions.length >= suggestLimit) break;
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
const compositionHint = suggestions.length >= 2 ? `These components work well together. For example, ${suggestions[0].component} can be combined with ${suggestions.slice(1, 3).map((item) => item.component).join(" and ")}.` : void 0;
|
|
957
|
-
const useCaseLower = useCase.toLowerCase();
|
|
958
|
-
const styleKeywords = [
|
|
959
|
-
"color",
|
|
960
|
-
"spacing",
|
|
961
|
-
"padding",
|
|
962
|
-
"margin",
|
|
963
|
-
"font",
|
|
964
|
-
"border",
|
|
965
|
-
"radius",
|
|
966
|
-
"shadow",
|
|
967
|
-
"variable",
|
|
968
|
-
"token",
|
|
969
|
-
"css",
|
|
970
|
-
"theme",
|
|
971
|
-
"dark mode",
|
|
972
|
-
"background",
|
|
973
|
-
"hover"
|
|
974
|
-
];
|
|
975
|
-
const isStyleQuery = styleKeywords.some(
|
|
976
|
-
(keyword) => useCaseLower.includes(keyword)
|
|
977
|
-
);
|
|
978
|
-
const noMatch = suggestions.length === 0 || !suggestions.some((item) => {
|
|
979
|
-
const component = componentsByName.get(item.component.toLowerCase());
|
|
980
|
-
return component ? hasDirectQueryOverlap(useCase, component) : false;
|
|
981
|
-
});
|
|
982
|
-
const belowThreshold = !noMatch && maxScore > 1 && !meetsMinimumThreshold(maxScore);
|
|
983
|
-
const weakMatch = !noMatch && (belowThreshold || suggestions.every((item) => item.confidence === "low"));
|
|
984
|
-
let recommendation;
|
|
985
|
-
let nextStep;
|
|
986
|
-
if (noMatch) {
|
|
987
|
-
recommendation = isStyleQuery ? `No matching components found. Your query seems styling-related \u2014 try ${ctx.toolNames.tokens} to find tokens.` : `No matching components found. Try different keywords or browse all components with ${ctx.toolNames.discover}.`;
|
|
988
|
-
nextStep = isStyleQuery ? `Use ${ctx.toolNames.tokens}(search: "${useCaseLower.split(/\s+/)[0]}") to find tokens.` : void 0;
|
|
989
|
-
} else if (weakMatch) {
|
|
990
|
-
recommendation = `Weak matches only \u2014 ${suggestions[0].component} might work but confidence is low.${isStyleQuery ? ` If you need tokens, try ${ctx.toolNames.tokens}.` : ""}`;
|
|
991
|
-
nextStep = `Use ${ctx.toolNames.inspect}("${suggestions[0].component}") to check if it fits, or try broader search terms.`;
|
|
992
|
-
} else {
|
|
993
|
-
recommendation = `Best match: ${suggestions[0].component} (${suggestions[0].confidence} confidence) - ${suggestions[0].description}`;
|
|
994
|
-
nextStep = `Use ${ctx.toolNames.inspect}("${suggestions[0].component}") for full details.`;
|
|
995
|
-
}
|
|
996
|
-
const tokenHint = isStyleQuery && !noMatch ? `Your query includes styling terms. For tokens, also try ${ctx.toolNames.tokens}(search: "${useCaseLower.split(/\s+/)[0]}").` : void 0;
|
|
997
|
-
const blockNames = blockMatches.map(
|
|
998
|
-
(match) => allBlocks.find((block) => block.name.toLowerCase() === match.name.toLowerCase())
|
|
999
|
-
).filter(Boolean).slice(0, 3).map((block) => block.name);
|
|
1000
|
-
const blockHint = blockNames.length > 0 ? `Related blocks: ${blockNames.join(", ")}. Use ${ctx.toolNames.blocks}(search: "${useCase}") for ready-to-use patterns.` : void 0;
|
|
1001
|
-
let fullBlocks;
|
|
1002
|
-
let fullTokens;
|
|
1003
|
-
let fullImports;
|
|
1004
|
-
if (depth === "full" && !noMatch) {
|
|
1005
|
-
const tokenData = ctx.data.tokens;
|
|
1006
|
-
const [blockSearchResults, tokenSearchResults] = await Promise.all([
|
|
1007
|
-
hybridSearch(fullQuery, localData, 5, "block", ctx.config.searchApiKey),
|
|
1008
|
-
tokenData ? hybridSearch(fullQuery, localData, 10, "token", ctx.config.searchApiKey) : Promise.resolve([])
|
|
1009
|
-
]);
|
|
1010
|
-
const topBlockScore = blockSearchResults.length > 0 ? blockSearchResults[0].score : 0;
|
|
1011
|
-
const relevantBlockResults = blockSearchResults.filter(
|
|
1012
|
-
(result2) => result2.score >= topBlockScore * 0.3
|
|
1013
|
-
);
|
|
1014
|
-
if (relevantBlockResults.length > 0) {
|
|
1015
|
-
fullBlocks = (await Promise.all(
|
|
1016
|
-
relevantBlockResults.slice(0, 5).map(async (result2) => {
|
|
1017
|
-
const block = allBlocks.find(
|
|
1018
|
-
(entry) => entry.name.toLowerCase() === result2.name.toLowerCase()
|
|
1019
|
-
);
|
|
1020
|
-
if (!block) return null;
|
|
1021
|
-
const imports = await buildImportStatements(
|
|
1022
|
-
block.components,
|
|
1023
|
-
async (componentName) => ctx.resolvePackageName(componentName)
|
|
1024
|
-
);
|
|
1025
|
-
const codeLines = block.code.split("\n");
|
|
1026
|
-
const code = codeLines.length > 30 ? `${codeLines.slice(0, 20).join("\n")}
|
|
1027
|
-
// ... truncated (${codeLines.length} lines total)` : block.code;
|
|
1028
|
-
return {
|
|
1029
|
-
name: block.name,
|
|
1030
|
-
description: block.description,
|
|
1031
|
-
components: block.components,
|
|
1032
|
-
code,
|
|
1033
|
-
imports
|
|
1034
|
-
};
|
|
1035
|
-
})
|
|
1036
|
-
)).filter(Boolean);
|
|
1037
|
-
}
|
|
1038
|
-
if (tokenSearchResults.length > 0 && tokenData) {
|
|
1039
|
-
fullTokens = {};
|
|
1040
|
-
const tokensByName = /* @__PURE__ */ new Map();
|
|
1041
|
-
for (const [cat, tokens] of Object.entries(tokenData.categories)) {
|
|
1042
|
-
for (const token of tokens) {
|
|
1043
|
-
tokensByName.set(token.name, cat);
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
for (const result2 of tokenSearchResults) {
|
|
1047
|
-
const cat = tokensByName.get(result2.name);
|
|
1048
|
-
if (cat) {
|
|
1049
|
-
if (!fullTokens[cat]) fullTokens[cat] = [];
|
|
1050
|
-
fullTokens[cat].push(result2.name);
|
|
1051
|
-
}
|
|
1052
|
-
}
|
|
1053
|
-
if (Object.keys(fullTokens).length === 0) fullTokens = void 0;
|
|
1054
|
-
}
|
|
1055
|
-
if (!fullTokens && tokenData) {
|
|
1056
|
-
const categories = extractTokenCategories(fullQuery);
|
|
1057
|
-
fullTokens = {};
|
|
1058
|
-
for (const cat of categories) {
|
|
1059
|
-
const tokens = tokenData.categories[cat];
|
|
1060
|
-
if (tokens && tokens.length > 0) {
|
|
1061
|
-
fullTokens[cat] = tokens.slice(0, 5).map((token) => token.name);
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
if (Object.keys(fullTokens).length === 0) fullTokens = void 0;
|
|
1065
|
-
}
|
|
1066
|
-
if (suggestions.length > 0) {
|
|
1067
|
-
fullImports = {};
|
|
1068
|
-
for (const item of suggestions) {
|
|
1069
|
-
if (!item) continue;
|
|
1070
|
-
const pkgName = ctx.resolvePackageName(item.component);
|
|
1071
|
-
fullImports[item.component] = `import { ${item.component} } from '${pkgName}';`;
|
|
1072
|
-
}
|
|
1073
|
-
}
|
|
1074
|
-
}
|
|
1075
|
-
const suggestResponse = verbosity === "compact" ? {
|
|
1076
|
-
useCase,
|
|
1077
|
-
suggestions: suggestions.map((item) => ({
|
|
1078
|
-
component: item.component,
|
|
1079
|
-
description: item.description,
|
|
1080
|
-
confidence: item.confidence
|
|
1081
|
-
})),
|
|
1082
|
-
recommendation
|
|
1083
|
-
} : {
|
|
1084
|
-
useCase,
|
|
1085
|
-
context: context || void 0,
|
|
1086
|
-
suggestions: depth === "full" ? suggestions.map((item) => ({
|
|
1087
|
-
...item,
|
|
1088
|
-
import: fullImports?.[item.component]
|
|
1089
|
-
})) : suggestions,
|
|
1090
|
-
noMatch,
|
|
1091
|
-
weakMatch,
|
|
1092
|
-
recommendation,
|
|
1093
|
-
compositionHint,
|
|
1094
|
-
...tokenHint && { tokenHint },
|
|
1095
|
-
...blockHint && { blockHint },
|
|
1096
|
-
nextStep,
|
|
1097
|
-
...fullBlocks && fullBlocks.length > 0 && { blocks: fullBlocks },
|
|
1098
|
-
...fullTokens && { tokens: fullTokens }
|
|
1099
|
-
};
|
|
1100
|
-
return {
|
|
1101
|
-
content: [
|
|
1102
|
-
{
|
|
1103
|
-
type: "text",
|
|
1104
|
-
text: JSON.stringify(suggestResponse)
|
|
1105
|
-
}
|
|
1106
|
-
]
|
|
1107
|
-
};
|
|
1108
|
-
}
|
|
1109
|
-
if (componentForAlts) {
|
|
1110
|
-
const component = findComponent(data.snapshot, componentForAlts);
|
|
1111
|
-
if (!component) {
|
|
1112
|
-
const closest = findClosestMatch(
|
|
1113
|
-
componentForAlts,
|
|
1114
|
-
componentNames(data.snapshot)
|
|
1115
|
-
);
|
|
1116
|
-
const suggestion = closest ? ` Did you mean "${closest}"?` : "";
|
|
1117
|
-
return {
|
|
1118
|
-
content: [
|
|
1119
|
-
{
|
|
1120
|
-
type: "text",
|
|
1121
|
-
text: JSON.stringify({
|
|
1122
|
-
error: `Component "${componentForAlts}" not found.${suggestion} Use ${ctx.toolNames.discover} to see available components.`
|
|
1123
|
-
})
|
|
1124
|
-
}
|
|
1125
|
-
],
|
|
1126
|
-
isError: true
|
|
1127
|
-
};
|
|
1128
|
-
}
|
|
1129
|
-
const relations = component.relations;
|
|
1130
|
-
const referencedBy = allSnapshotComponents.map((entry) => {
|
|
1131
|
-
const relation = entry.relations.find(
|
|
1132
|
-
(candidate) => candidate.componentName.toLowerCase() === component.name.toLowerCase()
|
|
1133
|
-
);
|
|
1134
|
-
if (!relation) return null;
|
|
1135
|
-
return {
|
|
1136
|
-
component: entry.name,
|
|
1137
|
-
relationship: relation.relationship,
|
|
1138
|
-
note: relation.note
|
|
1139
|
-
};
|
|
1140
|
-
}).filter(Boolean);
|
|
1141
|
-
const sameCategory = allSnapshotComponents.filter(
|
|
1142
|
-
(entry) => entry.category === component.category && entry.name.toLowerCase() !== component.name.toLowerCase()
|
|
1143
|
-
).map((entry) => ({
|
|
1144
|
-
component: entry.name,
|
|
1145
|
-
description: entry.description
|
|
1146
|
-
}));
|
|
1147
|
-
return {
|
|
1148
|
-
content: [
|
|
1149
|
-
{
|
|
1150
|
-
type: "text",
|
|
1151
|
-
text: JSON.stringify({
|
|
1152
|
-
component: component.name,
|
|
1153
|
-
category: component.category,
|
|
1154
|
-
directRelations: relations,
|
|
1155
|
-
referencedBy,
|
|
1156
|
-
sameCategory,
|
|
1157
|
-
suggestion: relations.find(
|
|
1158
|
-
(relation) => relation.relationship === "alternative"
|
|
1159
|
-
) ? `Consider ${relations.find((relation) => relation.relationship === "alternative")?.componentName}: ${relations.find((relation) => relation.relationship === "alternative")?.note}` : void 0
|
|
1160
|
-
})
|
|
1161
|
-
}
|
|
1162
|
-
]
|
|
1163
|
-
};
|
|
1164
|
-
}
|
|
1165
|
-
let filteredComponents = allSnapshotComponents.filter((component) => {
|
|
1166
|
-
if (category && !categoryMatches(component.category, category)) return false;
|
|
1167
|
-
if (status && (component.status ?? "stable") !== status) return false;
|
|
1168
|
-
return true;
|
|
1169
|
-
});
|
|
1170
|
-
if (search2) {
|
|
1171
|
-
const scored = keywordScoreComponents(
|
|
1172
|
-
search2,
|
|
1173
|
-
filteredComponents,
|
|
1174
|
-
ctx.indexes.componentIndex ?? void 0
|
|
1175
|
-
).map((result2) => {
|
|
1176
|
-
const component = filteredComponents.find(
|
|
1177
|
-
(entry) => entry.name.toLowerCase() === result2.name.toLowerCase()
|
|
1178
|
-
);
|
|
1179
|
-
if (!component) return null;
|
|
1180
|
-
return {
|
|
1181
|
-
component,
|
|
1182
|
-
score: result2.score + getRankingBonus(component)
|
|
1183
|
-
};
|
|
1184
|
-
}).filter(Boolean);
|
|
1185
|
-
filteredComponents = scored.sort((a, b) => b.score - a.score).map((entry) => entry.component);
|
|
1186
|
-
} else {
|
|
1187
|
-
filteredComponents = filteredComponents.sort((a, b) => {
|
|
1188
|
-
const bonusDiff = getRankingBonus(b) - getRankingBonus(a);
|
|
1189
|
-
if (bonusDiff !== 0) return bonusDiff;
|
|
1190
|
-
return a.name.localeCompare(b.name);
|
|
1191
|
-
});
|
|
1192
|
-
}
|
|
1193
|
-
const limitedComponents = listLimit === void 0 ? filteredComponents : filteredComponents.slice(0, listLimit);
|
|
1194
|
-
const components = limitedComponents.map((component) => {
|
|
1195
|
-
if (verbosity === "compact") {
|
|
1196
|
-
return {
|
|
1197
|
-
name: component.name,
|
|
1198
|
-
category: component.category,
|
|
1199
|
-
publicRef: component.publicRef,
|
|
1200
|
-
componentKey: component.id,
|
|
1201
|
-
tier: component.tier,
|
|
1202
|
-
isCanonical: component.isCanonical ?? false,
|
|
1203
|
-
...component.propsSummary.length > 0 && {
|
|
1204
|
-
propsSummary: component.propsSummary
|
|
1205
|
-
}
|
|
1206
|
-
};
|
|
1207
|
-
}
|
|
1208
|
-
return {
|
|
1209
|
-
name: component.name,
|
|
1210
|
-
category: component.category,
|
|
1211
|
-
description: component.description,
|
|
1212
|
-
status: component.status ?? "stable",
|
|
1213
|
-
publicRef: component.publicRef,
|
|
1214
|
-
componentKey: component.id,
|
|
1215
|
-
tier: component.tier,
|
|
1216
|
-
isCanonical: component.isCanonical ?? false,
|
|
1217
|
-
sourcePath: component.sourcePath,
|
|
1218
|
-
exampleCount: component.examples.length,
|
|
1219
|
-
tags: component.tags,
|
|
1220
|
-
...(includeCode || verbosity === "full") && component.examples[0]?.code ? {
|
|
1221
|
-
example: component.examples[0].code
|
|
1222
|
-
} : {}
|
|
1223
|
-
};
|
|
1224
|
-
});
|
|
1225
|
-
return {
|
|
1226
|
-
content: [
|
|
1227
|
-
{
|
|
1228
|
-
type: "text",
|
|
1229
|
-
text: JSON.stringify({
|
|
1230
|
-
total: filteredComponents.length,
|
|
1231
|
-
returned: components.length,
|
|
1232
|
-
components,
|
|
1233
|
-
categories: [...new Set(components.map((component) => component.category))],
|
|
1234
|
-
hint: components.length === 0 ? "No components found. Try broader search terms or check available categories." : components.length > 5 ? `Use ${ctx.toolNames.discover} with useCase for recommendations, or ${ctx.toolNames.inspect} for details on a specific component.` : void 0
|
|
1235
|
-
})
|
|
1236
|
-
}
|
|
1237
|
-
]
|
|
1238
|
-
};
|
|
1239
|
-
};
|
|
1240
|
-
|
|
1241
|
-
// src/tools/inspect.ts
|
|
1242
|
-
import { promises as fs } from "fs";
|
|
1243
|
-
import { existsSync as existsSync2 } from "fs";
|
|
1244
|
-
import { join as join2 } from "path";
|
|
1245
|
-
|
|
1246
|
-
// src/utils.ts
|
|
1247
|
-
function projectFields(obj, fields) {
|
|
1248
|
-
if (!fields || fields.length === 0) {
|
|
1249
|
-
return obj;
|
|
1250
|
-
}
|
|
1251
|
-
const result2 = {};
|
|
1252
|
-
for (const field of fields) {
|
|
1253
|
-
const parts = field.split(".");
|
|
1254
|
-
let source = obj;
|
|
1255
|
-
let target = result2;
|
|
1256
|
-
for (let i = 0; i < parts.length; i++) {
|
|
1257
|
-
const part = parts[i];
|
|
1258
|
-
const isLast = i === parts.length - 1;
|
|
1259
|
-
if (source === null || source === void 0 || typeof source !== "object") {
|
|
1260
|
-
break;
|
|
1261
|
-
}
|
|
1262
|
-
const sourceObj = source;
|
|
1263
|
-
const value = sourceObj[part];
|
|
1264
|
-
if (isLast) {
|
|
1265
|
-
target[part] = value;
|
|
1266
|
-
} else {
|
|
1267
|
-
if (!(part in target)) {
|
|
1268
|
-
target[part] = {};
|
|
1269
|
-
}
|
|
1270
|
-
target = target[part];
|
|
1271
|
-
source = value;
|
|
1272
|
-
}
|
|
1273
|
-
}
|
|
1274
|
-
}
|
|
1275
|
-
return result2;
|
|
1276
|
-
}
|
|
1277
|
-
|
|
1278
|
-
// src/tools/inspect.ts
|
|
1279
|
-
async function getSourceCode(component, projectRoot) {
|
|
1280
|
-
const sourcePath = component.sourcePath;
|
|
1281
|
-
if (!sourcePath) return void 0;
|
|
1282
|
-
const fullPath = join2(projectRoot, sourcePath);
|
|
1283
|
-
if (!existsSync2(fullPath)) return { path: sourcePath, code: null };
|
|
1284
|
-
try {
|
|
1285
|
-
const code = await fs.readFile(fullPath, "utf-8");
|
|
1286
|
-
return { path: sourcePath, code };
|
|
1287
|
-
} catch {
|
|
1288
|
-
return { path: sourcePath, code: null };
|
|
1289
|
-
}
|
|
1290
|
-
}
|
|
1291
|
-
var inspectHandler = async (args, ctx) => {
|
|
1292
|
-
const componentName = args?.component;
|
|
1293
|
-
const fields = args?.fields;
|
|
1294
|
-
const exampleName = args?.variant ?? void 0;
|
|
1295
|
-
const maxExamples = args?.maxExamples;
|
|
1296
|
-
const maxLines = args?.maxLines;
|
|
1297
|
-
const verbosity = args?.verbosity ?? "standard";
|
|
1298
|
-
if (!componentName) {
|
|
1299
|
-
return {
|
|
1300
|
-
content: [
|
|
1301
|
-
{
|
|
1302
|
-
type: "text",
|
|
1303
|
-
text: JSON.stringify({ error: "component is required" })
|
|
1304
|
-
}
|
|
1305
|
-
],
|
|
1306
|
-
isError: true
|
|
1307
|
-
};
|
|
1308
|
-
}
|
|
1309
|
-
const component = findComponent(ctx.data.snapshot, componentName);
|
|
1310
|
-
if (!component) {
|
|
1311
|
-
const closest = findClosestMatch(
|
|
1312
|
-
componentName,
|
|
1313
|
-
componentNames(ctx.data.snapshot)
|
|
1314
|
-
);
|
|
1315
|
-
const suggestion = closest ? ` Did you mean "${closest}"? Use ${ctx.toolNames.inspect}("${closest}") to inspect it.` : "";
|
|
1316
|
-
return {
|
|
1317
|
-
content: [
|
|
1318
|
-
{
|
|
1319
|
-
type: "text",
|
|
1320
|
-
text: JSON.stringify({
|
|
1321
|
-
error: `Component "${componentName}" not found.${suggestion} Use ${ctx.toolNames.discover} to see available components.`
|
|
1322
|
-
})
|
|
1323
|
-
}
|
|
1324
|
-
],
|
|
1325
|
-
isError: true
|
|
1326
|
-
};
|
|
1327
|
-
}
|
|
1328
|
-
const pkgName = ctx.resolvePackageName(component.name);
|
|
1329
|
-
let examples = component.examples;
|
|
1330
|
-
if (exampleName) {
|
|
1331
|
-
const query = exampleName.toLowerCase();
|
|
1332
|
-
let filtered = examples.filter((example) => example.name.toLowerCase() === query);
|
|
1333
|
-
if (filtered.length === 0) {
|
|
1334
|
-
filtered = examples.filter(
|
|
1335
|
-
(example) => example.name.toLowerCase().startsWith(query)
|
|
1336
|
-
);
|
|
1337
|
-
}
|
|
1338
|
-
if (filtered.length === 0) {
|
|
1339
|
-
filtered = examples.filter(
|
|
1340
|
-
(example) => example.name.toLowerCase().includes(query)
|
|
1341
|
-
);
|
|
1342
|
-
}
|
|
1343
|
-
if (filtered.length > 0) {
|
|
1344
|
-
examples = filtered;
|
|
1345
|
-
} else {
|
|
1346
|
-
return {
|
|
1347
|
-
content: [
|
|
1348
|
-
{
|
|
1349
|
-
type: "text",
|
|
1350
|
-
text: JSON.stringify({
|
|
1351
|
-
error: `Example "${exampleName}" not found for ${componentName}. Available: ${component.examples.map((example) => example.name).join(", ")}`
|
|
1352
|
-
})
|
|
1353
|
-
}
|
|
1354
|
-
],
|
|
1355
|
-
isError: true
|
|
1356
|
-
};
|
|
1357
|
-
}
|
|
1358
|
-
}
|
|
1359
|
-
if (maxExamples && maxExamples > 0) {
|
|
1360
|
-
examples = examples.slice(0, maxExamples);
|
|
1361
|
-
}
|
|
1362
|
-
const truncateCode = (code) => {
|
|
1363
|
-
if (!maxLines || maxLines <= 0) {
|
|
1364
|
-
return { code, truncated: false, remainingLines: 0 };
|
|
1365
|
-
}
|
|
1366
|
-
const lines = code.split("\n");
|
|
1367
|
-
if (lines.length <= maxLines) {
|
|
1368
|
-
return { code, truncated: false, remainingLines: 0 };
|
|
1369
|
-
}
|
|
1370
|
-
return {
|
|
1371
|
-
code: lines.slice(0, maxLines).join("\n"),
|
|
1372
|
-
truncated: true,
|
|
1373
|
-
remainingLines: lines.length - maxLines
|
|
1374
|
-
};
|
|
1375
|
-
};
|
|
1376
|
-
const renderedExamples = examples.map((example) => ({
|
|
1377
|
-
...example.code ? truncateCode(example.code) : { truncated: false, remainingLines: 0 },
|
|
1378
|
-
variant: example.name,
|
|
1379
|
-
description: example.description,
|
|
1380
|
-
code: example.code ? truncateCode(example.code).code : `<${component.name} />`,
|
|
1381
|
-
...example.code ? {} : {
|
|
1382
|
-
note: "No code example provided. Refer to props for customization."
|
|
1383
|
-
}
|
|
1384
|
-
}));
|
|
1385
|
-
const propsReference = Object.entries(component.props ?? {}).map(
|
|
1386
|
-
([propName, prop]) => ({
|
|
1387
|
-
name: propName,
|
|
1388
|
-
type: prop.type,
|
|
1389
|
-
required: prop.required,
|
|
1390
|
-
default: prop.default,
|
|
1391
|
-
description: prop.description,
|
|
1392
|
-
values: prop.values
|
|
1393
|
-
})
|
|
1394
|
-
);
|
|
1395
|
-
const propConstraints = Object.entries(component.props ?? {}).filter(
|
|
1396
|
-
([, prop]) => Boolean(prop.constraints && prop.constraints.length > 0)
|
|
1397
|
-
).map(([propName, prop]) => ({
|
|
1398
|
-
prop: propName,
|
|
1399
|
-
constraints: prop.constraints
|
|
1400
|
-
}));
|
|
1401
|
-
const fullResult = {
|
|
1402
|
-
meta: {
|
|
1403
|
-
id: component.id,
|
|
1404
|
-
name: component.name,
|
|
1405
|
-
description: component.description,
|
|
1406
|
-
category: component.category,
|
|
1407
|
-
status: component.status,
|
|
1408
|
-
publicRef: component.publicRef,
|
|
1409
|
-
publicSlug: component.publicSlug,
|
|
1410
|
-
isCanonical: component.isCanonical ?? false,
|
|
1411
|
-
tier: component.tier
|
|
1412
|
-
},
|
|
1413
|
-
props: propsReference,
|
|
1414
|
-
examples: {
|
|
1415
|
-
import: `import { ${component.name} } from '${pkgName}';`,
|
|
1416
|
-
code: renderedExamples
|
|
1417
|
-
},
|
|
1418
|
-
relations: component.relations,
|
|
1419
|
-
compoundChildren: component.compoundChildren,
|
|
1420
|
-
guidance: {
|
|
1421
|
-
when: getGuidanceWhen(component),
|
|
1422
|
-
whenNot: getGuidanceWhenNot(component),
|
|
1423
|
-
guidelines: component.guidance.guidelines,
|
|
1424
|
-
accessibility: component.guidance.accessibility,
|
|
1425
|
-
usageGuidance: component.guidance.usageGuidance,
|
|
1426
|
-
dos: component.guidance.dos,
|
|
1427
|
-
donts: component.guidance.donts,
|
|
1428
|
-
patterns: component.guidance.patterns,
|
|
1429
|
-
propConstraints,
|
|
1430
|
-
alternatives: component.relations?.filter((relation) => relation.relationship === "alternative").map((relation) => ({
|
|
1431
|
-
component: relation.componentName,
|
|
1432
|
-
note: relation.note
|
|
1433
|
-
})) ?? []
|
|
1434
|
-
},
|
|
1435
|
-
metadata: component.metadata,
|
|
1436
|
-
source: await getSourceCode(component, ctx.config.projectRoot)
|
|
1437
|
-
};
|
|
1438
|
-
const aliasMap = { usage: "guidance" };
|
|
1439
|
-
const resolvedFields = fields?.map((field) => {
|
|
1440
|
-
const parts = field.split(".");
|
|
1441
|
-
if (aliasMap[parts[0]]) parts[0] = aliasMap[parts[0]];
|
|
1442
|
-
return parts.join(".");
|
|
1443
|
-
});
|
|
1444
|
-
let result2;
|
|
1445
|
-
if (verbosity === "compact" && !resolvedFields?.length) {
|
|
1446
|
-
result2 = {
|
|
1447
|
-
meta: fullResult.meta,
|
|
1448
|
-
propsSummary: component.propsSummary,
|
|
1449
|
-
metadata: component.metadata
|
|
1450
|
-
};
|
|
1451
|
-
} else if (verbosity === "full") {
|
|
1452
|
-
result2 = resolvedFields && resolvedFields.length > 0 ? projectFields(fullResult, resolvedFields) : fullResult;
|
|
1453
|
-
} else if (resolvedFields && resolvedFields.length > 0) {
|
|
1454
|
-
result2 = projectFields(fullResult, resolvedFields);
|
|
1455
|
-
} else {
|
|
1456
|
-
const { source: _source, ...withoutSource } = fullResult;
|
|
1457
|
-
result2 = withoutSource;
|
|
1458
|
-
}
|
|
1459
|
-
return {
|
|
1460
|
-
content: [{ type: "text", text: JSON.stringify(result2) }]
|
|
1461
|
-
};
|
|
1462
|
-
};
|
|
1463
|
-
|
|
1464
|
-
// src/tools/blocks.ts
|
|
1465
|
-
var blocksHandler = async (args, ctx) => {
|
|
1466
|
-
const blockName = args?.name;
|
|
1467
|
-
const search2 = args?.search?.toLowerCase() ?? void 0;
|
|
1468
|
-
const component = args?.component?.toLowerCase() ?? void 0;
|
|
1469
|
-
const category = args?.category?.toLowerCase() ?? void 0;
|
|
1470
|
-
const blocksLimit = typeof args?.limit === "number" ? Math.min(Math.max(args.limit, 1), 50) : void 0;
|
|
1471
|
-
const allBlocks = listBlocks(ctx.data.snapshot);
|
|
1472
|
-
if (allBlocks.length === 0) {
|
|
1473
|
-
return {
|
|
1474
|
-
content: [{
|
|
1475
|
-
type: "text",
|
|
1476
|
-
text: JSON.stringify({
|
|
1477
|
-
total: 0,
|
|
1478
|
-
blocks: [],
|
|
1479
|
-
hint: `No composition blocks found. Blocks are reusable patterns showing how components wire together (e.g., "Login Form", "Settings Page"). Create .block.ts files and run \`${BRAND.cliCommand} build\`.`
|
|
1480
|
-
})
|
|
1481
|
-
}]
|
|
1482
|
-
};
|
|
1483
|
-
}
|
|
1484
|
-
let filtered = allBlocks;
|
|
1485
|
-
if (blockName) {
|
|
1486
|
-
filtered = filtered.filter(
|
|
1487
|
-
(b) => b.name.toLowerCase() === blockName.toLowerCase()
|
|
1488
|
-
);
|
|
1489
|
-
if (filtered.length === 0) {
|
|
1490
|
-
const allBlockNames = allBlocks.map((b) => b.name);
|
|
1491
|
-
const closest = findClosestMatch(blockName, allBlockNames);
|
|
1492
|
-
const suggestion = closest ? ` Did you mean "${closest}"?` : "";
|
|
1493
|
-
throw new Error(`Block "${blockName}" not found.${suggestion} Use ${ctx.toolNames.blocks} to see available blocks.`);
|
|
1494
|
-
}
|
|
1495
|
-
}
|
|
1496
|
-
if (search2) {
|
|
1497
|
-
if (ctx.indexes.blockIndex) {
|
|
1498
|
-
const ranked = searchBlocks(search2, ctx.indexes.blockIndex, 50);
|
|
1499
|
-
const rankedNames = new Set(ranked.map((r) => r.name.toLowerCase()));
|
|
1500
|
-
filtered = filtered.filter((b) => rankedNames.has(b.name.toLowerCase()));
|
|
1501
|
-
filtered.sort((a, b) => {
|
|
1502
|
-
const aIdx = ranked.findIndex((r) => r.name.toLowerCase() === a.name.toLowerCase());
|
|
1503
|
-
const bIdx = ranked.findIndex((r) => r.name.toLowerCase() === b.name.toLowerCase());
|
|
1504
|
-
return (aIdx === -1 ? Infinity : aIdx) - (bIdx === -1 ? Infinity : bIdx);
|
|
1505
|
-
});
|
|
1506
|
-
} else {
|
|
1507
|
-
filtered = filtered.filter((b) => {
|
|
1508
|
-
const haystack = [
|
|
1509
|
-
b.name,
|
|
1510
|
-
b.description,
|
|
1511
|
-
...b.tags ?? [],
|
|
1512
|
-
...b.components,
|
|
1513
|
-
b.category
|
|
1514
|
-
].join(" ").toLowerCase();
|
|
1515
|
-
return haystack.includes(search2);
|
|
1516
|
-
});
|
|
1517
|
-
}
|
|
1518
|
-
}
|
|
1519
|
-
if (component) {
|
|
1520
|
-
filtered = filtered.filter(
|
|
1521
|
-
(b) => b.components.some((c) => c.toLowerCase() === component)
|
|
1522
|
-
);
|
|
1523
|
-
}
|
|
1524
|
-
if (category) {
|
|
1525
|
-
filtered = filtered.filter(
|
|
1526
|
-
(b) => b.category.toLowerCase() === category
|
|
1527
|
-
);
|
|
1528
|
-
}
|
|
1529
|
-
const blocksUseIcons = filtered.some(
|
|
1530
|
-
(b) => b.components.some((c) => c === "Icon") || b.code && /\bIcon\b/.test(b.code)
|
|
1531
|
-
);
|
|
1532
|
-
const iconHint = blocksUseIcons ? "Icon components in block code are from @phosphor-icons/react. Import them as: import { IconName } from '@phosphor-icons/react';" : void 0;
|
|
1533
|
-
if (blocksLimit !== void 0) {
|
|
1534
|
-
filtered = filtered.slice(0, blocksLimit);
|
|
1535
|
-
}
|
|
1536
|
-
const verbosity = args?.verbosity ?? "standard";
|
|
1537
|
-
const blocksWithImports = await Promise.all(filtered.map(async (b) => {
|
|
1538
|
-
const imports = await buildImportStatements(
|
|
1539
|
-
b.components,
|
|
1540
|
-
async (componentName) => ctx.resolvePackageName(componentName)
|
|
1541
|
-
);
|
|
1542
|
-
const base = {
|
|
1543
|
-
name: b.name,
|
|
1544
|
-
description: b.description,
|
|
1545
|
-
category: b.category,
|
|
1546
|
-
components: b.components,
|
|
1547
|
-
tags: b.tags,
|
|
1548
|
-
import: imports.join("\n"),
|
|
1549
|
-
imports
|
|
1550
|
-
};
|
|
1551
|
-
if (verbosity === "compact") return base;
|
|
1552
|
-
if (verbosity === "full") return { ...base, code: b.code };
|
|
1553
|
-
const codeLines = b.code.split("\n");
|
|
1554
|
-
const code = codeLines.length > 30 ? codeLines.slice(0, 20).join("\n") + "\n// ... truncated (" + codeLines.length + " lines total)" : b.code;
|
|
1555
|
-
return { ...base, code };
|
|
1556
|
-
}));
|
|
1557
|
-
return {
|
|
1558
|
-
content: [{
|
|
1559
|
-
type: "text",
|
|
1560
|
-
text: JSON.stringify({
|
|
1561
|
-
total: blocksWithImports.length,
|
|
1562
|
-
blocks: blocksWithImports,
|
|
1563
|
-
...iconHint && { iconHint },
|
|
1564
|
-
...blocksWithImports.length === 0 && allBlocks.length > 0 && {
|
|
1565
|
-
hint: "No blocks matching your query. Try broader search terms."
|
|
1566
|
-
}
|
|
1567
|
-
})
|
|
1568
|
-
}]
|
|
1569
|
-
};
|
|
1570
|
-
};
|
|
1571
|
-
|
|
1572
|
-
// src/tools/tokens.ts
|
|
1573
|
-
var TOKEN_CATEGORY_ALIASES = {
|
|
1574
|
-
colors: ["color", "colors", "accent", "background", "foreground", "theme"],
|
|
1575
|
-
spacing: ["spacing", "space", "spaces", "padding", "margin", "gap", "inset"],
|
|
1576
|
-
typography: ["typography", "type", "font", "fonts", "letter", "line-height"],
|
|
1577
|
-
surfaces: ["surface", "surfaces", "canvas"],
|
|
1578
|
-
shadows: ["shadow", "shadows", "elevation"],
|
|
1579
|
-
radius: ["radius", "radii", "corner", "corners", "rounded", "rounding"],
|
|
1580
|
-
borders: ["border", "borders", "stroke", "outline"],
|
|
1581
|
-
text: ["text", "copy", "content"],
|
|
1582
|
-
focus: ["focus", "ring", "focus-ring"],
|
|
1583
|
-
layout: ["layout", "container", "grid", "breakpoint"],
|
|
1584
|
-
code: ["code"],
|
|
1585
|
-
"component-sizing": ["component-sizing", "sizing", "size", "sizes"]
|
|
1586
|
-
};
|
|
1587
|
-
function normalizeCategoryValue(value) {
|
|
1588
|
-
const normalized = value?.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
|
|
1589
|
-
return normalized && normalized.length > 0 ? normalized : void 0;
|
|
1590
|
-
}
|
|
1591
|
-
function resolveCategoryKeys(categories, requestedCategory) {
|
|
1592
|
-
const normalized = normalizeCategoryValue(requestedCategory);
|
|
1593
|
-
if (!normalized) {
|
|
1594
|
-
return Object.keys(categories);
|
|
1595
|
-
}
|
|
1596
|
-
const keys = Object.keys(categories);
|
|
1597
|
-
const exactMatches = keys.filter((key) => normalizeCategoryValue(key) === normalized);
|
|
1598
|
-
if (exactMatches.length > 0) {
|
|
1599
|
-
return exactMatches;
|
|
1600
|
-
}
|
|
1601
|
-
const canonical = Object.entries(TOKEN_CATEGORY_ALIASES).find(
|
|
1602
|
-
([categoryName, aliases]) => categoryName === normalized || aliases.includes(normalized)
|
|
1603
|
-
);
|
|
1604
|
-
if (canonical) {
|
|
1605
|
-
const aliases = [canonical[0], ...canonical[1]];
|
|
1606
|
-
const aliasMatches = keys.filter((key) => {
|
|
1607
|
-
const normalizedKey = normalizeCategoryValue(key) ?? "";
|
|
1608
|
-
return aliases.some((alias) => normalizedKey.includes(alias));
|
|
1609
|
-
});
|
|
1610
|
-
if (aliasMatches.length > 0) {
|
|
1611
|
-
return aliasMatches;
|
|
1612
|
-
}
|
|
1613
|
-
}
|
|
1614
|
-
return keys.filter((key) => (normalizeCategoryValue(key) ?? "").includes(normalized));
|
|
1615
|
-
}
|
|
1616
|
-
function canonicalizeCategory(category, tokens) {
|
|
1617
|
-
const normalizedCategory = normalizeCategoryValue(category);
|
|
1618
|
-
const candidates = [
|
|
1619
|
-
normalizedCategory,
|
|
1620
|
-
...tokens.flatMap((token) => [
|
|
1621
|
-
normalizeCategoryValue(token.category),
|
|
1622
|
-
normalizeCategoryValue(token.path?.[0]),
|
|
1623
|
-
normalizeCategoryValue(token.name.split(/[.:/-]/)[0])
|
|
1624
|
-
])
|
|
1625
|
-
].filter(Boolean);
|
|
1626
|
-
for (const candidate of candidates) {
|
|
1627
|
-
for (const [canonical, aliases] of Object.entries(TOKEN_CATEGORY_ALIASES)) {
|
|
1628
|
-
if (candidate === canonical || aliases.some(
|
|
1629
|
-
(alias) => candidate === alias || candidate.includes(alias) || alias.includes(candidate)
|
|
1630
|
-
)) {
|
|
1631
|
-
return canonical;
|
|
1632
|
-
}
|
|
1633
|
-
}
|
|
1634
|
-
}
|
|
1635
|
-
return normalizedCategory || "other";
|
|
1636
|
-
}
|
|
1637
|
-
function normalizeTokenCategories(categories) {
|
|
1638
|
-
const normalized = {};
|
|
1639
|
-
for (const [category, tokens] of Object.entries(categories)) {
|
|
1640
|
-
const canonical = canonicalizeCategory(category, tokens);
|
|
1641
|
-
if (!normalized[canonical]) normalized[canonical] = [];
|
|
1642
|
-
normalized[canonical].push(
|
|
1643
|
-
...tokens.map((token) => ({
|
|
1644
|
-
...token,
|
|
1645
|
-
category: canonical
|
|
1646
|
-
}))
|
|
1647
|
-
);
|
|
1648
|
-
}
|
|
1649
|
-
for (const tokens of Object.values(normalized)) {
|
|
1650
|
-
tokens.sort((a, b) => a.name.localeCompare(b.name));
|
|
1651
|
-
}
|
|
1652
|
-
return normalized;
|
|
1653
|
-
}
|
|
1654
|
-
var tokensHandler = async (args, ctx) => {
|
|
1655
|
-
const category = args?.category;
|
|
1656
|
-
const search2 = args?.search?.toLowerCase() ?? void 0;
|
|
1657
|
-
const tokensLimit = typeof args?.limit === "number" ? Math.min(Math.max(args.limit, 1), 100) : search2 ? 25 : void 0;
|
|
1658
|
-
const tokenData = ctx.data.tokens;
|
|
1659
|
-
if (!tokenData || tokenData.total === 0) {
|
|
1660
|
-
return {
|
|
1661
|
-
content: [{
|
|
1662
|
-
type: "text",
|
|
1663
|
-
text: JSON.stringify({
|
|
1664
|
-
total: 0,
|
|
1665
|
-
categories: {},
|
|
1666
|
-
hint: `No design tokens found. Add a tokens.include pattern to your ${BRAND.configFile} and run \`${BRAND.cliCommand} build\`.`
|
|
1667
|
-
})
|
|
1668
|
-
}]
|
|
1669
|
-
};
|
|
1670
|
-
}
|
|
1671
|
-
const normalizedCategories = normalizeTokenCategories(tokenData.categories);
|
|
1672
|
-
let filteredCategories = {};
|
|
1673
|
-
let filteredTotal = 0;
|
|
1674
|
-
const resolvedCategoryKeys = resolveCategoryKeys(normalizedCategories, category);
|
|
1675
|
-
const friendlyCategories = Object.keys(TOKEN_CATEGORY_ALIASES);
|
|
1676
|
-
const searchMatchesCategory = search2 ? resolveCategoryKeys(normalizedCategories, search2) : [];
|
|
1677
|
-
for (const [cat, tokens] of Object.entries(normalizedCategories)) {
|
|
1678
|
-
if (category && !resolvedCategoryKeys.includes(cat)) continue;
|
|
1679
|
-
let filtered = tokens;
|
|
1680
|
-
if (search2) {
|
|
1681
|
-
if (searchMatchesCategory.includes(cat)) {
|
|
1682
|
-
filtered = tokens;
|
|
1683
|
-
} else {
|
|
1684
|
-
filtered = tokens.filter(
|
|
1685
|
-
(t) => t.name.toLowerCase().includes(search2) || t.description && t.description.toLowerCase().includes(search2) || (normalizeCategoryValue(cat) ?? "").includes(search2) || t.value && t.value.toLowerCase().includes(search2) || t.path && t.path.join(" ").toLowerCase().includes(search2)
|
|
1686
|
-
);
|
|
1687
|
-
}
|
|
1688
|
-
}
|
|
1689
|
-
if (filtered.length > 0) {
|
|
1690
|
-
filteredCategories[cat] = filtered;
|
|
1691
|
-
filteredTotal += filtered.length;
|
|
1692
|
-
}
|
|
1693
|
-
}
|
|
1694
|
-
if (tokensLimit !== void 0) {
|
|
1695
|
-
const limited = limitTokensPerCategory(filteredCategories, tokensLimit);
|
|
1696
|
-
filteredCategories = limited.categories;
|
|
1697
|
-
filteredTotal = limited.total;
|
|
1698
|
-
}
|
|
1699
|
-
let hint;
|
|
1700
|
-
if (filteredTotal === 0) {
|
|
1701
|
-
if (category && search2) {
|
|
1702
|
-
const categoryTotal = resolvedCategoryKeys.reduce(
|
|
1703
|
-
(sum, key) => sum + (normalizedCategories[key]?.length ?? 0),
|
|
1704
|
-
0
|
|
1705
|
-
);
|
|
1706
|
-
hint = categoryTotal > 0 ? `No tokens matching "${search2}" in category "${category}" (${categoryTotal} tokens in this category). Try a broader search or remove the search term.` : `Category "${category}" not found. Try categories like: ${friendlyCategories.join(", ")}.`;
|
|
1707
|
-
} else if (search2) {
|
|
1708
|
-
hint = `No tokens matching "${search2}". Try categories like: ${friendlyCategories.join(", ")}.`;
|
|
1709
|
-
} else if (category) {
|
|
1710
|
-
hint = `Category "${category}" not found. Try categories like: ${friendlyCategories.join(", ")}.`;
|
|
1711
|
-
}
|
|
1712
|
-
} else if (!category && !search2) {
|
|
1713
|
-
hint = `Use var(--token-name) in your CSS/styles. Filter by category or search to narrow results.`;
|
|
1714
|
-
}
|
|
1715
|
-
return {
|
|
1716
|
-
content: [{
|
|
1717
|
-
type: "text",
|
|
1718
|
-
text: JSON.stringify({
|
|
1719
|
-
prefix: tokenData.prefix,
|
|
1720
|
-
total: filteredTotal,
|
|
1721
|
-
totalAvailable: tokenData.total,
|
|
1722
|
-
categories: filteredCategories,
|
|
1723
|
-
...hint && { hint },
|
|
1724
|
-
...!category && !search2 && {
|
|
1725
|
-
availableCategories: Object.entries(normalizedCategories).map(([cat, tokens]) => ({
|
|
1726
|
-
category: cat,
|
|
1727
|
-
count: tokens.length
|
|
1728
|
-
}))
|
|
1729
|
-
}
|
|
1730
|
-
})
|
|
1731
|
-
}]
|
|
1732
|
-
};
|
|
1733
|
-
};
|
|
1734
|
-
|
|
1735
|
-
// src/service.ts
|
|
1736
|
-
var DEFAULT_ENDPOINTS = {
|
|
1737
|
-
render: "/fragments/render",
|
|
1738
|
-
compare: "/fragments/compare",
|
|
1739
|
-
fix: "/fragments/fix",
|
|
1740
|
-
a11y: "/fragments/a11y"
|
|
1741
|
-
};
|
|
1742
|
-
async function renderComponent(viewerUrl, request, endpoints) {
|
|
1743
|
-
const renderUrl = `${viewerUrl}${endpoints?.render ?? DEFAULT_ENDPOINTS.render}`;
|
|
1744
|
-
const response = await fetch(renderUrl, {
|
|
1745
|
-
method: "POST",
|
|
1746
|
-
headers: { "Content-Type": "application/json" },
|
|
1747
|
-
body: JSON.stringify({
|
|
1748
|
-
component: request.component,
|
|
1749
|
-
props: request.props ?? {},
|
|
1750
|
-
variant: request.variant,
|
|
1751
|
-
viewport: request.viewport ?? { width: 800, height: 600 }
|
|
1752
|
-
})
|
|
1753
|
-
});
|
|
1754
|
-
return await response.json();
|
|
1755
|
-
}
|
|
1756
|
-
async function compareComponent(viewerUrl, request, endpoints) {
|
|
1757
|
-
const compareUrl = `${viewerUrl}${endpoints?.compare ?? DEFAULT_ENDPOINTS.compare}`;
|
|
1758
|
-
const response = await fetch(compareUrl, {
|
|
1759
|
-
method: "POST",
|
|
1760
|
-
headers: { "Content-Type": "application/json" },
|
|
1761
|
-
body: JSON.stringify(request)
|
|
1762
|
-
});
|
|
1763
|
-
return await response.json();
|
|
1764
|
-
}
|
|
1765
|
-
async function fixComponent(viewerUrl, request, endpoints) {
|
|
1766
|
-
const fixUrl = `${viewerUrl}${endpoints?.fix ?? DEFAULT_ENDPOINTS.fix}`;
|
|
1767
|
-
const response = await fetch(fixUrl, {
|
|
1768
|
-
method: "POST",
|
|
1769
|
-
headers: { "Content-Type": "application/json" },
|
|
1770
|
-
body: JSON.stringify(request)
|
|
1771
|
-
});
|
|
1772
|
-
return await response.json();
|
|
1773
|
-
}
|
|
1774
|
-
async function auditComponent(viewerUrl, request, endpoints) {
|
|
1775
|
-
const a11yUrl = `${viewerUrl}${endpoints?.a11y ?? DEFAULT_ENDPOINTS.a11y}`;
|
|
1776
|
-
const response = await fetch(a11yUrl, {
|
|
1777
|
-
method: "POST",
|
|
1778
|
-
headers: { "Content-Type": "application/json" },
|
|
1779
|
-
body: JSON.stringify({
|
|
1780
|
-
component: request.component,
|
|
1781
|
-
variant: request.variant,
|
|
1782
|
-
standard: request.standard,
|
|
1783
|
-
includeFixPatches: request.includeFixPatches
|
|
1784
|
-
})
|
|
1785
|
-
});
|
|
1786
|
-
const raw = await response.json();
|
|
1787
|
-
if (raw.error) {
|
|
1788
|
-
return {
|
|
1789
|
-
component: request.component,
|
|
1790
|
-
results: [],
|
|
1791
|
-
score: 0,
|
|
1792
|
-
aaPercent: 0,
|
|
1793
|
-
aaaPercent: 0,
|
|
1794
|
-
passed: false,
|
|
1795
|
-
standard: request.standard ?? "AA",
|
|
1796
|
-
error: raw.error
|
|
1797
|
-
};
|
|
1798
|
-
}
|
|
1799
|
-
const results = raw.results ?? [];
|
|
1800
|
-
const standard = request.standard ?? "AA";
|
|
1801
|
-
let totalCritical = 0;
|
|
1802
|
-
let totalSerious = 0;
|
|
1803
|
-
let totalModerate = 0;
|
|
1804
|
-
let totalMinor = 0;
|
|
1805
|
-
for (const r of results) {
|
|
1806
|
-
totalCritical += r.summary.critical;
|
|
1807
|
-
totalSerious += r.summary.serious;
|
|
1808
|
-
totalModerate += r.summary.moderate;
|
|
1809
|
-
totalMinor += r.summary.minor;
|
|
1810
|
-
}
|
|
1811
|
-
const deductions = totalCritical * 10 + totalSerious * 5 + totalModerate * 2 + totalMinor * 1;
|
|
1812
|
-
const score = Math.max(0, 100 - deductions);
|
|
1813
|
-
const variantCount = results.length;
|
|
1814
|
-
const aaPassCount = results.filter((r) => {
|
|
1815
|
-
const critical = r.summary.critical;
|
|
1816
|
-
const serious = r.summary.serious;
|
|
1817
|
-
return critical === 0 && serious === 0;
|
|
1818
|
-
}).length;
|
|
1819
|
-
const aaaPassCount = results.filter((r) => {
|
|
1820
|
-
const total = r.summary.critical + r.summary.serious + r.summary.moderate + r.summary.minor;
|
|
1821
|
-
return total === 0;
|
|
1822
|
-
}).length;
|
|
1823
|
-
const totalPasses = results.reduce((sum, r) => sum + r.passes, 0);
|
|
1824
|
-
const totalViolations = totalCritical + totalSerious + totalModerate + totalMinor;
|
|
1825
|
-
const emptyAudit = results.length > 0 && totalPasses === 0 && totalViolations === 0;
|
|
1826
|
-
const aaPercent = variantCount > 0 ? Math.round(aaPassCount / variantCount * 100) : 100;
|
|
1827
|
-
const aaaPercent = variantCount > 0 ? Math.round(aaaPassCount / variantCount * 100) : 100;
|
|
1828
|
-
const aaPass = !emptyAudit && totalCritical === 0 && totalSerious === 0;
|
|
1829
|
-
const aaaPass = !emptyAudit && totalViolations === 0;
|
|
1830
|
-
const passed = standard === "AAA" ? aaaPass : aaPass;
|
|
1831
|
-
return {
|
|
1832
|
-
component: request.component,
|
|
1833
|
-
results,
|
|
1834
|
-
score: emptyAudit ? 0 : score,
|
|
1835
|
-
aaPercent: emptyAudit ? 0 : aaPercent,
|
|
1836
|
-
aaaPercent: emptyAudit ? 0 : aaaPercent,
|
|
1837
|
-
...emptyAudit && { emptyAudit },
|
|
1838
|
-
passed,
|
|
1839
|
-
standard
|
|
1840
|
-
};
|
|
1841
|
-
}
|
|
1842
|
-
|
|
1843
|
-
// src/tools/render.ts
|
|
1844
|
-
var NO_VIEWER_MSG = "This tool requires a running dev server. Add --viewer-url http://localhost:PORT to this server's config args. Start the viewer with 'fragments dev' in your component library directory.";
|
|
1845
|
-
var renderHandler = async (args, ctx) => {
|
|
1846
|
-
const componentName = args?.component;
|
|
1847
|
-
const variantName = args?.variant;
|
|
1848
|
-
const props = args?.props ?? {};
|
|
1849
|
-
const viewport = args?.viewport;
|
|
1850
|
-
const figmaUrl = args?.figmaUrl;
|
|
1851
|
-
const threshold = args?.threshold ?? (figmaUrl ? 1 : ctx.config.threshold ?? DEFAULTS.diffThreshold);
|
|
1852
|
-
if (!componentName) {
|
|
1853
|
-
return {
|
|
1854
|
-
content: [{ type: "text", text: "Error: component name is required" }],
|
|
1855
|
-
isError: true
|
|
1856
|
-
};
|
|
1857
|
-
}
|
|
1858
|
-
{
|
|
1859
|
-
const component = findComponentByName(ctx.data.snapshot, componentName);
|
|
1860
|
-
if (!component) {
|
|
1861
|
-
const allNames = componentNames(ctx.data.snapshot);
|
|
1862
|
-
const closest = findClosestMatch(componentName, allNames);
|
|
1863
|
-
const suggestion = closest ? ` Did you mean "${closest}"?` : "";
|
|
1864
|
-
throw new Error(`Component "${componentName}" not found.${suggestion} Use ${ctx.toolNames.discover} to see available components.`);
|
|
24
|
+
try {
|
|
25
|
+
const content = readFileSync(configPath, "utf-8");
|
|
26
|
+
return JSON.parse(content);
|
|
27
|
+
} catch (e) {
|
|
28
|
+
throw new Error(`Failed to parse ${configPath}: ${e instanceof Error ? e.message : String(e)}`);
|
|
1865
29
|
}
|
|
1866
30
|
}
|
|
1867
|
-
const
|
|
1868
|
-
if (
|
|
1869
|
-
return {
|
|
1870
|
-
content: [{
|
|
1871
|
-
type: "text",
|
|
1872
|
-
text: NO_VIEWER_MSG
|
|
1873
|
-
}],
|
|
1874
|
-
isError: true
|
|
1875
|
-
};
|
|
1876
|
-
}
|
|
1877
|
-
if (figmaUrl) {
|
|
31
|
+
const pkgPath = join(projectRoot, "package.json");
|
|
32
|
+
if (existsSync(pkgPath)) {
|
|
1878
33
|
try {
|
|
1879
|
-
const
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
figmaUrl,
|
|
1884
|
-
threshold
|
|
1885
|
-
}, ctx.config.fileConfig?.endpoints);
|
|
1886
|
-
if (result2.error) {
|
|
1887
|
-
return {
|
|
1888
|
-
content: [{
|
|
1889
|
-
type: "text",
|
|
1890
|
-
text: `Compare error: ${result2.error}${result2.suggestion ? `
|
|
1891
|
-
Suggestion: ${result2.suggestion}` : ""}`
|
|
1892
|
-
}],
|
|
1893
|
-
isError: true
|
|
1894
|
-
};
|
|
1895
|
-
}
|
|
1896
|
-
const content = [];
|
|
1897
|
-
const summaryText = result2.match ? `MATCH: ${componentName} matches Figma design (${result2.diffPercentage}% diff, threshold: ${result2.threshold}%)` : `MISMATCH: ${componentName} differs from Figma design by ${result2.diffPercentage}% (threshold: ${result2.threshold}%)`;
|
|
1898
|
-
content.push({ type: "text", text: summaryText });
|
|
1899
|
-
if (result2.diff && !result2.match) {
|
|
1900
|
-
content.push({
|
|
1901
|
-
type: "image",
|
|
1902
|
-
data: result2.diff.replace("data:image/png;base64,", ""),
|
|
1903
|
-
mimeType: "image/png"
|
|
1904
|
-
});
|
|
1905
|
-
content.push({
|
|
1906
|
-
type: "text",
|
|
1907
|
-
text: `Diff image above shows visual differences (red highlights). Changed regions: ${result2.changedRegions?.length ?? 0}`
|
|
1908
|
-
});
|
|
1909
|
-
}
|
|
1910
|
-
content.push({
|
|
1911
|
-
type: "text",
|
|
1912
|
-
text: JSON.stringify({
|
|
1913
|
-
match: result2.match,
|
|
1914
|
-
diffPercentage: result2.diffPercentage,
|
|
1915
|
-
threshold: result2.threshold,
|
|
1916
|
-
figmaUrl: result2.figmaUrl,
|
|
1917
|
-
changedRegions: result2.changedRegions
|
|
1918
|
-
})
|
|
1919
|
-
});
|
|
1920
|
-
return { content };
|
|
1921
|
-
} catch (error) {
|
|
1922
|
-
return {
|
|
1923
|
-
content: [{
|
|
1924
|
-
type: "text",
|
|
1925
|
-
text: `Failed to compare component: ${error instanceof Error ? error.message : "Unknown error"}. Make sure the Fragments dev server is running and FIGMA_ACCESS_TOKEN is set.`
|
|
1926
|
-
}],
|
|
1927
|
-
isError: true
|
|
1928
|
-
};
|
|
34
|
+
const content = readFileSync(pkgPath, "utf-8");
|
|
35
|
+
const pkg = JSON.parse(content);
|
|
36
|
+
if (pkg.dsMcp) return pkg.dsMcp;
|
|
37
|
+
} catch {
|
|
1929
38
|
}
|
|
1930
39
|
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// src/server.ts
|
|
44
|
+
import { buildMcpTools, buildToolNames, MCP_TOOL_DEFINITIONS } from "@fragments-sdk/context/mcp-tools";
|
|
45
|
+
|
|
46
|
+
// src/version.ts
|
|
47
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
48
|
+
function readPackageVersion() {
|
|
1931
49
|
try {
|
|
1932
|
-
const
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
}, ctx.config.fileConfig?.endpoints);
|
|
1938
|
-
if (result2.error) {
|
|
1939
|
-
return {
|
|
1940
|
-
content: [{ type: "text", text: `Render error: ${result2.error}` }],
|
|
1941
|
-
isError: true
|
|
1942
|
-
};
|
|
1943
|
-
}
|
|
1944
|
-
return {
|
|
1945
|
-
content: [
|
|
1946
|
-
{
|
|
1947
|
-
type: "image",
|
|
1948
|
-
data: result2.screenshot.replace("data:image/png;base64,", ""),
|
|
1949
|
-
mimeType: "image/png"
|
|
1950
|
-
},
|
|
1951
|
-
{
|
|
1952
|
-
type: "text",
|
|
1953
|
-
text: `Successfully rendered ${componentName}${variantName ? ` (variant: "${variantName}")` : ""}${Object.keys(props).length > 0 ? ` with props: ${JSON.stringify(props)}` : ""}`
|
|
1954
|
-
}
|
|
1955
|
-
]
|
|
1956
|
-
};
|
|
1957
|
-
} catch (error) {
|
|
1958
|
-
return {
|
|
1959
|
-
content: [{
|
|
1960
|
-
type: "text",
|
|
1961
|
-
text: `Failed to render component: ${error instanceof Error ? error.message : "Unknown error"}. Make sure the Fragments dev server is running.`
|
|
1962
|
-
}],
|
|
1963
|
-
isError: true
|
|
1964
|
-
};
|
|
50
|
+
const raw = readFileSync2(new URL("../package.json", import.meta.url), "utf-8");
|
|
51
|
+
const pkg = JSON.parse(raw);
|
|
52
|
+
return pkg.version ?? "0.0.0";
|
|
53
|
+
} catch {
|
|
54
|
+
return "0.0.0";
|
|
1965
55
|
}
|
|
1966
|
-
}
|
|
56
|
+
}
|
|
57
|
+
var MCP_SERVER_VERSION = readPackageVersion();
|
|
1967
58
|
|
|
1968
|
-
// src/
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
const
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
text: NO_VIEWER_MSG2
|
|
1990
|
-
}],
|
|
1991
|
-
isError: true
|
|
1992
|
-
};
|
|
59
|
+
// src/catalog-meta.ts
|
|
60
|
+
function getCatalogMeta(data) {
|
|
61
|
+
const rawUpdatedAt = data.validateFixContext?.updatedAt ?? data.snapshot.metadata.updatedAt;
|
|
62
|
+
const updatedAt = typeof rawUpdatedAt === "number" ? new Date(rawUpdatedAt).toISOString() : rawUpdatedAt;
|
|
63
|
+
return {
|
|
64
|
+
catalogRevision: data.validateFixContext?.catalogRevision ?? data.snapshot.metadata.revision,
|
|
65
|
+
updatedAt
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/token-suggestions.ts
|
|
70
|
+
function propertyFamilyFor(property, value) {
|
|
71
|
+
const prop = property.toLowerCase().trim();
|
|
72
|
+
const normalizedValue = value?.toLowerCase().trim();
|
|
73
|
+
if (prop.includes("shadow")) return "shadow";
|
|
74
|
+
if (prop.includes("z-index")) return "z-index";
|
|
75
|
+
if (prop.includes("transition") || prop.includes("duration") || prop.includes("animation")) {
|
|
76
|
+
return "duration";
|
|
77
|
+
}
|
|
78
|
+
if (prop.includes("font") || prop.includes("line-height") || prop === "letter-spacing") {
|
|
79
|
+
return "typography";
|
|
1993
80
|
}
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
variant: variantName,
|
|
1998
|
-
fixType
|
|
1999
|
-
}, ctx.config.fileConfig?.endpoints);
|
|
2000
|
-
if (result2.error) {
|
|
2001
|
-
return {
|
|
2002
|
-
content: [{
|
|
2003
|
-
type: "text",
|
|
2004
|
-
text: `Fix generation error: ${result2.error}`
|
|
2005
|
-
}],
|
|
2006
|
-
isError: true
|
|
2007
|
-
};
|
|
2008
|
-
}
|
|
2009
|
-
return {
|
|
2010
|
-
content: [{
|
|
2011
|
-
type: "text",
|
|
2012
|
-
text: JSON.stringify({
|
|
2013
|
-
component: componentName,
|
|
2014
|
-
variant: variantName ?? "all",
|
|
2015
|
-
fixType,
|
|
2016
|
-
patches: result2.patches,
|
|
2017
|
-
summary: result2.summary,
|
|
2018
|
-
patchCount: result2.patches.length,
|
|
2019
|
-
nextStep: result2.patches.length > 0 ? `Apply patches using your editor or \`patch\` command, then run ${ctx.toolNames.render} to confirm fixes.` : void 0
|
|
2020
|
-
})
|
|
2021
|
-
}]
|
|
2022
|
-
};
|
|
2023
|
-
} catch (error) {
|
|
2024
|
-
return {
|
|
2025
|
-
content: [{
|
|
2026
|
-
type: "text",
|
|
2027
|
-
text: `Failed to generate fixes: ${error instanceof Error ? error.message : "Unknown error"}. Make sure the Fragments dev server is running.`
|
|
2028
|
-
}],
|
|
2029
|
-
isError: true
|
|
2030
|
-
};
|
|
81
|
+
if (prop.includes("radius")) return "radius";
|
|
82
|
+
if (prop.endsWith("border-width") || prop === "border-width" || prop === "outline-width" || prop === "stroke-width") {
|
|
83
|
+
return "border-width";
|
|
2031
84
|
}
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
// src/tools/a11y.ts
|
|
2035
|
-
var NO_VIEWER_MSG3 = "This tool requires a running dev server. Add --viewer-url http://localhost:PORT to this server's config args. Start the viewer with 'fragments dev' in your component library directory.";
|
|
2036
|
-
var a11yHandler = async (args, ctx) => {
|
|
2037
|
-
const componentName = args?.component;
|
|
2038
|
-
const variantName = args?.variant ?? void 0;
|
|
2039
|
-
const standard = args?.standard ?? "AA";
|
|
2040
|
-
const includeFixPatches = args?.includeFixPatches ?? false;
|
|
2041
|
-
if (!componentName) {
|
|
2042
|
-
throw new Error("component is required");
|
|
2043
|
-
}
|
|
2044
|
-
{
|
|
2045
|
-
const fragment = findComponentByName(ctx.data.snapshot, componentName);
|
|
2046
|
-
if (!fragment) {
|
|
2047
|
-
const allNames = componentNames(ctx.data.snapshot);
|
|
2048
|
-
const closest = findClosestMatch(componentName, allNames);
|
|
2049
|
-
const suggestion = closest ? ` Did you mean "${closest}"?` : "";
|
|
2050
|
-
throw new Error(`Component "${componentName}" not found.${suggestion} Use ${ctx.toolNames.discover} to see available components.`);
|
|
2051
|
-
}
|
|
85
|
+
if (prop.includes("color") || prop === "background" || prop.startsWith("background-") || prop === "fill" || prop === "stroke" || prop === "caret-color" || prop === "accent-color" || (prop === "border" || prop === "outline") && normalizedValue && looksLikeColor(normalizedValue)) {
|
|
86
|
+
return "color";
|
|
2052
87
|
}
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
return {
|
|
2056
|
-
content: [{
|
|
2057
|
-
type: "text",
|
|
2058
|
-
text: NO_VIEWER_MSG3
|
|
2059
|
-
}],
|
|
2060
|
-
isError: true
|
|
2061
|
-
};
|
|
88
|
+
if (prop === "margin" || prop.startsWith("margin-") || prop === "padding" || prop.startsWith("padding-") || prop === "gap" || prop === "row-gap" || prop === "column-gap" || prop === "inset" || prop === "top" || prop === "right" || prop === "bottom" || prop === "left" || prop.endsWith("width") || prop.endsWith("height")) {
|
|
89
|
+
return "spacing";
|
|
2062
90
|
}
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
let nextStep;
|
|
2080
|
-
if (result2.emptyAudit) {
|
|
2081
|
-
nextStep = `No testable elements found for ${variantName ? `variant "${variantName}"` : componentName}. The variant may not exist or renders no accessible content. Check available variants with ${ctx.toolNames.inspect}("${componentName}").`;
|
|
2082
|
-
} else if (result2.passed) {
|
|
2083
|
-
nextStep = 'All accessibility checks passed. Consider running with standard: "AAA" for enhanced compliance.';
|
|
2084
|
-
} else {
|
|
2085
|
-
nextStep = `Fix the violations above, then re-run ${ctx.toolNames.a11y} to verify. Use ${ctx.toolNames.fix} for automated fixes.`;
|
|
2086
|
-
}
|
|
91
|
+
return "other";
|
|
92
|
+
}
|
|
93
|
+
function suggestToken(input) {
|
|
94
|
+
const family = propertyFamilyFor(input.property, input.value);
|
|
95
|
+
const limit = Math.min(Math.max(input.limit ?? 5, 1), 10);
|
|
96
|
+
const candidates = input.tokens ? scoreCandidates(input.tokens, family, input) : [];
|
|
97
|
+
const top = candidates.slice(0, limit).map(
|
|
98
|
+
(candidate) => presentCandidate(candidate, input.tokens)
|
|
99
|
+
);
|
|
100
|
+
const meta = {
|
|
101
|
+
propertyFamily: family,
|
|
102
|
+
catalogRevision: input.catalogRevision,
|
|
103
|
+
updatedAt: input.updatedAt,
|
|
104
|
+
candidateCount: candidates.length
|
|
105
|
+
};
|
|
106
|
+
if (family === "other") {
|
|
2087
107
|
return {
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
variant: variantName ?? "all",
|
|
2093
|
-
standard,
|
|
2094
|
-
totalViolations: result2.results.reduce((sum, r) => sum + r.summary.total, 0),
|
|
2095
|
-
variantsPassingAA: `${result2.aaPercent}%`,
|
|
2096
|
-
variantsPassingAAA: `${result2.aaaPercent}%`,
|
|
2097
|
-
passed: result2.passed,
|
|
2098
|
-
...result2.emptyAudit && { emptyAudit: true },
|
|
2099
|
-
results: result2.results,
|
|
2100
|
-
nextStep
|
|
2101
|
-
})
|
|
2102
|
-
}]
|
|
108
|
+
alternatives: [],
|
|
109
|
+
noSuggestion: true,
|
|
110
|
+
noSuggestionReason: `No token family is known for CSS property "${input.property}".`,
|
|
111
|
+
_meta: meta
|
|
2103
112
|
};
|
|
2104
|
-
}
|
|
113
|
+
}
|
|
114
|
+
if (!input.tokens || input.tokens.total === 0) {
|
|
2105
115
|
return {
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
isError: true
|
|
116
|
+
alternatives: [],
|
|
117
|
+
noSuggestion: true,
|
|
118
|
+
noSuggestionReason: "No design tokens are available in the active catalog.",
|
|
119
|
+
_meta: meta
|
|
2111
120
|
};
|
|
2112
121
|
}
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
// src/graph-handler.ts
|
|
2116
|
-
import {
|
|
2117
|
-
ComponentGraphEngine,
|
|
2118
|
-
deserializeGraph
|
|
2119
|
-
} from "@fragments-sdk/context/graph";
|
|
2120
|
-
function handleGraphTool(args, serializedGraph, blocks, componentNames2) {
|
|
2121
|
-
if (!serializedGraph) {
|
|
122
|
+
if (top.length === 0) {
|
|
2122
123
|
return {
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
isError: true
|
|
124
|
+
alternatives: [],
|
|
125
|
+
noSuggestion: true,
|
|
126
|
+
noSuggestionReason: `No ${family} tokens are available for CSS property "${input.property}".`,
|
|
127
|
+
_meta: meta
|
|
2128
128
|
};
|
|
2129
129
|
}
|
|
2130
|
-
const
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
}
|
|
2155
|
-
if (!engine.hasNode(args.component)) {
|
|
2156
|
-
const closest = componentNames2 ? findClosestMatch(args.component, componentNames2) : null;
|
|
2157
|
-
const suggestion = closest ? ` Did you mean "${closest}"?` : "";
|
|
2158
|
-
return { text: JSON.stringify({ error: `Component "${args.component}" not found in graph.${suggestion}` }), isError: true };
|
|
2159
|
-
}
|
|
2160
|
-
const deps = engine.dependencies(args.component, edgeTypes);
|
|
2161
|
-
return {
|
|
2162
|
-
text: JSON.stringify({
|
|
2163
|
-
mode: "dependencies",
|
|
2164
|
-
component: args.component,
|
|
2165
|
-
count: deps.length,
|
|
2166
|
-
dependencies: deps.map((e) => ({
|
|
2167
|
-
component: e.target,
|
|
2168
|
-
type: e.type,
|
|
2169
|
-
weight: e.weight,
|
|
2170
|
-
note: e.note,
|
|
2171
|
-
provenance: e.provenance
|
|
2172
|
-
}))
|
|
2173
|
-
})
|
|
2174
|
-
};
|
|
2175
|
-
}
|
|
2176
|
-
case "dependents": {
|
|
2177
|
-
if (!args.component) {
|
|
2178
|
-
return { text: JSON.stringify({ error: "component is required for dependents mode" }), isError: true };
|
|
2179
|
-
}
|
|
2180
|
-
if (!engine.hasNode(args.component)) {
|
|
2181
|
-
const closest = componentNames2 ? findClosestMatch(args.component, componentNames2) : null;
|
|
2182
|
-
const suggestion = closest ? ` Did you mean "${closest}"?` : "";
|
|
2183
|
-
return { text: JSON.stringify({ error: `Component "${args.component}" not found in graph.${suggestion}` }), isError: true };
|
|
2184
|
-
}
|
|
2185
|
-
const deps = engine.dependents(args.component, edgeTypes);
|
|
2186
|
-
return {
|
|
2187
|
-
text: JSON.stringify({
|
|
2188
|
-
mode: "dependents",
|
|
2189
|
-
component: args.component,
|
|
2190
|
-
count: deps.length,
|
|
2191
|
-
dependents: deps.map((e) => ({
|
|
2192
|
-
component: e.source,
|
|
2193
|
-
type: e.type,
|
|
2194
|
-
weight: e.weight,
|
|
2195
|
-
note: e.note,
|
|
2196
|
-
provenance: e.provenance
|
|
2197
|
-
}))
|
|
2198
|
-
})
|
|
2199
|
-
};
|
|
2200
|
-
}
|
|
2201
|
-
case "impact": {
|
|
2202
|
-
if (!args.component) {
|
|
2203
|
-
return { text: JSON.stringify({ error: "component is required for impact mode" }), isError: true };
|
|
2204
|
-
}
|
|
2205
|
-
if (!engine.hasNode(args.component)) {
|
|
2206
|
-
const closest = componentNames2 ? findClosestMatch(args.component, componentNames2) : null;
|
|
2207
|
-
const suggestion = closest ? ` Did you mean "${closest}"?` : "";
|
|
2208
|
-
return { text: JSON.stringify({ error: `Component "${args.component}" not found in graph.${suggestion}` }), isError: true };
|
|
2209
|
-
}
|
|
2210
|
-
const result2 = engine.impact(args.component, args.maxDepth ?? 3);
|
|
2211
|
-
return {
|
|
2212
|
-
text: JSON.stringify({
|
|
2213
|
-
mode: "impact",
|
|
2214
|
-
...result2,
|
|
2215
|
-
summary: `Changing ${args.component} affects ${result2.totalAffected} component(s) and ${result2.affectedBlocks.length} block(s)`
|
|
2216
|
-
})
|
|
2217
|
-
};
|
|
2218
|
-
}
|
|
2219
|
-
case "path": {
|
|
2220
|
-
if (!args.component || !args.target) {
|
|
2221
|
-
return { text: JSON.stringify({ error: "component and target are required for path mode" }), isError: true };
|
|
2222
|
-
}
|
|
2223
|
-
const result2 = engine.path(args.component, args.target);
|
|
2224
|
-
return {
|
|
2225
|
-
text: JSON.stringify({
|
|
2226
|
-
mode: "path",
|
|
2227
|
-
from: args.component,
|
|
2228
|
-
to: args.target,
|
|
2229
|
-
...result2,
|
|
2230
|
-
edges: result2.edges.map((e) => ({
|
|
2231
|
-
source: e.source,
|
|
2232
|
-
target: e.target,
|
|
2233
|
-
type: e.type
|
|
2234
|
-
}))
|
|
2235
|
-
})
|
|
2236
|
-
};
|
|
2237
|
-
}
|
|
2238
|
-
case "composition": {
|
|
2239
|
-
if (!args.component) {
|
|
2240
|
-
return { text: JSON.stringify({ error: "component is required for composition mode" }), isError: true };
|
|
2241
|
-
}
|
|
2242
|
-
if (!engine.hasNode(args.component)) {
|
|
2243
|
-
const closest = componentNames2 ? findClosestMatch(args.component, componentNames2) : null;
|
|
2244
|
-
const suggestion = closest ? ` Did you mean "${closest}"?` : "";
|
|
2245
|
-
return { text: JSON.stringify({ error: `Component "${args.component}" not found in graph.${suggestion}` }), isError: true };
|
|
2246
|
-
}
|
|
2247
|
-
const tree = engine.composition(args.component);
|
|
2248
|
-
return {
|
|
2249
|
-
text: JSON.stringify({
|
|
2250
|
-
mode: "composition",
|
|
2251
|
-
...tree
|
|
2252
|
-
})
|
|
2253
|
-
};
|
|
2254
|
-
}
|
|
2255
|
-
case "alternatives": {
|
|
2256
|
-
if (!args.component) {
|
|
2257
|
-
return { text: JSON.stringify({ error: "component is required for alternatives mode" }), isError: true };
|
|
2258
|
-
}
|
|
2259
|
-
if (!engine.hasNode(args.component)) {
|
|
2260
|
-
const closest = componentNames2 ? findClosestMatch(args.component, componentNames2) : null;
|
|
2261
|
-
const suggestion = closest ? ` Did you mean "${closest}"?` : "";
|
|
2262
|
-
return { text: JSON.stringify({ error: `Component "${args.component}" not found in graph.${suggestion}` }), isError: true };
|
|
130
|
+
const [recommended, ...alternatives] = top;
|
|
131
|
+
return {
|
|
132
|
+
recommended,
|
|
133
|
+
alternatives,
|
|
134
|
+
_meta: meta
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function scoreCandidates(tokenData, family, input) {
|
|
138
|
+
if (family === "other") return [];
|
|
139
|
+
const value = normalizeComparableValue(input.value);
|
|
140
|
+
const flatTokens = tokenData.flat.length > 0 ? tokenData.flat : Object.values(tokenData.categories).flat();
|
|
141
|
+
const familyTokens = flatTokens.filter((token) => !isGarbageToken(token)).map((token) => ({ token, family: tokenFamily(token) })).filter((entry) => entry.family === family);
|
|
142
|
+
const scored = familyTokens.map(({ token }) => {
|
|
143
|
+
const tokenValue = normalizeComparableValue(token.value);
|
|
144
|
+
let score = 50;
|
|
145
|
+
let reason = "family-match";
|
|
146
|
+
if (value && tokenValue && value === tokenValue) {
|
|
147
|
+
score += 60;
|
|
148
|
+
reason = "exact-value-match";
|
|
149
|
+
} else if (value && tokenValue && family !== "color") {
|
|
150
|
+
const distanceScore = lengthDistanceScore(value, tokenValue);
|
|
151
|
+
if (distanceScore > 0) {
|
|
152
|
+
score += distanceScore;
|
|
153
|
+
reason = "nearest-neighbor";
|
|
2263
154
|
}
|
|
2264
|
-
const alts = engine.alternatives(args.component);
|
|
2265
|
-
return {
|
|
2266
|
-
text: JSON.stringify({
|
|
2267
|
-
mode: "alternatives",
|
|
2268
|
-
component: args.component,
|
|
2269
|
-
count: alts.length,
|
|
2270
|
-
alternatives: alts
|
|
2271
|
-
})
|
|
2272
|
-
};
|
|
2273
|
-
}
|
|
2274
|
-
case "islands": {
|
|
2275
|
-
const islands = engine.islands();
|
|
2276
|
-
return {
|
|
2277
|
-
text: JSON.stringify({
|
|
2278
|
-
mode: "islands",
|
|
2279
|
-
count: islands.length,
|
|
2280
|
-
islands: islands.map((island, i) => ({
|
|
2281
|
-
id: i + 1,
|
|
2282
|
-
size: island.length,
|
|
2283
|
-
components: island
|
|
2284
|
-
}))
|
|
2285
|
-
})
|
|
2286
|
-
};
|
|
2287
155
|
}
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
}),
|
|
2293
|
-
isError: true
|
|
2294
|
-
};
|
|
2295
|
-
}
|
|
156
|
+
score += nameRelevanceScore(token, input.property, input.context);
|
|
157
|
+
return { token, family, score, reason };
|
|
158
|
+
});
|
|
159
|
+
return scored.filter((candidate) => candidate.score > 0).sort((a, b) => b.score - a.score || a.token.name.localeCompare(b.token.name));
|
|
2296
160
|
}
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
const
|
|
2301
|
-
mode: args?.mode ?? "health",
|
|
2302
|
-
component: args?.component,
|
|
2303
|
-
target: args?.target,
|
|
2304
|
-
edgeTypes: args?.edgeTypes,
|
|
2305
|
-
maxDepth: args?.maxDepth
|
|
2306
|
-
};
|
|
2307
|
-
const data = ctx.data;
|
|
2308
|
-
const allNames = componentNames(data.snapshot);
|
|
2309
|
-
const result2 = handleGraphTool(
|
|
2310
|
-
graphArgs,
|
|
2311
|
-
data.graph,
|
|
2312
|
-
data.blocks,
|
|
2313
|
-
allNames
|
|
2314
|
-
);
|
|
2315
|
-
if (result2.isError) {
|
|
2316
|
-
return {
|
|
2317
|
-
content: [{ type: "text", text: result2.text }],
|
|
2318
|
-
isError: true
|
|
2319
|
-
};
|
|
2320
|
-
}
|
|
161
|
+
function presentCandidate(candidate, tokenData) {
|
|
162
|
+
const cssVar = cssVarForToken(candidate.token);
|
|
163
|
+
const resolvedValue = resolvedValueForToken(candidate.token);
|
|
164
|
+
const confidence = candidate.score >= 105 ? "high" : candidate.score >= 65 ? "medium" : "low";
|
|
2321
165
|
return {
|
|
2322
|
-
|
|
166
|
+
name: dottedNameForToken(candidate.token, tokenData),
|
|
167
|
+
...cssVar && { cssVar },
|
|
168
|
+
...cssVar && {
|
|
169
|
+
cssValue: resolvedValue ? `var(${cssVar}, ${resolvedValue})` : `var(${cssVar})`
|
|
170
|
+
},
|
|
171
|
+
...resolvedValue && { resolvedValue },
|
|
172
|
+
confidence,
|
|
173
|
+
reason: candidate.reason
|
|
2323
174
|
};
|
|
2324
|
-
}
|
|
175
|
+
}
|
|
176
|
+
function isGarbageToken(token) {
|
|
177
|
+
const value = token.value?.trim();
|
|
178
|
+
if (!value) return false;
|
|
179
|
+
if (value.includes("#{") || value.includes("$")) return true;
|
|
180
|
+
if (/^\$[\w-]+/.test(token.name)) return true;
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
function tokenFamily(token) {
|
|
184
|
+
const haystack = [
|
|
185
|
+
token.type,
|
|
186
|
+
token.category,
|
|
187
|
+
...token.path ?? [],
|
|
188
|
+
token.name
|
|
189
|
+
].filter(Boolean).join(" ").toLowerCase();
|
|
190
|
+
if (/\b(color|colour|background|foreground|surface|palette)\b/.test(haystack)) {
|
|
191
|
+
return "color";
|
|
192
|
+
}
|
|
193
|
+
if (/\b(radius|radii|rounded|corner)\b/.test(haystack)) return "radius";
|
|
194
|
+
if (/\b(border-width|border width|stroke-width|stroke width)\b/.test(haystack)) {
|
|
195
|
+
return "border-width";
|
|
196
|
+
}
|
|
197
|
+
if (/\b(shadow|elevation)\b/.test(haystack)) return "shadow";
|
|
198
|
+
if (/\b(font|type|typography|line-height|letter-spacing)\b/.test(haystack)) {
|
|
199
|
+
return "typography";
|
|
200
|
+
}
|
|
201
|
+
if (/\b(duration|transition|animation)\b/.test(haystack)) return "duration";
|
|
202
|
+
if (/\b(z-index|zindex)\b/.test(haystack)) return "z-index";
|
|
203
|
+
if (/\b(space|spacing|size|sizing|width|height|gap|padding|margin|inset)\b/.test(haystack)) {
|
|
204
|
+
return "spacing";
|
|
205
|
+
}
|
|
206
|
+
if (/\bborder\b/.test(haystack)) return "border-width";
|
|
207
|
+
return "other";
|
|
208
|
+
}
|
|
209
|
+
function cssVarForToken(token) {
|
|
210
|
+
if (token.name.startsWith("--")) return token.name;
|
|
211
|
+
const match = token.value?.match(/var\((--[\w-]+)/);
|
|
212
|
+
return match?.[1];
|
|
213
|
+
}
|
|
214
|
+
function dottedNameForToken(token, tokenData) {
|
|
215
|
+
if (!token.name.startsWith("--")) return token.name;
|
|
216
|
+
let name = token.name.slice(2);
|
|
217
|
+
const prefix = tokenData?.prefix?.replace(/^--/, "").replace(/-$/, "");
|
|
218
|
+
if (prefix && name.startsWith(`${prefix}-`)) {
|
|
219
|
+
name = name.slice(prefix.length + 1);
|
|
220
|
+
}
|
|
221
|
+
return name.replace(/-/g, ".");
|
|
222
|
+
}
|
|
223
|
+
function resolvedValueForToken(token) {
|
|
224
|
+
const value = token.value?.trim();
|
|
225
|
+
if (!value) return void 0;
|
|
226
|
+
const fallback = value.match(/var\(--[\w-]+,\s*([^)]+)\)/)?.[1]?.trim();
|
|
227
|
+
if (fallback) return fallback;
|
|
228
|
+
if (value.startsWith("var(")) return void 0;
|
|
229
|
+
return value;
|
|
230
|
+
}
|
|
231
|
+
function normalizeComparableValue(value) {
|
|
232
|
+
if (!value) return void 0;
|
|
233
|
+
const trimmed = value.trim().toLowerCase();
|
|
234
|
+
const color = normalizeColor(trimmed);
|
|
235
|
+
if (color) return color;
|
|
236
|
+
const length = parseLength(trimmed);
|
|
237
|
+
if (length) return `${length.value}${length.unit}`;
|
|
238
|
+
return trimmed.replace(/\s+/g, " ");
|
|
239
|
+
}
|
|
240
|
+
function looksLikeColor(value) {
|
|
241
|
+
return /^#([\da-f]{3,8})$/i.test(value) || /^rgba?\(/i.test(value) || /^hsla?\(/i.test(value);
|
|
242
|
+
}
|
|
243
|
+
function normalizeColor(value) {
|
|
244
|
+
const hex = value.match(/^#([\da-f]{3}|[\da-f]{6}|[\da-f]{8})$/i);
|
|
245
|
+
if (!hex) return void 0;
|
|
246
|
+
const body = hex[1].toLowerCase();
|
|
247
|
+
if (body.length === 3) {
|
|
248
|
+
return `#${body[0]}${body[0]}${body[1]}${body[1]}${body[2]}${body[2]}`;
|
|
249
|
+
}
|
|
250
|
+
return `#${body}`;
|
|
251
|
+
}
|
|
252
|
+
function parseLength(value) {
|
|
253
|
+
const match = value.match(/^(-?\d+(?:\.\d+)?)(px|rem|em|%)$/);
|
|
254
|
+
if (!match) return void 0;
|
|
255
|
+
return { value: Number(match[1]), unit: match[2] };
|
|
256
|
+
}
|
|
257
|
+
function lengthDistanceScore(inputValue, tokenValue) {
|
|
258
|
+
const input = parseLength(inputValue);
|
|
259
|
+
const token = parseLength(tokenValue);
|
|
260
|
+
if (!input || !token || input.unit !== token.unit) return 0;
|
|
261
|
+
const distance = Math.abs(input.value - token.value);
|
|
262
|
+
if (distance === 0) return 60;
|
|
263
|
+
if (distance <= 2) return 35;
|
|
264
|
+
if (distance <= 4) return 20;
|
|
265
|
+
if (distance <= 8) return 10;
|
|
266
|
+
return 0;
|
|
267
|
+
}
|
|
268
|
+
function nameRelevanceScore(token, property, context) {
|
|
269
|
+
const haystack = [token.name, token.category, ...token.path ?? []].join(" ").toLowerCase();
|
|
270
|
+
const prop = property.toLowerCase();
|
|
271
|
+
let score = 0;
|
|
272
|
+
for (const part of prop.split(/[^a-z0-9]+/).filter((part2) => part2.length > 2)) {
|
|
273
|
+
if (haystack.includes(part)) score += 4;
|
|
274
|
+
}
|
|
275
|
+
if (context === "component" && haystack.includes("component")) score += 3;
|
|
276
|
+
if (context === "global" && haystack.includes("global")) score += 3;
|
|
277
|
+
return score;
|
|
278
|
+
}
|
|
2325
279
|
|
|
2326
|
-
// src/tools/
|
|
2327
|
-
var
|
|
2328
|
-
const
|
|
2329
|
-
|
|
2330
|
-
const filter = args?.filter ?? void 0;
|
|
2331
|
-
let entries = Object.values(ctx.data.components).filter((component) => component.performance).map((component) => ({
|
|
2332
|
-
name: component.name,
|
|
2333
|
-
...component.performance
|
|
2334
|
-
}));
|
|
2335
|
-
if (entries.length === 0) {
|
|
280
|
+
// src/tools/tokens-suggest.ts
|
|
281
|
+
var tokensSuggestHandler = async (args, ctx) => {
|
|
282
|
+
const property = args.property;
|
|
283
|
+
if (!property || typeof property !== "string") {
|
|
2336
284
|
return {
|
|
2337
285
|
content: [
|
|
2338
286
|
{
|
|
2339
287
|
type: "text",
|
|
2340
|
-
text: JSON.stringify({
|
|
2341
|
-
total: 0,
|
|
2342
|
-
components: [],
|
|
2343
|
-
hint: `No performance data found. Run \`${BRAND.cliCommand} perf\` first to measure bundle sizes.`
|
|
2344
|
-
})
|
|
288
|
+
text: JSON.stringify({ error: "property is required." })
|
|
2345
289
|
}
|
|
2346
|
-
]
|
|
290
|
+
],
|
|
291
|
+
isError: true
|
|
2347
292
|
};
|
|
2348
293
|
}
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
}
|
|
2359
|
-
if (filter) {
|
|
2360
|
-
if (filter === "over-budget") {
|
|
2361
|
-
entries = entries.filter((entry) => entry.overBudget);
|
|
2362
|
-
} else {
|
|
2363
|
-
entries = entries.filter((entry) => entry.complexity === filter);
|
|
2364
|
-
}
|
|
2365
|
-
}
|
|
2366
|
-
switch (sort) {
|
|
2367
|
-
case "name":
|
|
2368
|
-
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
2369
|
-
break;
|
|
2370
|
-
case "budget":
|
|
2371
|
-
entries.sort((a, b) => b.budgetPercent - a.budgetPercent);
|
|
2372
|
-
break;
|
|
2373
|
-
case "size":
|
|
2374
|
-
default:
|
|
2375
|
-
entries.sort((a, b) => b.bundleSize - a.bundleSize);
|
|
2376
|
-
break;
|
|
2377
|
-
}
|
|
294
|
+
const context = args.context;
|
|
295
|
+
const catalogMeta = getCatalogMeta(ctx.data);
|
|
296
|
+
const result2 = suggestToken({
|
|
297
|
+
tokens: ctx.data.tokens,
|
|
298
|
+
property,
|
|
299
|
+
value: typeof args.value === "string" ? args.value : void 0,
|
|
300
|
+
context: context === "component" || context === "block" || context === "global" ? context : void 0,
|
|
301
|
+
catalogRevision: catalogMeta.catalogRevision,
|
|
302
|
+
updatedAt: catalogMeta.updatedAt
|
|
303
|
+
});
|
|
2378
304
|
return {
|
|
2379
|
-
content: [
|
|
2380
|
-
|
|
2381
|
-
type: "text",
|
|
2382
|
-
text: JSON.stringify({
|
|
2383
|
-
total: entries.length,
|
|
2384
|
-
summary: ctx.data.performanceSummary ?? void 0,
|
|
2385
|
-
components: entries
|
|
2386
|
-
})
|
|
2387
|
-
}
|
|
2388
|
-
]
|
|
305
|
+
content: [{ type: "text", text: JSON.stringify(result2) }],
|
|
306
|
+
_meta: result2._meta
|
|
2389
307
|
};
|
|
2390
308
|
};
|
|
2391
309
|
|
|
2392
310
|
// src/tools/spec-govern.ts
|
|
2393
|
-
|
|
2394
|
-
critical: 10,
|
|
2395
|
-
serious: 5,
|
|
2396
|
-
moderate: 2,
|
|
2397
|
-
minor: 1
|
|
2398
|
-
};
|
|
311
|
+
import { SEVERITY_WEIGHTS, maxSeverity } from "@fragments-sdk/core/severity";
|
|
2399
312
|
function isRuleEnabled(rule, defaultEnabled = true) {
|
|
2400
313
|
if (rule === void 0) return defaultEnabled;
|
|
2401
314
|
if (typeof rule === "boolean") return rule;
|
|
@@ -2434,9 +347,8 @@ function flattenNodes(spec) {
|
|
|
2434
347
|
return flattened;
|
|
2435
348
|
}
|
|
2436
349
|
function worstSeverity(violations) {
|
|
2437
|
-
const order = ["critical", "serious", "moderate", "minor"];
|
|
2438
350
|
return violations.reduce(
|
|
2439
|
-
(worst, violation) =>
|
|
351
|
+
(worst, violation) => maxSeverity(worst, violation.severity),
|
|
2440
352
|
"minor"
|
|
2441
353
|
);
|
|
2442
354
|
}
|
|
@@ -2448,12 +360,25 @@ function result(validator, violations) {
|
|
|
2448
360
|
violations
|
|
2449
361
|
};
|
|
2450
362
|
}
|
|
363
|
+
var SEVERITY_SCORE_CAPS = {
|
|
364
|
+
critical: 25,
|
|
365
|
+
serious: 50,
|
|
366
|
+
moderate: 80,
|
|
367
|
+
minor: 95
|
|
368
|
+
};
|
|
369
|
+
function verdictFor(violations) {
|
|
370
|
+
if (violations.length === 0) return "pass";
|
|
371
|
+
const worst = worstSeverity(violations);
|
|
372
|
+
return worst === "critical" || worst === "serious" ? "fail" : "warn";
|
|
373
|
+
}
|
|
2451
374
|
function computeScore(violations) {
|
|
375
|
+
if (violations.length === 0) return 100;
|
|
2452
376
|
const penalty = violations.reduce(
|
|
2453
377
|
(sum, violation) => sum + SEVERITY_WEIGHTS[violation.severity],
|
|
2454
378
|
0
|
|
2455
379
|
);
|
|
2456
|
-
|
|
380
|
+
const cap = SEVERITY_SCORE_CAPS[worstSeverity(violations)];
|
|
381
|
+
return Math.min(cap, Math.max(0, 100 - penalty));
|
|
2457
382
|
}
|
|
2458
383
|
function validateComponents(nodes, options) {
|
|
2459
384
|
const rules = options.policy?.rules ?? {};
|
|
@@ -2524,7 +449,76 @@ function validateSafety(nodes) {
|
|
|
2524
449
|
}
|
|
2525
450
|
}
|
|
2526
451
|
}
|
|
2527
|
-
return result("safety", violations);
|
|
452
|
+
return result("safety", violations);
|
|
453
|
+
}
|
|
454
|
+
function editDistance(a, b) {
|
|
455
|
+
const rows = a.length + 1;
|
|
456
|
+
const cols = b.length + 1;
|
|
457
|
+
const distances = Array.from({ length: rows }, () => Array(cols).fill(0));
|
|
458
|
+
for (let i = 0; i < rows; i++) distances[i][0] = i;
|
|
459
|
+
for (let j = 0; j < cols; j++) distances[0][j] = j;
|
|
460
|
+
for (let i = 1; i < rows; i++) {
|
|
461
|
+
for (let j = 1; j < cols; j++) {
|
|
462
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
463
|
+
distances[i][j] = Math.min(
|
|
464
|
+
distances[i - 1][j] + 1,
|
|
465
|
+
distances[i][j - 1] + 1,
|
|
466
|
+
distances[i - 1][j - 1] + cost
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return distances[a.length][b.length];
|
|
471
|
+
}
|
|
472
|
+
function normalizePropValue(value) {
|
|
473
|
+
return value.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
474
|
+
}
|
|
475
|
+
function closestAllowedValue(rawValue, allowedValues) {
|
|
476
|
+
const normalizedRaw = normalizePropValue(rawValue);
|
|
477
|
+
const ranked = allowedValues.map((value) => {
|
|
478
|
+
const normalizedValue = normalizePropValue(value);
|
|
479
|
+
const distance = editDistance(normalizedRaw, normalizedValue);
|
|
480
|
+
const prefixMatch = normalizedRaw.length >= 4 && (normalizedValue.startsWith(normalizedRaw) || normalizedRaw.startsWith(normalizedValue));
|
|
481
|
+
return { value, distance, prefixMatch };
|
|
482
|
+
}).sort((a, b) => {
|
|
483
|
+
if (a.prefixMatch !== b.prefixMatch) return a.prefixMatch ? -1 : 1;
|
|
484
|
+
return a.distance - b.distance || a.value.localeCompare(b.value);
|
|
485
|
+
});
|
|
486
|
+
const best = ranked[0];
|
|
487
|
+
if (!best) return void 0;
|
|
488
|
+
if (best.prefixMatch || best.distance <= 2) return best.value;
|
|
489
|
+
return void 0;
|
|
490
|
+
}
|
|
491
|
+
function validateProps(nodes, options) {
|
|
492
|
+
const rules = options.policy?.rules ?? {};
|
|
493
|
+
const propRule = rules["props/valid-values"];
|
|
494
|
+
const componentProps = options.componentProps ?? {};
|
|
495
|
+
const violations = [];
|
|
496
|
+
if (!isRuleEnabled(propRule)) {
|
|
497
|
+
return result("props", violations);
|
|
498
|
+
}
|
|
499
|
+
for (const [index, node] of nodes.entries()) {
|
|
500
|
+
const type = nodeType(node);
|
|
501
|
+
const propSchema = componentProps[type] ?? componentProps[parentType(type)];
|
|
502
|
+
if (!propSchema) continue;
|
|
503
|
+
for (const [prop, value] of Object.entries(node.props ?? {})) {
|
|
504
|
+
const allowedValues = propSchema[prop]?.values?.filter(Boolean) ?? [];
|
|
505
|
+
if (allowedValues.length === 0) continue;
|
|
506
|
+
const rawValue = typeof value === "string" ? value : void 0;
|
|
507
|
+
if (!rawValue || allowedValues.includes(rawValue)) continue;
|
|
508
|
+
const closest = closestAllowedValue(rawValue, allowedValues);
|
|
509
|
+
violations.push({
|
|
510
|
+
nodeId: nodeId(node, index),
|
|
511
|
+
nodeType: type,
|
|
512
|
+
rule: "props/invalid-value",
|
|
513
|
+
severity: ruleSeverity(propRule, "moderate"),
|
|
514
|
+
message: `Prop "${prop}" on ${type} has invalid value "${rawValue}"`,
|
|
515
|
+
suggestion: closest ? `Use "${closest}" for prop "${prop}" on ${type}.` : `Use one of: ${allowedValues.join(", ")}`,
|
|
516
|
+
prop,
|
|
517
|
+
rawValue
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return result("props", violations);
|
|
2528
522
|
}
|
|
2529
523
|
function hasHardcodedCssValue(value) {
|
|
2530
524
|
if (value.includes("var(")) return false;
|
|
@@ -2576,14 +570,29 @@ function validateTokens(nodes, options) {
|
|
|
2576
570
|
}
|
|
2577
571
|
return result("tokens", violations);
|
|
2578
572
|
}
|
|
573
|
+
function textFromUnknown(value) {
|
|
574
|
+
if (typeof value === "string") return value.trim();
|
|
575
|
+
if (Array.isArray(value)) return textFromChildren(value);
|
|
576
|
+
if (typeof value === "object" && value !== null) {
|
|
577
|
+
return textFromNode(value);
|
|
578
|
+
}
|
|
579
|
+
return "";
|
|
580
|
+
}
|
|
581
|
+
function textFromNode(node) {
|
|
582
|
+
const type = nodeType(node).toLowerCase();
|
|
583
|
+
const props = node.props ?? {};
|
|
584
|
+
const propText = [
|
|
585
|
+
textFromUnknown(props.children),
|
|
586
|
+
type === "text" ? textFromUnknown(props.value) : ""
|
|
587
|
+
].filter(Boolean);
|
|
588
|
+
const childText = textFromChildren(node.children);
|
|
589
|
+
return [...propText, childText].join(" ").trim();
|
|
590
|
+
}
|
|
2579
591
|
function textFromChildren(children) {
|
|
592
|
+
if (typeof children === "string") return children.trim();
|
|
2580
593
|
if (!Array.isArray(children)) return "";
|
|
2581
594
|
return children.map((child) => {
|
|
2582
|
-
|
|
2583
|
-
if (typeof child === "object" && child !== null && !Array.isArray(child)) {
|
|
2584
|
-
return textFromChildren(child.children);
|
|
2585
|
-
}
|
|
2586
|
-
return "";
|
|
595
|
+
return textFromUnknown(child);
|
|
2587
596
|
}).join(" ").trim();
|
|
2588
597
|
}
|
|
2589
598
|
function validateA11y(nodes) {
|
|
@@ -2593,7 +602,7 @@ function validateA11y(nodes) {
|
|
|
2593
602
|
if (!/button/i.test(type)) continue;
|
|
2594
603
|
const props = node.props ?? {};
|
|
2595
604
|
const label = props["aria-label"] ?? props["aria-labelledby"] ?? props.title;
|
|
2596
|
-
const childText =
|
|
605
|
+
const childText = textFromNode(node);
|
|
2597
606
|
if (typeof label === "string" && label.trim().length > 0) continue;
|
|
2598
607
|
if (childText.length > 0) continue;
|
|
2599
608
|
violations.push({
|
|
@@ -2613,6 +622,7 @@ function runSpecGovern(spec, options) {
|
|
|
2613
622
|
const results = [
|
|
2614
623
|
validateSafety(nodes),
|
|
2615
624
|
validateComponents(nodes, options),
|
|
625
|
+
validateProps(nodes, options),
|
|
2616
626
|
validateTokens(nodes, options),
|
|
2617
627
|
validateA11y(nodes)
|
|
2618
628
|
];
|
|
@@ -2621,6 +631,7 @@ function runSpecGovern(spec, options) {
|
|
|
2621
631
|
new Set(nodes.map((node) => nodeType(node)))
|
|
2622
632
|
);
|
|
2623
633
|
return {
|
|
634
|
+
verdict: verdictFor(violations),
|
|
2624
635
|
passed: results.every((entry) => entry.passed),
|
|
2625
636
|
score: computeScore(violations),
|
|
2626
637
|
results,
|
|
@@ -2638,7 +649,7 @@ function formatVerdict(verdict, format = "summary") {
|
|
|
2638
649
|
}
|
|
2639
650
|
const lines = [];
|
|
2640
651
|
const icon = verdict.passed ? "ok" : "fail";
|
|
2641
|
-
lines.push(`${icon} Governance check: score ${verdict.score}/100`);
|
|
652
|
+
lines.push(`${icon} Governance check: verdict ${verdict.verdict}, score ${verdict.score}/100`);
|
|
2642
653
|
lines.push("");
|
|
2643
654
|
for (const entry of verdict.results) {
|
|
2644
655
|
const resultIcon = entry.passed ? "ok" : "fail";
|
|
@@ -2657,6 +668,14 @@ function formatVerdict(verdict, format = "summary") {
|
|
|
2657
668
|
}
|
|
2658
669
|
|
|
2659
670
|
// src/tools/govern.ts
|
|
671
|
+
function buildComponentProps(ctx) {
|
|
672
|
+
return Object.fromEntries(
|
|
673
|
+
Object.values(ctx.data.components).map((component) => [
|
|
674
|
+
component.name,
|
|
675
|
+
component.props
|
|
676
|
+
])
|
|
677
|
+
);
|
|
678
|
+
}
|
|
2660
679
|
var governHandler = async (args, ctx) => {
|
|
2661
680
|
const spec = args?.spec;
|
|
2662
681
|
if (!spec || typeof spec !== "object") {
|
|
@@ -2665,7 +684,7 @@ var governHandler = async (args, ctx) => {
|
|
|
2665
684
|
{
|
|
2666
685
|
type: "text",
|
|
2667
686
|
text: JSON.stringify({
|
|
2668
|
-
error: "spec is required and must be an object with { nodes: [{ id, type, props, children }] }"
|
|
687
|
+
error: "spec is required and must be an object with { nodes: [{ id, type, props, children }], root?, metadata? }. See the govern.schema MCP resource for the full schema."
|
|
2669
688
|
})
|
|
2670
689
|
}
|
|
2671
690
|
],
|
|
@@ -2681,12 +700,15 @@ var governHandler = async (args, ctx) => {
|
|
|
2681
700
|
const verdict = runSpecGovern(spec, {
|
|
2682
701
|
allowedComponents,
|
|
2683
702
|
tokenPrefix: ctx.data.tokens?.prefix,
|
|
2684
|
-
policy: policyOverrides
|
|
703
|
+
policy: policyOverrides,
|
|
704
|
+
componentProps: buildComponentProps(ctx)
|
|
2685
705
|
});
|
|
2686
706
|
const text = format === "summary" ? formatVerdict(verdict, "summary") : JSON.stringify(verdict);
|
|
2687
707
|
return {
|
|
2688
708
|
content: [{ type: "text", text }],
|
|
2689
709
|
_meta: {
|
|
710
|
+
...getCatalogMeta(ctx.data),
|
|
711
|
+
verdict: verdict.verdict,
|
|
2690
712
|
score: verdict.score,
|
|
2691
713
|
passed: verdict.passed,
|
|
2692
714
|
violationCount: verdict.results.reduce(
|
|
@@ -2703,7 +725,7 @@ var governHandler = async (args, ctx) => {
|
|
|
2703
725
|
{
|
|
2704
726
|
type: "text",
|
|
2705
727
|
text: JSON.stringify({
|
|
2706
|
-
error: isSpecError ? `Invalid spec format: ${message}. Expected: { nodes: [{ id
|
|
728
|
+
error: isSpecError ? `Invalid spec format: ${message}. Expected: { nodes: [{ id?: string, type: string, props?: object, children?: array|string }], root?: string, metadata?: object }. See the govern.schema MCP resource for examples.` : message
|
|
2707
729
|
})
|
|
2708
730
|
}
|
|
2709
731
|
],
|
|
@@ -2752,8 +774,11 @@ function validateNodeShape(node, path) {
|
|
|
2752
774
|
return `${path}.props must be an object`;
|
|
2753
775
|
}
|
|
2754
776
|
if ("children" in node && node.children !== void 0) {
|
|
777
|
+
if (typeof node.children === "string") {
|
|
778
|
+
return null;
|
|
779
|
+
}
|
|
2755
780
|
if (!Array.isArray(node.children)) {
|
|
2756
|
-
return `${path}.children must be an array`;
|
|
781
|
+
return `${path}.children must be an array or string`;
|
|
2757
782
|
}
|
|
2758
783
|
for (const [index, child] of node.children.entries()) {
|
|
2759
784
|
if (!isPlainObject(child)) continue;
|
|
@@ -2898,6 +923,59 @@ function getNextAction(status, replacements, unresolvedAmbiguityCount) {
|
|
|
2898
923
|
if (replacements.length > 0) return "review_partial_fix";
|
|
2899
924
|
return "revise_input";
|
|
2900
925
|
}
|
|
926
|
+
function authorizationForAmbiguity(args) {
|
|
927
|
+
const required = [];
|
|
928
|
+
if (!args.applyFixes) {
|
|
929
|
+
required.push("applyFixes");
|
|
930
|
+
}
|
|
931
|
+
if (!args.allowElicitation) {
|
|
932
|
+
required.push("allowElicitation");
|
|
933
|
+
} else if (!args.supportsElicitation) {
|
|
934
|
+
required.push("clientCapabilities.elicitation.form");
|
|
935
|
+
}
|
|
936
|
+
if (!args.allowSampling) {
|
|
937
|
+
required.push("allowSampling");
|
|
938
|
+
} else if (!args.supportsSampling) {
|
|
939
|
+
required.push("clientCapabilities.sampling");
|
|
940
|
+
}
|
|
941
|
+
return required;
|
|
942
|
+
}
|
|
943
|
+
function buildWouldFixIfAuthorized(args) {
|
|
944
|
+
const entries = [];
|
|
945
|
+
if (!args.applyFixes) {
|
|
946
|
+
entries.push(
|
|
947
|
+
...args.deterministicPreview.map((replacement) => ({
|
|
948
|
+
action: "replace_component",
|
|
949
|
+
nodeId: replacement.nodeId,
|
|
950
|
+
from: replacement.from,
|
|
951
|
+
to: replacement.to,
|
|
952
|
+
reason: replacement.reason,
|
|
953
|
+
requiredAuthorization: ["applyFixes"]
|
|
954
|
+
}))
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
const requiredAuthorization = authorizationForAmbiguity({
|
|
958
|
+
applyFixes: args.applyFixes,
|
|
959
|
+
allowElicitation: args.allowElicitation,
|
|
960
|
+
allowSampling: args.allowSampling,
|
|
961
|
+
supportsElicitation: args.supportsElicitation,
|
|
962
|
+
supportsSampling: args.supportsSampling
|
|
963
|
+
});
|
|
964
|
+
if (requiredAuthorization.length > 0) {
|
|
965
|
+
entries.push(
|
|
966
|
+
...args.unresolvedAmbiguities.map((ambiguity) => ({
|
|
967
|
+
action: "resolve_ambiguous_component",
|
|
968
|
+
nodeId: ambiguity.nodeId,
|
|
969
|
+
from: ambiguity.from,
|
|
970
|
+
candidates: ambiguity.candidates,
|
|
971
|
+
rankedCandidates: ambiguity.rankedCandidates,
|
|
972
|
+
reason: ambiguity.reason,
|
|
973
|
+
requiredAuthorization
|
|
974
|
+
}))
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
return entries;
|
|
978
|
+
}
|
|
2901
979
|
async function emitValidateAndFixTelemetry(args) {
|
|
2902
980
|
if (!args.ctx.mcp?.server) return;
|
|
2903
981
|
try {
|
|
@@ -2919,11 +997,20 @@ async function emitValidateAndFixTelemetry(args) {
|
|
|
2919
997
|
function buildAllowedComponents(ctx) {
|
|
2920
998
|
return buildEffectiveComponents(ctx).filter(({ selection }) => selection === "preferred" || selection === "allowed").map(({ component }) => component.name);
|
|
2921
999
|
}
|
|
1000
|
+
function buildComponentProps2(ctx) {
|
|
1001
|
+
return Object.fromEntries(
|
|
1002
|
+
Object.values(ctx.data.components).map((component) => [
|
|
1003
|
+
component.name,
|
|
1004
|
+
component.props
|
|
1005
|
+
])
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
2922
1008
|
function runGovern(spec, ctx, policyOverrides) {
|
|
2923
1009
|
return runSpecGovern(spec, {
|
|
2924
1010
|
allowedComponents: buildAllowedComponents(ctx),
|
|
2925
1011
|
tokenPrefix: ctx.data.tokens?.prefix,
|
|
2926
|
-
policy: policyOverrides
|
|
1012
|
+
policy: policyOverrides,
|
|
1013
|
+
componentProps: buildComponentProps2(ctx)
|
|
2927
1014
|
});
|
|
2928
1015
|
}
|
|
2929
1016
|
function walkNodes2(nodes, visitor, path = "nodes") {
|
|
@@ -3237,11 +1324,14 @@ var validateAndFixHandler = async (args, ctx) => {
|
|
|
3237
1324
|
let replacements = [];
|
|
3238
1325
|
let ambiguities = [];
|
|
3239
1326
|
const resolutionPath = [];
|
|
1327
|
+
const supportsElicitation = supportsFormElicitation(ctx);
|
|
1328
|
+
const supportsModelSampling = supportsSampling(ctx);
|
|
1329
|
+
const preview = !originalVerdict.passed ? applyDeterministicReplacements(spec, ctx) : void 0;
|
|
1330
|
+
if (!applyFixes && preview) {
|
|
1331
|
+
ambiguities = preview.ambiguities;
|
|
1332
|
+
}
|
|
3240
1333
|
if (!originalVerdict.passed && applyFixes) {
|
|
3241
|
-
const result2 = applyDeterministicReplacements(
|
|
3242
|
-
spec,
|
|
3243
|
-
ctx
|
|
3244
|
-
);
|
|
1334
|
+
const result2 = preview ?? applyDeterministicReplacements(spec, ctx);
|
|
3245
1335
|
replacements = result2.replacements;
|
|
3246
1336
|
ambiguities = result2.ambiguities;
|
|
3247
1337
|
if (replacements.length > 0) {
|
|
@@ -3251,7 +1341,7 @@ var validateAndFixHandler = async (args, ctx) => {
|
|
|
3251
1341
|
const unresolvedAmbiguities2 = ambiguities.filter(
|
|
3252
1342
|
(ambiguity) => !replacements.some((replacement) => replacement.nodeId === ambiguity.nodeId)
|
|
3253
1343
|
);
|
|
3254
|
-
if (unresolvedAmbiguities2.length > 0 && allowElicitation &&
|
|
1344
|
+
if (unresolvedAmbiguities2.length > 0 && allowElicitation && supportsElicitation) {
|
|
3255
1345
|
workingSpec ??= cloneSpec(spec);
|
|
3256
1346
|
const elicitedReplacements = await resolveAmbiguitiesWithElicitation(
|
|
3257
1347
|
unresolvedAmbiguities2,
|
|
@@ -3266,7 +1356,7 @@ var validateAndFixHandler = async (args, ctx) => {
|
|
|
3266
1356
|
const remainingAmbiguities = ambiguities.filter(
|
|
3267
1357
|
(ambiguity) => !replacements.some((replacement) => replacement.nodeId === ambiguity.nodeId)
|
|
3268
1358
|
);
|
|
3269
|
-
if (remainingAmbiguities.length > 0 && allowSampling &&
|
|
1359
|
+
if (remainingAmbiguities.length > 0 && allowSampling && supportsModelSampling) {
|
|
3270
1360
|
workingSpec ??= cloneSpec(spec);
|
|
3271
1361
|
const sampledReplacements = await resolveAmbiguitiesWithSampling(
|
|
3272
1362
|
remainingAmbiguities,
|
|
@@ -3287,6 +1377,18 @@ var validateAndFixHandler = async (args, ctx) => {
|
|
|
3287
1377
|
const unresolvedAmbiguities = ambiguities.filter(
|
|
3288
1378
|
(ambiguity) => !replacements.some((replacement) => replacement.nodeId === ambiguity.nodeId)
|
|
3289
1379
|
).map(({ nodeRef: _nodeRef, ...ambiguity }) => ambiguity);
|
|
1380
|
+
const unresolvedAmbiguitiesWithRefs = ambiguities.filter(
|
|
1381
|
+
(ambiguity) => !replacements.some((replacement) => replacement.nodeId === ambiguity.nodeId)
|
|
1382
|
+
);
|
|
1383
|
+
const wouldFixIfAuthorized = buildWouldFixIfAuthorized({
|
|
1384
|
+
deterministicPreview: preview?.replacements ?? [],
|
|
1385
|
+
unresolvedAmbiguities: unresolvedAmbiguitiesWithRefs,
|
|
1386
|
+
applyFixes,
|
|
1387
|
+
allowElicitation,
|
|
1388
|
+
allowSampling,
|
|
1389
|
+
supportsElicitation,
|
|
1390
|
+
supportsSampling: supportsModelSampling
|
|
1391
|
+
});
|
|
3290
1392
|
const attestation = {
|
|
3291
1393
|
sourceType: ctx.data.snapshot.sourceType,
|
|
3292
1394
|
sourceLabel: ctx.data.snapshot.sourceLabel,
|
|
@@ -3301,9 +1403,9 @@ var validateAndFixHandler = async (args, ctx) => {
|
|
|
3301
1403
|
overrideApplied: Boolean(policyOverrides)
|
|
3302
1404
|
},
|
|
3303
1405
|
clientCapabilities: {
|
|
3304
|
-
sampling:
|
|
1406
|
+
sampling: supportsModelSampling,
|
|
3305
1407
|
samplingTools: Boolean(ctx.mcp?.clientCapabilities?.sampling?.tools),
|
|
3306
|
-
elicitationForm:
|
|
1408
|
+
elicitationForm: supportsElicitation,
|
|
3307
1409
|
roots: Boolean(ctx.mcp?.clientCapabilities?.roots)
|
|
3308
1410
|
},
|
|
3309
1411
|
capabilitiesUsed: {
|
|
@@ -3333,6 +1435,7 @@ var validateAndFixHandler = async (args, ctx) => {
|
|
|
3333
1435
|
replacements,
|
|
3334
1436
|
unresolvedAmbiguities.length
|
|
3335
1437
|
);
|
|
1438
|
+
const catalogMeta = getCatalogMeta(ctx.data);
|
|
3336
1439
|
const payload = {
|
|
3337
1440
|
status,
|
|
3338
1441
|
nextAction,
|
|
@@ -3358,7 +1461,8 @@ var validateAndFixHandler = async (args, ctx) => {
|
|
|
3358
1461
|
effectiveComponents,
|
|
3359
1462
|
"forbidden"
|
|
3360
1463
|
),
|
|
3361
|
-
unresolvedAmbiguities
|
|
1464
|
+
unresolvedAmbiguities,
|
|
1465
|
+
wouldFixIfAuthorized
|
|
3362
1466
|
};
|
|
3363
1467
|
await emitValidateAndFixTelemetry({
|
|
3364
1468
|
ctx,
|
|
@@ -3381,13 +1485,14 @@ var validateAndFixHandler = async (args, ctx) => {
|
|
|
3381
1485
|
}
|
|
3382
1486
|
],
|
|
3383
1487
|
_meta: {
|
|
1488
|
+
...catalogMeta,
|
|
3384
1489
|
status,
|
|
3385
1490
|
nextAction,
|
|
3386
1491
|
replacementCount: replacements.length,
|
|
3387
1492
|
passed: finalVerdict.passed,
|
|
3388
1493
|
unresolvedAmbiguityCount: unresolvedAmbiguities.length,
|
|
3389
|
-
|
|
3390
|
-
|
|
1494
|
+
wouldFixIfAuthorizedCount: wouldFixIfAuthorized.length,
|
|
1495
|
+
resolutionPath
|
|
3391
1496
|
}
|
|
3392
1497
|
};
|
|
3393
1498
|
} catch (error) {
|
|
@@ -3406,52 +1511,22 @@ var validateAndFixHandler = async (args, ctx) => {
|
|
|
3406
1511
|
}
|
|
3407
1512
|
};
|
|
3408
1513
|
|
|
3409
|
-
// src/
|
|
3410
|
-
var generateUiHandler = async (args, ctx) => {
|
|
3411
|
-
const prompt = args?.prompt;
|
|
3412
|
-
if (!prompt) {
|
|
3413
|
-
throw new Error("prompt is required");
|
|
3414
|
-
}
|
|
3415
|
-
const currentTree = args?.currentTree;
|
|
3416
|
-
const playgroundUrl = ctx.config.playgroundUrl ?? "https://usefragments.com";
|
|
3417
|
-
const response = await fetch(`${playgroundUrl}/api/playground/generate`, {
|
|
3418
|
-
method: "POST",
|
|
3419
|
-
headers: { "Content-Type": "application/json" },
|
|
3420
|
-
body: JSON.stringify({
|
|
3421
|
-
prompt,
|
|
3422
|
-
...currentTree && { currentSpec: currentTree }
|
|
3423
|
-
})
|
|
3424
|
-
});
|
|
3425
|
-
if (!response.ok) {
|
|
3426
|
-
const errorBody = await response.text();
|
|
3427
|
-
throw new Error(`Playground API error (${response.status}): ${errorBody}`);
|
|
3428
|
-
}
|
|
3429
|
-
const text = await response.text();
|
|
3430
|
-
return {
|
|
3431
|
-
content: [{
|
|
3432
|
-
type: "text",
|
|
3433
|
-
text
|
|
3434
|
-
}]
|
|
3435
|
-
};
|
|
3436
|
-
};
|
|
3437
|
-
|
|
3438
|
-
// src/findings-service.ts
|
|
1514
|
+
// src/cloud-http.ts
|
|
3439
1515
|
var DEFAULT_CLOUD_URL = "https://app.usefragments.com";
|
|
3440
1516
|
function normalizeCloudUrl(url) {
|
|
3441
1517
|
if (!url) return DEFAULT_CLOUD_URL;
|
|
3442
1518
|
return url.replace(/\/+$/, "");
|
|
3443
1519
|
}
|
|
3444
|
-
async function
|
|
3445
|
-
const base = normalizeCloudUrl(cloudUrl);
|
|
3446
|
-
const url = new URL(`${base}
|
|
3447
|
-
if (
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
if (params.limit != null) url.searchParams.set("limit", String(params.limit));
|
|
1520
|
+
async function cloudFetchJson(args) {
|
|
1521
|
+
const base = normalizeCloudUrl(args.cloudUrl);
|
|
1522
|
+
const url = new URL(`${base}${args.path}`);
|
|
1523
|
+
if (args.query) {
|
|
1524
|
+
for (const [key, value] of Object.entries(args.query)) {
|
|
1525
|
+
if (value !== void 0) url.searchParams.set(key, String(value));
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
3453
1528
|
const response = await fetch(url.toString(), {
|
|
3454
|
-
headers: { "X-API-Key": apiKey }
|
|
1529
|
+
headers: { "X-API-Key": args.apiKey }
|
|
3455
1530
|
});
|
|
3456
1531
|
if (!response.ok) {
|
|
3457
1532
|
const body = await response.text();
|
|
@@ -3463,11 +1538,29 @@ async function fetchFindings(apiKey, params, cloudUrl) {
|
|
|
3463
1538
|
message = body;
|
|
3464
1539
|
}
|
|
3465
1540
|
throw new Error(
|
|
3466
|
-
`Cloud
|
|
1541
|
+
`Cloud ${args.resource} API error (${response.status}): ${message}`
|
|
3467
1542
|
);
|
|
3468
1543
|
}
|
|
3469
1544
|
return await response.json();
|
|
3470
1545
|
}
|
|
1546
|
+
|
|
1547
|
+
// src/findings-service.ts
|
|
1548
|
+
async function fetchFindings(apiKey, params, cloudUrl) {
|
|
1549
|
+
return cloudFetchJson({
|
|
1550
|
+
apiKey,
|
|
1551
|
+
cloudUrl,
|
|
1552
|
+
path: "/api/findings",
|
|
1553
|
+
resource: "findings",
|
|
1554
|
+
query: {
|
|
1555
|
+
status: params.status,
|
|
1556
|
+
severity: params.severity,
|
|
1557
|
+
category: params.category,
|
|
1558
|
+
ruleId: params.ruleId,
|
|
1559
|
+
filePath: params.filePath,
|
|
1560
|
+
limit: params.limit
|
|
1561
|
+
}
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
3471
1564
|
async function fetchFindingsForFile(apiKey, filePath, cloudUrl) {
|
|
3472
1565
|
return fetchFindings(
|
|
3473
1566
|
apiKey,
|
|
@@ -3475,8 +1568,59 @@ async function fetchFindingsForFile(apiKey, filePath, cloudUrl) {
|
|
|
3475
1568
|
cloudUrl
|
|
3476
1569
|
);
|
|
3477
1570
|
}
|
|
1571
|
+
function buildFindingSourceUrl(finding) {
|
|
1572
|
+
const { repoFullName, commitSha, filePath, line } = finding;
|
|
1573
|
+
if (!repoFullName || !commitSha || !filePath) return void 0;
|
|
1574
|
+
if (!/^[-\w.]+\/[-\w.]+$/.test(repoFullName)) return void 0;
|
|
1575
|
+
if (!/^[a-fA-F0-9]{7,64}$/.test(commitSha)) return void 0;
|
|
1576
|
+
const normalizedPath = filePath.replace(/^\/+/, "");
|
|
1577
|
+
if (!normalizedPath) return void 0;
|
|
1578
|
+
const encodedPath = normalizedPath.split("/").map((segment) => encodeURIComponent(segment)).join("/");
|
|
1579
|
+
const lineSuffix = line != null && line > 0 ? `#L${Math.floor(line)}` : "";
|
|
1580
|
+
return `https://github.com/${repoFullName}/blob/${commitSha}/${encodedPath}${lineSuffix}`;
|
|
1581
|
+
}
|
|
1582
|
+
async function fetchFindingsSummary(apiKey, params, cloudUrl) {
|
|
1583
|
+
const { findings } = await fetchFindings(
|
|
1584
|
+
apiKey,
|
|
1585
|
+
{ ...params, limit: params.limit ?? 200 },
|
|
1586
|
+
cloudUrl
|
|
1587
|
+
);
|
|
1588
|
+
return summarizeFindings(findings, params);
|
|
1589
|
+
}
|
|
1590
|
+
function summarizeFindings(findings, params) {
|
|
1591
|
+
const bySeverity = { error: 0, warning: 0, info: 0 };
|
|
1592
|
+
const byStatus = { open: 0, resolved: 0, ignored: 0 };
|
|
1593
|
+
const byCategory = {};
|
|
1594
|
+
const byRuleId = {};
|
|
1595
|
+
const byFilePath = {};
|
|
1596
|
+
for (const finding of findings) {
|
|
1597
|
+
bySeverity[finding.severity] += 1;
|
|
1598
|
+
byStatus[finding.status] += 1;
|
|
1599
|
+
increment(byRuleId, finding.ruleId);
|
|
1600
|
+
if (finding.category) increment(byCategory, finding.category);
|
|
1601
|
+
if (finding.filePath) increment(byFilePath, finding.filePath);
|
|
1602
|
+
}
|
|
1603
|
+
const { limit: _limit, ...filters } = params;
|
|
1604
|
+
return {
|
|
1605
|
+
total: findings.length,
|
|
1606
|
+
filters,
|
|
1607
|
+
bySeverity,
|
|
1608
|
+
byStatus,
|
|
1609
|
+
byCategory,
|
|
1610
|
+
byRuleId,
|
|
1611
|
+
byFilePath,
|
|
1612
|
+
topFiles: topEntries(byFilePath, "filePath"),
|
|
1613
|
+
topRules: topEntries(byRuleId, "ruleId")
|
|
1614
|
+
};
|
|
1615
|
+
}
|
|
1616
|
+
function increment(counts, key) {
|
|
1617
|
+
counts[key] = (counts[key] ?? 0) + 1;
|
|
1618
|
+
}
|
|
1619
|
+
function topEntries(counts, keyName) {
|
|
1620
|
+
return Object.entries(counts).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 10).map(([key, count]) => ({ [keyName]: key, count }));
|
|
1621
|
+
}
|
|
3478
1622
|
|
|
3479
|
-
// src/tools/
|
|
1623
|
+
// src/tools/cloud-auth.ts
|
|
3480
1624
|
function resolveCloudApiKey(ctx) {
|
|
3481
1625
|
return ctx.config.cloudApiKey ?? ctx.config.fileConfig?.cloud?.apiKey ?? process.env.FRAGMENTS_API_KEY;
|
|
3482
1626
|
}
|
|
@@ -3496,6 +1640,38 @@ function missingKeyError() {
|
|
|
3496
1640
|
isError: true
|
|
3497
1641
|
};
|
|
3498
1642
|
}
|
|
1643
|
+
|
|
1644
|
+
// src/tools/findings.ts
|
|
1645
|
+
function enrichFindings(findings, ctx) {
|
|
1646
|
+
return findings.map((finding) => {
|
|
1647
|
+
const sourceUrl = finding.sourceUrl ?? buildFindingSourceUrl(finding);
|
|
1648
|
+
const withSourceUrl = sourceUrl ? { ...finding, sourceUrl } : finding;
|
|
1649
|
+
if (!finding.prop || !finding.rawValue || !ctx.data.tokens) {
|
|
1650
|
+
return withSourceUrl;
|
|
1651
|
+
}
|
|
1652
|
+
const catalogMeta = getCatalogMeta(ctx.data);
|
|
1653
|
+
const suggestion = suggestToken({
|
|
1654
|
+
tokens: ctx.data.tokens,
|
|
1655
|
+
property: finding.prop,
|
|
1656
|
+
value: finding.rawValue,
|
|
1657
|
+
catalogRevision: catalogMeta.catalogRevision,
|
|
1658
|
+
updatedAt: catalogMeta.updatedAt
|
|
1659
|
+
});
|
|
1660
|
+
if (!suggestion.recommended) {
|
|
1661
|
+
const {
|
|
1662
|
+
suggestedToken: _discarded,
|
|
1663
|
+
suggestedTokenDetails: _details,
|
|
1664
|
+
...rest
|
|
1665
|
+
} = withSourceUrl;
|
|
1666
|
+
return rest;
|
|
1667
|
+
}
|
|
1668
|
+
return {
|
|
1669
|
+
...withSourceUrl,
|
|
1670
|
+
suggestedToken: suggestion.recommended.cssVar ?? suggestion.recommended.name,
|
|
1671
|
+
suggestedTokenDetails: suggestion.recommended
|
|
1672
|
+
};
|
|
1673
|
+
});
|
|
1674
|
+
}
|
|
3499
1675
|
var findingsListHandler = async (args, ctx) => {
|
|
3500
1676
|
const apiKey = resolveCloudApiKey(ctx);
|
|
3501
1677
|
if (!apiKey) return missingKeyError();
|
|
@@ -3510,9 +1686,65 @@ var findingsListHandler = async (args, ctx) => {
|
|
|
3510
1686
|
if (args.limit != null) params.limit = Number(args.limit);
|
|
3511
1687
|
try {
|
|
3512
1688
|
const result2 = await fetchFindings(apiKey, params, cloudUrl);
|
|
1689
|
+
const findings = enrichFindings(result2.findings, ctx);
|
|
1690
|
+
const catalogMeta = getCatalogMeta(ctx.data);
|
|
1691
|
+
return {
|
|
1692
|
+
content: [
|
|
1693
|
+
{
|
|
1694
|
+
type: "text",
|
|
1695
|
+
text: JSON.stringify({ ...result2, findings })
|
|
1696
|
+
}
|
|
1697
|
+
],
|
|
1698
|
+
_meta: {
|
|
1699
|
+
...catalogMeta,
|
|
1700
|
+
count: findings.length,
|
|
1701
|
+
tokenSuggestionCount: findings.filter(
|
|
1702
|
+
(finding) => finding.suggestedTokenDetails
|
|
1703
|
+
).length
|
|
1704
|
+
}
|
|
1705
|
+
};
|
|
1706
|
+
} catch (error) {
|
|
1707
|
+
return {
|
|
1708
|
+
content: [
|
|
1709
|
+
{
|
|
1710
|
+
type: "text",
|
|
1711
|
+
text: JSON.stringify({
|
|
1712
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1713
|
+
})
|
|
1714
|
+
}
|
|
1715
|
+
],
|
|
1716
|
+
isError: true
|
|
1717
|
+
};
|
|
1718
|
+
}
|
|
1719
|
+
};
|
|
1720
|
+
var findingsSummaryHandler = async (args, ctx) => {
|
|
1721
|
+
const apiKey = resolveCloudApiKey(ctx);
|
|
1722
|
+
if (!apiKey) return missingKeyError();
|
|
1723
|
+
const cloudUrl = resolveCloudUrl(ctx);
|
|
1724
|
+
const params = {};
|
|
1725
|
+
if (args.status) params.status = args.status;
|
|
1726
|
+
if (args.severity)
|
|
1727
|
+
params.severity = args.severity;
|
|
1728
|
+
if (args.category) params.category = String(args.category);
|
|
1729
|
+
if (args.ruleId) params.ruleId = String(args.ruleId);
|
|
1730
|
+
if (args.filePath) params.filePath = String(args.filePath);
|
|
1731
|
+
if (args.limit != null) params.limit = Number(args.limit);
|
|
1732
|
+
try {
|
|
1733
|
+
const summary = await fetchFindingsSummary(apiKey, params, cloudUrl);
|
|
1734
|
+
const catalogMeta = getCatalogMeta(ctx.data);
|
|
3513
1735
|
return {
|
|
3514
|
-
content: [
|
|
3515
|
-
|
|
1736
|
+
content: [
|
|
1737
|
+
{
|
|
1738
|
+
type: "text",
|
|
1739
|
+
text: JSON.stringify(summary)
|
|
1740
|
+
}
|
|
1741
|
+
],
|
|
1742
|
+
_meta: {
|
|
1743
|
+
...catalogMeta,
|
|
1744
|
+
total: summary.total,
|
|
1745
|
+
topFileCount: summary.topFiles.length,
|
|
1746
|
+
topRuleCount: summary.topRules.length
|
|
1747
|
+
}
|
|
3516
1748
|
};
|
|
3517
1749
|
} catch (error) {
|
|
3518
1750
|
return {
|
|
@@ -3546,7 +1778,11 @@ var findingsForFileHandler = async (args, ctx) => {
|
|
|
3546
1778
|
const cloudUrl = resolveCloudUrl(ctx);
|
|
3547
1779
|
try {
|
|
3548
1780
|
const result2 = await fetchFindingsForFile(apiKey, filePath, cloudUrl);
|
|
3549
|
-
const findings =
|
|
1781
|
+
const findings = enrichFindings(
|
|
1782
|
+
result2.findings.filter((f) => f.filePath === filePath),
|
|
1783
|
+
ctx
|
|
1784
|
+
);
|
|
1785
|
+
const catalogMeta = getCatalogMeta(ctx.data);
|
|
3550
1786
|
return {
|
|
3551
1787
|
content: [
|
|
3552
1788
|
{
|
|
@@ -3554,7 +1790,14 @@ var findingsForFileHandler = async (args, ctx) => {
|
|
|
3554
1790
|
text: JSON.stringify({ findings, filePath })
|
|
3555
1791
|
}
|
|
3556
1792
|
],
|
|
3557
|
-
_meta: {
|
|
1793
|
+
_meta: {
|
|
1794
|
+
...catalogMeta,
|
|
1795
|
+
count: findings.length,
|
|
1796
|
+
filePath,
|
|
1797
|
+
tokenSuggestionCount: findings.filter(
|
|
1798
|
+
(finding) => finding.suggestedTokenDetails
|
|
1799
|
+
).length
|
|
1800
|
+
}
|
|
3558
1801
|
};
|
|
3559
1802
|
} catch (error) {
|
|
3560
1803
|
return {
|
|
@@ -3571,43 +1814,208 @@ var findingsForFileHandler = async (args, ctx) => {
|
|
|
3571
1814
|
}
|
|
3572
1815
|
};
|
|
3573
1816
|
|
|
1817
|
+
// src/tools/swap-to-canonical.ts
|
|
1818
|
+
import * as ts from "typescript";
|
|
1819
|
+
import { resolveCanonicalForHtmlElement, formatRawHtmlElement } from "@fragments-sdk/classifier";
|
|
1820
|
+
|
|
1821
|
+
// src/canonical-mappings-service.ts
|
|
1822
|
+
async function fetchCanonicalMappings(apiKey, cloudUrl) {
|
|
1823
|
+
const catalog = await cloudFetchJson({
|
|
1824
|
+
apiKey,
|
|
1825
|
+
cloudUrl,
|
|
1826
|
+
path: "/api/catalog",
|
|
1827
|
+
resource: "catalog"
|
|
1828
|
+
});
|
|
1829
|
+
return catalog.canonicalMappings ?? [];
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
// src/tools/swap-to-canonical.ts
|
|
1833
|
+
function errorResult(message) {
|
|
1834
|
+
return {
|
|
1835
|
+
content: [
|
|
1836
|
+
{
|
|
1837
|
+
type: "text",
|
|
1838
|
+
text: JSON.stringify({ error: message })
|
|
1839
|
+
}
|
|
1840
|
+
],
|
|
1841
|
+
isError: true
|
|
1842
|
+
};
|
|
1843
|
+
}
|
|
1844
|
+
var resolveCanonicalForElement = resolveCanonicalForHtmlElement;
|
|
1845
|
+
function walkRawJsxElements(source) {
|
|
1846
|
+
const results = [];
|
|
1847
|
+
const visit = (node) => {
|
|
1848
|
+
let opening;
|
|
1849
|
+
if (ts.isJsxSelfClosingElement(node)) {
|
|
1850
|
+
opening = node;
|
|
1851
|
+
} else if (ts.isJsxElement(node)) {
|
|
1852
|
+
opening = node.openingElement;
|
|
1853
|
+
}
|
|
1854
|
+
if (opening) {
|
|
1855
|
+
const tagNameNode = opening.tagName;
|
|
1856
|
+
if (ts.isIdentifier(tagNameNode)) {
|
|
1857
|
+
const tagName = tagNameNode.text;
|
|
1858
|
+
if (/^[a-z]/.test(tagName)) {
|
|
1859
|
+
const attrs = /* @__PURE__ */ new Map();
|
|
1860
|
+
for (const attr of opening.attributes.properties) {
|
|
1861
|
+
if (!ts.isJsxAttribute(attr)) continue;
|
|
1862
|
+
if (!attr.name || !ts.isIdentifier(attr.name)) continue;
|
|
1863
|
+
const name = attr.name.text;
|
|
1864
|
+
if (!attr.initializer) {
|
|
1865
|
+
attrs.set(name, { value: "", dynamic: false });
|
|
1866
|
+
continue;
|
|
1867
|
+
}
|
|
1868
|
+
if (ts.isStringLiteral(attr.initializer)) {
|
|
1869
|
+
attrs.set(name, {
|
|
1870
|
+
value: attr.initializer.text,
|
|
1871
|
+
dynamic: false
|
|
1872
|
+
});
|
|
1873
|
+
} else if (ts.isJsxExpression(attr.initializer)) {
|
|
1874
|
+
attrs.set(name, { value: "", dynamic: true });
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
const start = opening.getStart(source);
|
|
1878
|
+
const { line } = source.getLineAndCharacterOfPosition(start);
|
|
1879
|
+
results.push({ tagName, attrs, line: line + 1 });
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
ts.forEachChild(node, visit);
|
|
1884
|
+
};
|
|
1885
|
+
visit(source);
|
|
1886
|
+
return results;
|
|
1887
|
+
}
|
|
1888
|
+
var formatRawElement = formatRawHtmlElement;
|
|
1889
|
+
function buildPropMapping(attrs, rowMapping) {
|
|
1890
|
+
const transforms = new Map(rowMapping.map((m) => [m.rawProp, m]));
|
|
1891
|
+
const mapping = [];
|
|
1892
|
+
for (const rawProp of attrs.keys()) {
|
|
1893
|
+
if (rawProp === "type") continue;
|
|
1894
|
+
mapping.push(transforms.get(rawProp) ?? { rawProp, canonicalProp: rawProp });
|
|
1895
|
+
}
|
|
1896
|
+
return mapping;
|
|
1897
|
+
}
|
|
1898
|
+
function groupMappingsByCanonical(mappings) {
|
|
1899
|
+
const out = /* @__PURE__ */ new Map();
|
|
1900
|
+
for (const m of mappings) {
|
|
1901
|
+
if (!m.importPath) continue;
|
|
1902
|
+
const list = out.get(m.canonical) ?? [];
|
|
1903
|
+
list.push(m);
|
|
1904
|
+
out.set(m.canonical, list);
|
|
1905
|
+
}
|
|
1906
|
+
for (const list of out.values()) {
|
|
1907
|
+
list.sort((a, b) => {
|
|
1908
|
+
const ac = a.confidence ?? 0;
|
|
1909
|
+
const bc = b.confidence ?? 0;
|
|
1910
|
+
if (ac !== bc) return bc - ac;
|
|
1911
|
+
return a.name.localeCompare(b.name);
|
|
1912
|
+
});
|
|
1913
|
+
}
|
|
1914
|
+
return out;
|
|
1915
|
+
}
|
|
1916
|
+
function buildSwapSuggestions(args) {
|
|
1917
|
+
const eligible = args.mappings.filter(
|
|
1918
|
+
(m) => (m.canonicalStatus ?? legacyStatusForMapping(m)) === "confirmed"
|
|
1919
|
+
);
|
|
1920
|
+
if (eligible.length === 0) return [];
|
|
1921
|
+
const byCanonical = groupMappingsByCanonical(eligible);
|
|
1922
|
+
if (byCanonical.size === 0) return [];
|
|
1923
|
+
const source = ts.createSourceFile(
|
|
1924
|
+
args.filePath,
|
|
1925
|
+
args.fileContent,
|
|
1926
|
+
ts.ScriptTarget.Latest,
|
|
1927
|
+
/* setParentNodes */
|
|
1928
|
+
true,
|
|
1929
|
+
ts.ScriptKind.TSX
|
|
1930
|
+
);
|
|
1931
|
+
const elements = walkRawJsxElements(source);
|
|
1932
|
+
const suggestions = [];
|
|
1933
|
+
for (const el of elements) {
|
|
1934
|
+
const canonical = resolveCanonicalForElement(el.tagName, el.attrs);
|
|
1935
|
+
if (!canonical) continue;
|
|
1936
|
+
const candidates = byCanonical.get(canonical);
|
|
1937
|
+
if (!candidates || candidates.length === 0) continue;
|
|
1938
|
+
const primary = candidates[0];
|
|
1939
|
+
const importPath = primary.importPath;
|
|
1940
|
+
if (!importPath) continue;
|
|
1941
|
+
const suggestion = {
|
|
1942
|
+
rawElement: formatRawElement(el.tagName, el.attrs),
|
|
1943
|
+
canonical,
|
|
1944
|
+
componentName: primary.name,
|
|
1945
|
+
importPath,
|
|
1946
|
+
propMapping: buildPropMapping(el.attrs, primary.propMapping),
|
|
1947
|
+
line: el.line
|
|
1948
|
+
};
|
|
1949
|
+
if (candidates.length > 1) {
|
|
1950
|
+
suggestion.alternates = candidates.slice(1).filter((c) => c.importPath).map((c) => ({
|
|
1951
|
+
name: c.name,
|
|
1952
|
+
importPath: c.importPath,
|
|
1953
|
+
confidence: c.confidence ?? 0
|
|
1954
|
+
}));
|
|
1955
|
+
}
|
|
1956
|
+
suggestions.push(suggestion);
|
|
1957
|
+
}
|
|
1958
|
+
return suggestions;
|
|
1959
|
+
}
|
|
1960
|
+
function legacyStatusForMapping(mapping) {
|
|
1961
|
+
return mapping.canonicalConfidence === "overridden" || mapping.canonicalConfidence === "auto" ? "confirmed" : "proposed";
|
|
1962
|
+
}
|
|
1963
|
+
var swapToCanonicalHandler = async (args, ctx) => {
|
|
1964
|
+
const apiKey = resolveCloudApiKey(ctx);
|
|
1965
|
+
if (!apiKey) return missingKeyError();
|
|
1966
|
+
const filePath = typeof args.filePath === "string" ? args.filePath : "";
|
|
1967
|
+
const fileContent = typeof args.fileContent === "string" ? args.fileContent : "";
|
|
1968
|
+
if (!filePath || !fileContent) {
|
|
1969
|
+
return errorResult("filePath and fileContent are required.");
|
|
1970
|
+
}
|
|
1971
|
+
const cloudUrl = resolveCloudUrl(ctx);
|
|
1972
|
+
let mappings;
|
|
1973
|
+
try {
|
|
1974
|
+
mappings = await fetchCanonicalMappings(apiKey, cloudUrl);
|
|
1975
|
+
} catch (error) {
|
|
1976
|
+
return errorResult(error instanceof Error ? error.message : String(error));
|
|
1977
|
+
}
|
|
1978
|
+
const suggestions = buildSwapSuggestions({
|
|
1979
|
+
filePath,
|
|
1980
|
+
fileContent,
|
|
1981
|
+
mappings
|
|
1982
|
+
});
|
|
1983
|
+
const message = mappings.length === 0 ? "No confirmed canonical primitives are configured for this project. Visit the Components page to confirm primitives." : void 0;
|
|
1984
|
+
return {
|
|
1985
|
+
content: [
|
|
1986
|
+
{
|
|
1987
|
+
type: "text",
|
|
1988
|
+
text: JSON.stringify({ suggestions, filePath, message })
|
|
1989
|
+
}
|
|
1990
|
+
],
|
|
1991
|
+
_meta: {
|
|
1992
|
+
count: suggestions.length,
|
|
1993
|
+
mappingCount: mappings.length,
|
|
1994
|
+
filePath
|
|
1995
|
+
}
|
|
1996
|
+
};
|
|
1997
|
+
};
|
|
1998
|
+
|
|
3574
1999
|
// src/tools/index.ts
|
|
3575
2000
|
var CORE_TOOLS = {
|
|
3576
|
-
|
|
3577
|
-
inspect: inspectHandler,
|
|
3578
|
-
blocks: blocksHandler,
|
|
3579
|
-
tokens: tokensHandler,
|
|
3580
|
-
graph: graphHandler,
|
|
3581
|
-
perf: perfHandler,
|
|
2001
|
+
"tokens.suggest": tokensSuggestHandler,
|
|
3582
2002
|
govern: governHandler,
|
|
3583
2003
|
validate_and_fix: validateAndFixHandler,
|
|
3584
2004
|
findings_list: findingsListHandler,
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
render: renderHandler,
|
|
3589
|
-
fix: fixHandler,
|
|
3590
|
-
a11y: a11yHandler
|
|
3591
|
-
};
|
|
3592
|
-
var INFRA_TOOLS = {
|
|
3593
|
-
generate_ui: generateUiHandler
|
|
2005
|
+
findings_summary: findingsSummaryHandler,
|
|
2006
|
+
findings_for_file: findingsForFileHandler,
|
|
2007
|
+
swap_to_canonical: swapToCanonicalHandler
|
|
3594
2008
|
};
|
|
2009
|
+
var VIEWER_TOOLS = {};
|
|
2010
|
+
var INFRA_TOOLS = {};
|
|
3595
2011
|
var BUILTIN_TOOLS = {
|
|
3596
2012
|
...CORE_TOOLS,
|
|
3597
2013
|
...VIEWER_TOOLS,
|
|
3598
2014
|
...INFRA_TOOLS
|
|
3599
2015
|
};
|
|
3600
2016
|
var TOOL_CAPABILITIES = {
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
blocks: ["blocks"],
|
|
3604
|
-
tokens: ["tokens"],
|
|
3605
|
-
graph: ["graph"],
|
|
3606
|
-
perf: ["performance"],
|
|
3607
|
-
validate_and_fix: ["components"],
|
|
3608
|
-
render: ["components"],
|
|
3609
|
-
fix: ["components"],
|
|
3610
|
-
a11y: ["components"]
|
|
2017
|
+
"tokens.suggest": ["tokens"],
|
|
2018
|
+
validate_and_fix: ["components"]
|
|
3611
2019
|
};
|
|
3612
2020
|
|
|
3613
2021
|
// src/registry.ts
|
|
@@ -3747,16 +2155,17 @@ function telemetryMiddleware(logger) {
|
|
|
3747
2155
|
}
|
|
3748
2156
|
|
|
3749
2157
|
// src/source-selection.ts
|
|
3750
|
-
import { existsSync as
|
|
3751
|
-
import { join as
|
|
2158
|
+
import { existsSync as existsSync7 } from "fs";
|
|
2159
|
+
import { join as join6 } from "path";
|
|
3752
2160
|
|
|
3753
2161
|
// src/adapters/fragments-json.ts
|
|
3754
2162
|
import { readFile } from "fs/promises";
|
|
3755
2163
|
|
|
3756
2164
|
// src/discovery.ts
|
|
3757
|
-
import { existsSync as
|
|
3758
|
-
import { join as
|
|
2165
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3, readdirSync } from "fs";
|
|
2166
|
+
import { join as join2, dirname, resolve } from "path";
|
|
3759
2167
|
import { createRequire } from "module";
|
|
2168
|
+
import { BRAND } from "@fragments-sdk/core";
|
|
3760
2169
|
function resolveWorkspaceGlob(baseDir, pattern) {
|
|
3761
2170
|
const parts = pattern.split("/");
|
|
3762
2171
|
let dirs = [baseDir];
|
|
@@ -3768,14 +2177,14 @@ function resolveWorkspaceGlob(baseDir, pattern) {
|
|
|
3768
2177
|
try {
|
|
3769
2178
|
for (const entry of readdirSync(d, { withFileTypes: true })) {
|
|
3770
2179
|
if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
|
|
3771
|
-
next.push(
|
|
2180
|
+
next.push(join2(d, entry.name));
|
|
3772
2181
|
}
|
|
3773
2182
|
}
|
|
3774
2183
|
} catch {
|
|
3775
2184
|
}
|
|
3776
2185
|
} else {
|
|
3777
|
-
const candidate =
|
|
3778
|
-
if (
|
|
2186
|
+
const candidate = join2(d, part);
|
|
2187
|
+
if (existsSync2(candidate)) next.push(candidate);
|
|
3779
2188
|
}
|
|
3780
2189
|
}
|
|
3781
2190
|
dirs = next;
|
|
@@ -3784,8 +2193,8 @@ function resolveWorkspaceGlob(baseDir, pattern) {
|
|
|
3784
2193
|
}
|
|
3785
2194
|
function getWorkspaceDirs(rootDir) {
|
|
3786
2195
|
const dirs = [];
|
|
3787
|
-
const rootPkgPath =
|
|
3788
|
-
if (
|
|
2196
|
+
const rootPkgPath = join2(rootDir, "package.json");
|
|
2197
|
+
if (existsSync2(rootPkgPath)) {
|
|
3789
2198
|
try {
|
|
3790
2199
|
const rootPkg = JSON.parse(readFileSync3(rootPkgPath, "utf-8"));
|
|
3791
2200
|
const workspaces = Array.isArray(rootPkg.workspaces) ? rootPkg.workspaces : rootPkg.workspaces?.packages;
|
|
@@ -3798,8 +2207,8 @@ function getWorkspaceDirs(rootDir) {
|
|
|
3798
2207
|
} catch {
|
|
3799
2208
|
}
|
|
3800
2209
|
}
|
|
3801
|
-
const pnpmWsPath =
|
|
3802
|
-
if (
|
|
2210
|
+
const pnpmWsPath = join2(rootDir, "pnpm-workspace.yaml");
|
|
2211
|
+
if (existsSync2(pnpmWsPath)) {
|
|
3803
2212
|
try {
|
|
3804
2213
|
const content = readFileSync3(pnpmWsPath, "utf-8");
|
|
3805
2214
|
const lines = content.split("\n");
|
|
@@ -3832,8 +2241,8 @@ function resolveDepPackageJson(localRequire, depName) {
|
|
|
3832
2241
|
const mainPath = localRequire.resolve(depName);
|
|
3833
2242
|
let dir = dirname(mainPath);
|
|
3834
2243
|
while (true) {
|
|
3835
|
-
const candidate =
|
|
3836
|
-
if (
|
|
2244
|
+
const candidate = join2(dir, "package.json");
|
|
2245
|
+
if (existsSync2(candidate)) {
|
|
3837
2246
|
const pkg = JSON.parse(readFileSync3(candidate, "utf-8"));
|
|
3838
2247
|
if (pkg.name === depName) return candidate;
|
|
3839
2248
|
}
|
|
@@ -3846,23 +2255,23 @@ function resolveDepPackageJson(localRequire, depName) {
|
|
|
3846
2255
|
return null;
|
|
3847
2256
|
}
|
|
3848
2257
|
function findFragmentsInDeps(dir, found, depField) {
|
|
3849
|
-
const pkgJsonPath =
|
|
3850
|
-
if (!
|
|
2258
|
+
const pkgJsonPath = join2(dir, "package.json");
|
|
2259
|
+
if (!existsSync2(pkgJsonPath)) return;
|
|
3851
2260
|
try {
|
|
3852
2261
|
const pkgJson = JSON.parse(readFileSync3(pkgJsonPath, "utf-8"));
|
|
3853
2262
|
const allDeps = {
|
|
3854
2263
|
...pkgJson.dependencies,
|
|
3855
2264
|
...pkgJson.devDependencies
|
|
3856
2265
|
};
|
|
3857
|
-
const localRequire = createRequire(
|
|
2266
|
+
const localRequire = createRequire(join2(dir, "noop.js"));
|
|
3858
2267
|
for (const depName of Object.keys(allDeps)) {
|
|
3859
2268
|
try {
|
|
3860
2269
|
const depPkgPath = resolveDepPackageJson(localRequire, depName);
|
|
3861
2270
|
if (!depPkgPath) continue;
|
|
3862
2271
|
const depPkg = JSON.parse(readFileSync3(depPkgPath, "utf-8"));
|
|
3863
2272
|
if (depPkg[depField]) {
|
|
3864
|
-
const fragmentsPath =
|
|
3865
|
-
if (
|
|
2273
|
+
const fragmentsPath = join2(dirname(depPkgPath), depPkg[depField]);
|
|
2274
|
+
if (existsSync2(fragmentsPath) && !found.includes(fragmentsPath)) {
|
|
3866
2275
|
found.push(fragmentsPath);
|
|
3867
2276
|
}
|
|
3868
2277
|
}
|
|
@@ -3877,8 +2286,8 @@ function findDesignSystemJson(startDir, outFile, depField) {
|
|
|
3877
2286
|
const resolvedStart = resolve(startDir);
|
|
3878
2287
|
let dir = resolvedStart;
|
|
3879
2288
|
while (true) {
|
|
3880
|
-
const candidate =
|
|
3881
|
-
if (
|
|
2289
|
+
const candidate = join2(dir, outFile);
|
|
2290
|
+
if (existsSync2(candidate)) {
|
|
3882
2291
|
found.push(candidate);
|
|
3883
2292
|
break;
|
|
3884
2293
|
}
|
|
@@ -3887,7 +2296,7 @@ function findDesignSystemJson(startDir, outFile, depField) {
|
|
|
3887
2296
|
dir = parent;
|
|
3888
2297
|
}
|
|
3889
2298
|
findFragmentsInDeps(resolvedStart, found, depField);
|
|
3890
|
-
if (found.length === 0 ||
|
|
2299
|
+
if (found.length === 0 || existsSync2(join2(resolvedStart, "pnpm-workspace.yaml"))) {
|
|
3891
2300
|
const workspaceDirs = getWorkspaceDirs(resolvedStart);
|
|
3892
2301
|
for (const wsDir of workspaceDirs) {
|
|
3893
2302
|
findFragmentsInDeps(wsDir, found, depField);
|
|
@@ -3902,8 +2311,8 @@ function findBundleManifest(startDir) {
|
|
|
3902
2311
|
const found = [];
|
|
3903
2312
|
let dir = resolve(startDir);
|
|
3904
2313
|
while (true) {
|
|
3905
|
-
const candidate =
|
|
3906
|
-
if (
|
|
2314
|
+
const candidate = join2(dir, BRAND.dataDir, BRAND.manifestFile);
|
|
2315
|
+
if (existsSync2(candidate)) {
|
|
3907
2316
|
found.push(candidate);
|
|
3908
2317
|
break;
|
|
3909
2318
|
}
|
|
@@ -3914,6 +2323,9 @@ function findBundleManifest(startDir) {
|
|
|
3914
2323
|
return found;
|
|
3915
2324
|
}
|
|
3916
2325
|
|
|
2326
|
+
// src/adapters/fragments-json.ts
|
|
2327
|
+
import { BRAND as BRAND2 } from "@fragments-sdk/core";
|
|
2328
|
+
|
|
3917
2329
|
// src/adapters/snapshot-converters.ts
|
|
3918
2330
|
import { mcpSnapshotSchema } from "@fragments-sdk/core";
|
|
3919
2331
|
function slugify(value) {
|
|
@@ -4059,13 +2471,15 @@ function tokensFromCompiledTokenData(tokens) {
|
|
|
4059
2471
|
category,
|
|
4060
2472
|
value: valueToString(entry.value),
|
|
4061
2473
|
description: entry.description
|
|
4062
|
-
}));
|
|
4063
|
-
|
|
4064
|
-
|
|
2474
|
+
})).filter((token) => !isGarbageToken(token));
|
|
2475
|
+
if (normalized.length > 0) {
|
|
2476
|
+
categories[category] = normalized;
|
|
2477
|
+
flat.push(...normalized);
|
|
2478
|
+
}
|
|
4065
2479
|
}
|
|
4066
2480
|
return {
|
|
4067
2481
|
prefix: tokens.prefix,
|
|
4068
|
-
total:
|
|
2482
|
+
total: flat.length,
|
|
4069
2483
|
categories,
|
|
4070
2484
|
flat
|
|
4071
2485
|
};
|
|
@@ -4104,18 +2518,18 @@ var FragmentsJsonAdapter = class {
|
|
|
4104
2518
|
const paths = this.discover(projectRoot);
|
|
4105
2519
|
if (paths.length === 0) {
|
|
4106
2520
|
throw new Error(
|
|
4107
|
-
`No ${
|
|
2521
|
+
`No ${BRAND2.outFile} found. Searched ${projectRoot} and package.json dependencies.
|
|
4108
2522
|
|
|
4109
2523
|
Fix: Add a project-level MCP config so the server runs from your workspace root:
|
|
4110
2524
|
|
|
4111
2525
|
Cursor: .cursor/mcp.json
|
|
4112
2526
|
VS Code: .vscode/mcp.json
|
|
4113
|
-
Claude: claude mcp add ${
|
|
2527
|
+
Claude: claude mcp add ${BRAND2.nameLower} -- npx @fragments-sdk/mcp
|
|
4114
2528
|
Windsurf: .windsurf/mcp.json
|
|
4115
2529
|
|
|
4116
2530
|
Or pass --project-root: npx @fragments-sdk/mcp -p /path/to/project
|
|
4117
2531
|
|
|
4118
|
-
If you're a library author, run \`${
|
|
2532
|
+
If you're a library author, run \`${BRAND2.cliCommand} build\` first.`
|
|
4119
2533
|
);
|
|
4120
2534
|
}
|
|
4121
2535
|
const content = await readFile(paths[0], "utf-8");
|
|
@@ -4164,7 +2578,7 @@ If you're a library author, run \`${BRAND.cliCommand} build\` first.`
|
|
|
4164
2578
|
const snapshot = validateSnapshot({
|
|
4165
2579
|
schemaVersion: 1,
|
|
4166
2580
|
sourceType: "fragments-json",
|
|
4167
|
-
sourceLabel:
|
|
2581
|
+
sourceLabel: BRAND2.outFile,
|
|
4168
2582
|
capabilities: buildCapabilities({
|
|
4169
2583
|
components,
|
|
4170
2584
|
blocks,
|
|
@@ -4200,12 +2614,12 @@ If you're a library author, run \`${BRAND.cliCommand} build\` first.`
|
|
|
4200
2614
|
};
|
|
4201
2615
|
|
|
4202
2616
|
// src/adapters/auto-extract.ts
|
|
4203
|
-
import { existsSync as
|
|
4204
|
-
import { join as
|
|
2617
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
|
|
2618
|
+
import { join as join5, relative, sep } from "path";
|
|
4205
2619
|
|
|
4206
2620
|
// src/adapters/discover-components.ts
|
|
4207
|
-
import { readdirSync as readdirSync2, existsSync as
|
|
4208
|
-
import { join as
|
|
2621
|
+
import { readdirSync as readdirSync2, existsSync as existsSync3 } from "fs";
|
|
2622
|
+
import { join as join3, extname, basename } from "path";
|
|
4209
2623
|
var EXCLUDED_DIRS = /* @__PURE__ */ new Set([
|
|
4210
2624
|
"node_modules",
|
|
4211
2625
|
"dist",
|
|
@@ -4241,10 +2655,10 @@ function discoverComponentFiles(projectRoot) {
|
|
|
4241
2655
|
"src/ui",
|
|
4242
2656
|
"lib/ui",
|
|
4243
2657
|
"packages"
|
|
4244
|
-
].map((d) =>
|
|
2658
|
+
].map((d) => join3(projectRoot, d)).filter((d) => existsSync3(d));
|
|
4245
2659
|
if (scanDirs.length === 0) {
|
|
4246
|
-
const srcDir =
|
|
4247
|
-
if (
|
|
2660
|
+
const srcDir = join3(projectRoot, "src");
|
|
2661
|
+
if (existsSync3(srcDir)) scanDirs.push(srcDir);
|
|
4248
2662
|
}
|
|
4249
2663
|
for (const dir of scanDirs) {
|
|
4250
2664
|
walkDir(dir, results, seen);
|
|
@@ -4263,14 +2677,14 @@ function walkDir(dir, results, seen, depth = 0) {
|
|
|
4263
2677
|
if (entry.name.startsWith(".")) continue;
|
|
4264
2678
|
if (entry.isDirectory()) {
|
|
4265
2679
|
if (EXCLUDED_DIRS.has(entry.name)) continue;
|
|
4266
|
-
walkDir(
|
|
2680
|
+
walkDir(join3(dir, entry.name), results, seen, depth + 1);
|
|
4267
2681
|
continue;
|
|
4268
2682
|
}
|
|
4269
2683
|
if (!entry.isFile()) continue;
|
|
4270
2684
|
const ext = extname(entry.name);
|
|
4271
2685
|
if (ext !== ".tsx" && ext !== ".jsx") continue;
|
|
4272
2686
|
if (EXCLUDED_PATTERNS.some((p) => p.test(entry.name))) continue;
|
|
4273
|
-
const filePath =
|
|
2687
|
+
const filePath = join3(dir, entry.name);
|
|
4274
2688
|
if (seen.has(filePath)) continue;
|
|
4275
2689
|
seen.add(filePath);
|
|
4276
2690
|
const name = inferComponentName(entry.name, dir);
|
|
@@ -4288,8 +2702,8 @@ function inferComponentName(fileName, dirPath) {
|
|
|
4288
2702
|
}
|
|
4289
2703
|
|
|
4290
2704
|
// src/adapters/scan-tokens.ts
|
|
4291
|
-
import { readdirSync as readdirSync3, readFileSync as readFileSync4, existsSync as
|
|
4292
|
-
import { join as
|
|
2705
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
|
|
2706
|
+
import { join as join4, extname as extname2 } from "path";
|
|
4293
2707
|
function scanTokens(projectRoot) {
|
|
4294
2708
|
const cssFiles = discoverCssFiles(projectRoot);
|
|
4295
2709
|
if (cssFiles.length === 0) return void 0;
|
|
@@ -4327,7 +2741,7 @@ function discoverCssFiles(projectRoot) {
|
|
|
4327
2741
|
"styles",
|
|
4328
2742
|
"css",
|
|
4329
2743
|
"app"
|
|
4330
|
-
].map((d) =>
|
|
2744
|
+
].map((d) => join4(projectRoot, d)).filter((d) => existsSync4(d));
|
|
4331
2745
|
searchDirs.push(projectRoot);
|
|
4332
2746
|
for (const dir of searchDirs) {
|
|
4333
2747
|
try {
|
|
@@ -4336,22 +2750,22 @@ function discoverCssFiles(projectRoot) {
|
|
|
4336
2750
|
if (!entry.isFile()) continue;
|
|
4337
2751
|
const ext = extname2(entry.name);
|
|
4338
2752
|
if (ext === ".css" || ext === ".scss") {
|
|
4339
|
-
files.push(
|
|
2753
|
+
files.push(join4(dir, entry.name));
|
|
4340
2754
|
}
|
|
4341
2755
|
}
|
|
4342
2756
|
} catch {
|
|
4343
2757
|
continue;
|
|
4344
2758
|
}
|
|
4345
2759
|
}
|
|
4346
|
-
const srcDir =
|
|
4347
|
-
if (
|
|
2760
|
+
const srcDir = join4(projectRoot, "src");
|
|
2761
|
+
if (existsSync4(srcDir)) {
|
|
4348
2762
|
try {
|
|
4349
2763
|
for (const subEntry of readdirSync3(srcDir, { withFileTypes: true })) {
|
|
4350
2764
|
if (subEntry.isDirectory() && ["styles", "css", "theme", "tokens"].includes(subEntry.name)) {
|
|
4351
|
-
const subDir =
|
|
2765
|
+
const subDir = join4(srcDir, subEntry.name);
|
|
4352
2766
|
for (const file of readdirSync3(subDir, { withFileTypes: true })) {
|
|
4353
2767
|
if (file.isFile() && (file.name.endsWith(".css") || file.name.endsWith(".scss"))) {
|
|
4354
|
-
files.push(
|
|
2768
|
+
files.push(join4(subDir, file.name));
|
|
4355
2769
|
}
|
|
4356
2770
|
}
|
|
4357
2771
|
}
|
|
@@ -4583,7 +2997,7 @@ Check that your tsconfig.json includes the component directories.`
|
|
|
4583
2997
|
var extractorModulePromise = null;
|
|
4584
2998
|
async function loadExtractorModule() {
|
|
4585
2999
|
if (!extractorModulePromise) {
|
|
4586
|
-
extractorModulePromise = import("./dist-
|
|
3000
|
+
extractorModulePromise = import("./dist-LVC53MY5.js");
|
|
4587
3001
|
}
|
|
4588
3002
|
return extractorModulePromise;
|
|
4589
3003
|
}
|
|
@@ -4849,15 +3263,15 @@ function inferCategory(relativePath) {
|
|
|
4849
3263
|
function findTsConfig(projectRoot) {
|
|
4850
3264
|
const candidates = ["tsconfig.json", "tsconfig.app.json"];
|
|
4851
3265
|
for (const name of candidates) {
|
|
4852
|
-
const p =
|
|
4853
|
-
if (
|
|
3266
|
+
const p = join5(projectRoot, name);
|
|
3267
|
+
if (existsSync5(p)) return p;
|
|
4854
3268
|
}
|
|
4855
3269
|
return null;
|
|
4856
3270
|
}
|
|
4857
3271
|
function readPackageName(projectRoot) {
|
|
4858
3272
|
try {
|
|
4859
|
-
const pkgPath =
|
|
4860
|
-
if (!
|
|
3273
|
+
const pkgPath = join5(projectRoot, "package.json");
|
|
3274
|
+
if (!existsSync5(pkgPath)) return void 0;
|
|
4861
3275
|
const pkg = JSON.parse(readFileSync5(pkgPath, "utf-8"));
|
|
4862
3276
|
return pkg.name;
|
|
4863
3277
|
} catch {
|
|
@@ -4880,7 +3294,36 @@ function chooseComponentSource(args) {
|
|
|
4880
3294
|
}
|
|
4881
3295
|
return args.catalogComponents;
|
|
4882
3296
|
}
|
|
4883
|
-
|
|
3297
|
+
function dominantString(values) {
|
|
3298
|
+
const counts = /* @__PURE__ */ new Map();
|
|
3299
|
+
for (const value of values) {
|
|
3300
|
+
const trimmed = typeof value === "string" ? value.trim() : "";
|
|
3301
|
+
if (!trimmed) continue;
|
|
3302
|
+
counts.set(trimmed, (counts.get(trimmed) ?? 0) + 1);
|
|
3303
|
+
}
|
|
3304
|
+
const ranked = [...counts.entries()].sort(
|
|
3305
|
+
(a, b) => b[1] - a[1] || a[0].localeCompare(b[0])
|
|
3306
|
+
);
|
|
3307
|
+
if (ranked.length === 0) return void 0;
|
|
3308
|
+
if (ranked[1] && ranked[1][1] === ranked[0][1]) return void 0;
|
|
3309
|
+
return ranked[0][0];
|
|
3310
|
+
}
|
|
3311
|
+
function inferDesignSystemMetadataFromComponents(components) {
|
|
3312
|
+
const packageName = dominantString(
|
|
3313
|
+
components.map((component) => component.packageName)
|
|
3314
|
+
);
|
|
3315
|
+
const packageComponents = packageName ? components.filter((component) => component.packageName === packageName) : components;
|
|
3316
|
+
const importPath = dominantString(
|
|
3317
|
+
packageComponents.map(
|
|
3318
|
+
(component) => component.importPath ?? component.packageName
|
|
3319
|
+
)
|
|
3320
|
+
);
|
|
3321
|
+
return {
|
|
3322
|
+
packageName: packageName ?? null,
|
|
3323
|
+
importPath: importPath ?? packageName ?? null
|
|
3324
|
+
};
|
|
3325
|
+
}
|
|
3326
|
+
var TOKEN_CATEGORY_ALIASES = {
|
|
4884
3327
|
color: ["color", "colors", "accent", "background", "foreground", "danger", "brand"],
|
|
4885
3328
|
spacing: ["spacing", "space", "padding", "margin", "gap", "inset"],
|
|
4886
3329
|
typography: ["typography", "font", "text", "copy", "line-height", "letter"],
|
|
@@ -4911,7 +3354,7 @@ function canonicalizeTokenCategory(token) {
|
|
|
4911
3354
|
].map(normalizeValue).filter(Boolean);
|
|
4912
3355
|
for (const candidate of candidates) {
|
|
4913
3356
|
for (const [canonical, aliases] of Object.entries(
|
|
4914
|
-
|
|
3357
|
+
TOKEN_CATEGORY_ALIASES
|
|
4915
3358
|
)) {
|
|
4916
3359
|
if (candidate === canonical || aliases.some(
|
|
4917
3360
|
(alias) => candidate === alias || candidate.includes(alias) || alias.includes(candidate)
|
|
@@ -4924,7 +3367,7 @@ function canonicalizeTokenCategory(token) {
|
|
|
4924
3367
|
}
|
|
4925
3368
|
function groupTokens(flat) {
|
|
4926
3369
|
const categories = {};
|
|
4927
|
-
const normalizedFlat = (flat ?? []).map((token) => {
|
|
3370
|
+
const normalizedFlat = (flat ?? []).filter((token) => !isGarbageToken(token)).map((token) => {
|
|
4928
3371
|
const category = canonicalizeTokenCategory(token);
|
|
4929
3372
|
const normalized = {
|
|
4930
3373
|
name: token.name,
|
|
@@ -5055,6 +3498,7 @@ var CloudCatalogAdapter = class {
|
|
|
5055
3498
|
constructor(options) {
|
|
5056
3499
|
this.options = options;
|
|
5057
3500
|
}
|
|
3501
|
+
options;
|
|
5058
3502
|
name = "cloud";
|
|
5059
3503
|
async load(_projectRoot) {
|
|
5060
3504
|
const headers = {
|
|
@@ -5073,14 +3517,19 @@ var CloudCatalogAdapter = class {
|
|
|
5073
3517
|
}
|
|
5074
3518
|
const raw = await response.json();
|
|
5075
3519
|
const validateFixRaw = validateFixResponse && validateFixResponse.ok ? await validateFixResponse.json() : void 0;
|
|
5076
|
-
const designSystem = mergeDesignSystemMetadata(
|
|
5077
|
-
raw.designSystem,
|
|
5078
|
-
validateFixRaw?.content?.designSystem
|
|
5079
|
-
);
|
|
5080
3520
|
const sourceComponents = chooseComponentSource({
|
|
5081
3521
|
catalogComponents: raw.components ?? [],
|
|
5082
3522
|
contextComponents: validateFixRaw?.content?.components ?? []
|
|
5083
3523
|
});
|
|
3524
|
+
const inferredDesignSystem = inferDesignSystemMetadataFromComponents(sourceComponents);
|
|
3525
|
+
const designSystem = mergeDesignSystemMetadata(
|
|
3526
|
+
{
|
|
3527
|
+
...raw.designSystem,
|
|
3528
|
+
packageName: raw.designSystem?.packageName ?? inferredDesignSystem.packageName,
|
|
3529
|
+
importPath: raw.designSystem?.importPath ?? inferredDesignSystem.importPath
|
|
3530
|
+
},
|
|
3531
|
+
validateFixRaw?.content?.designSystem
|
|
3532
|
+
);
|
|
5084
3533
|
const components = Object.fromEntries(
|
|
5085
3534
|
sourceComponents.map((component) => [
|
|
5086
3535
|
component.componentKey,
|
|
@@ -5147,7 +3596,7 @@ var CloudCatalogAdapter = class {
|
|
|
5147
3596
|
};
|
|
5148
3597
|
|
|
5149
3598
|
// src/adapters/bundle.ts
|
|
5150
|
-
import { existsSync as
|
|
3599
|
+
import { existsSync as existsSync6 } from "fs";
|
|
5151
3600
|
import { readFile as readFile2 } from "fs/promises";
|
|
5152
3601
|
import { dirname as dirname3, resolve as resolve2 } from "path";
|
|
5153
3602
|
import {
|
|
@@ -5155,6 +3604,7 @@ import {
|
|
|
5155
3604
|
bundleManifestSchema,
|
|
5156
3605
|
bundleTokenFileSchema
|
|
5157
3606
|
} from "@fragments-sdk/core";
|
|
3607
|
+
import { BRAND as BRAND3 } from "@fragments-sdk/core";
|
|
5158
3608
|
async function readJsonFile(path, parser, label) {
|
|
5159
3609
|
const content = await readFile2(path, "utf-8");
|
|
5160
3610
|
try {
|
|
@@ -5273,7 +3723,7 @@ var BundleAdapter = class {
|
|
|
5273
3723
|
const manifests = this.discover(projectRoot);
|
|
5274
3724
|
if (manifests.length === 0) {
|
|
5275
3725
|
throw new Error(
|
|
5276
|
-
`No ${
|
|
3726
|
+
`No ${BRAND3.dataDir}/${BRAND3.manifestFile} found. Run \`${BRAND3.cliCommand} context install --cloud\` or commit a Fragments bundle into your workspace.`
|
|
5277
3727
|
);
|
|
5278
3728
|
}
|
|
5279
3729
|
const manifestPath = manifests[0];
|
|
@@ -5285,7 +3735,7 @@ var BundleAdapter = class {
|
|
|
5285
3735
|
const bundleDir = dirname3(manifestPath);
|
|
5286
3736
|
const repoRoot = dirname3(bundleDir);
|
|
5287
3737
|
const tokensPath = resolve2(bundleDir, "tokens.json");
|
|
5288
|
-
const tokensFile =
|
|
3738
|
+
const tokensFile = existsSync6(tokensPath) ? await readJsonFile(tokensPath, bundleTokenFileSchema, "bundle tokens") : void 0;
|
|
5289
3739
|
const components = Object.fromEntries(
|
|
5290
3740
|
await Promise.all(
|
|
5291
3741
|
Object.values(manifest.components).map(async (entry) => {
|
|
@@ -5331,7 +3781,7 @@ var BundleAdapter = class {
|
|
|
5331
3781
|
const snapshot = validateSnapshot({
|
|
5332
3782
|
schemaVersion: 1,
|
|
5333
3783
|
sourceType: "bundle",
|
|
5334
|
-
sourceLabel: `${
|
|
3784
|
+
sourceLabel: `${BRAND3.dataDir}/${BRAND3.manifestFile}`,
|
|
5335
3785
|
capabilities: buildCapabilities({
|
|
5336
3786
|
components,
|
|
5337
3787
|
tokens
|
|
@@ -5382,7 +3832,7 @@ function resolveCloudUrl2(fileConfig) {
|
|
|
5382
3832
|
return fileConfig?.cloud?.url ?? process.env.FRAGMENTS_CLOUD_URL;
|
|
5383
3833
|
}
|
|
5384
3834
|
function hasTsProject(projectRoot) {
|
|
5385
|
-
return
|
|
3835
|
+
return existsSync7(join6(projectRoot, "tsconfig.json")) || existsSync7(join6(projectRoot, "tsconfig.app.json"));
|
|
5386
3836
|
}
|
|
5387
3837
|
function resolveDataAdapter(config, fileConfig) {
|
|
5388
3838
|
const source = config.source ?? fileConfig?.source ?? "auto";
|
|
@@ -5396,7 +3846,7 @@ function resolveDataAdapter(config, fileConfig) {
|
|
|
5396
3846
|
case "cloud":
|
|
5397
3847
|
if (!cloudApiKey) {
|
|
5398
3848
|
throw new Error(
|
|
5399
|
-
"Cloud source requires a Cloud API key.
|
|
3849
|
+
"Cloud source requires a Cloud API key. Pass --cloud-api-key, set FRAGMENTS_API_KEY, or configure cloud.apiKey in ds-mcp.config.json."
|
|
5400
3850
|
);
|
|
5401
3851
|
}
|
|
5402
3852
|
return {
|
|
@@ -5440,7 +3890,142 @@ function resolveDataAdapter(config, fileConfig) {
|
|
|
5440
3890
|
}
|
|
5441
3891
|
}
|
|
5442
3892
|
function resolveSearchApiKey(config, fileConfig) {
|
|
5443
|
-
return config.searchApiKey ??
|
|
3893
|
+
return config.searchApiKey ?? fileConfig?.vectorSearch?.apiKey;
|
|
3894
|
+
}
|
|
3895
|
+
|
|
3896
|
+
// src/spec-schema.ts
|
|
3897
|
+
var UI_SPEC_SCHEMA_URI = "fragments://schemas/govern.schema";
|
|
3898
|
+
var UI_SPEC_SCHEMA_NAME = "govern.schema";
|
|
3899
|
+
var UI_SPEC_SCHEMA_MIME_TYPE = "application/schema+json";
|
|
3900
|
+
var UI_SPEC_SCHEMA_RESOURCE = {
|
|
3901
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
3902
|
+
$id: UI_SPEC_SCHEMA_URI,
|
|
3903
|
+
title: "Fragments UI Spec",
|
|
3904
|
+
description: "Input shape accepted by the Fragments govern and validate_and_fix MCP tools.",
|
|
3905
|
+
type: "object",
|
|
3906
|
+
required: ["nodes"],
|
|
3907
|
+
additionalProperties: false,
|
|
3908
|
+
properties: {
|
|
3909
|
+
root: {
|
|
3910
|
+
type: "string",
|
|
3911
|
+
description: "Optional node id to treat as the root. When omitted, top-level nodes are evaluated in order."
|
|
3912
|
+
},
|
|
3913
|
+
metadata: {
|
|
3914
|
+
type: "object",
|
|
3915
|
+
description: "Optional caller metadata. The validator stores and echoes only fields that downstream tools understand.",
|
|
3916
|
+
additionalProperties: true
|
|
3917
|
+
},
|
|
3918
|
+
nodes: {
|
|
3919
|
+
type: "array",
|
|
3920
|
+
description: "Top-level UI nodes. Each node names a component type and may carry props plus nested children.",
|
|
3921
|
+
items: { $ref: "#/$defs/node" }
|
|
3922
|
+
}
|
|
3923
|
+
},
|
|
3924
|
+
$defs: {
|
|
3925
|
+
node: {
|
|
3926
|
+
type: "object",
|
|
3927
|
+
required: ["type"],
|
|
3928
|
+
additionalProperties: false,
|
|
3929
|
+
properties: {
|
|
3930
|
+
id: {
|
|
3931
|
+
type: "string",
|
|
3932
|
+
description: "Stable caller-provided id. Used in violations and fixedSpec patches."
|
|
3933
|
+
},
|
|
3934
|
+
type: {
|
|
3935
|
+
type: "string",
|
|
3936
|
+
description: 'Design-system component name, including compound names such as "DatePicker.Trigger".'
|
|
3937
|
+
},
|
|
3938
|
+
props: {
|
|
3939
|
+
type: "object",
|
|
3940
|
+
description: 'Component props. Put simple text labels in props.children, e.g. { "children": "Save" }.',
|
|
3941
|
+
additionalProperties: true
|
|
3942
|
+
},
|
|
3943
|
+
children: {
|
|
3944
|
+
description: "Nested child nodes, or a string text child for leaf content. Prefer props.children for simple button labels.",
|
|
3945
|
+
oneOf: [
|
|
3946
|
+
{ type: "string" },
|
|
3947
|
+
{
|
|
3948
|
+
type: "array",
|
|
3949
|
+
items: {
|
|
3950
|
+
oneOf: [{ type: "string" }, { $ref: "#/$defs/node" }]
|
|
3951
|
+
}
|
|
3952
|
+
}
|
|
3953
|
+
]
|
|
3954
|
+
}
|
|
3955
|
+
}
|
|
3956
|
+
}
|
|
3957
|
+
},
|
|
3958
|
+
examples: {
|
|
3959
|
+
valid: [
|
|
3960
|
+
{
|
|
3961
|
+
nodes: [
|
|
3962
|
+
{
|
|
3963
|
+
id: "save",
|
|
3964
|
+
type: "Button",
|
|
3965
|
+
props: { variant: "primary", children: "Save" }
|
|
3966
|
+
}
|
|
3967
|
+
]
|
|
3968
|
+
},
|
|
3969
|
+
{
|
|
3970
|
+
root: "card",
|
|
3971
|
+
metadata: { source: "agent-draft" },
|
|
3972
|
+
nodes: [
|
|
3973
|
+
{
|
|
3974
|
+
id: "card",
|
|
3975
|
+
type: "Card",
|
|
3976
|
+
props: {},
|
|
3977
|
+
children: [
|
|
3978
|
+
{
|
|
3979
|
+
id: "title",
|
|
3980
|
+
type: "Heading",
|
|
3981
|
+
props: { level: 2, children: "Billing" }
|
|
3982
|
+
},
|
|
3983
|
+
{
|
|
3984
|
+
id: "submit",
|
|
3985
|
+
type: "Button",
|
|
3986
|
+
props: { children: "Update plan" }
|
|
3987
|
+
}
|
|
3988
|
+
]
|
|
3989
|
+
}
|
|
3990
|
+
]
|
|
3991
|
+
}
|
|
3992
|
+
],
|
|
3993
|
+
invalid: [
|
|
3994
|
+
{
|
|
3995
|
+
reason: "String event handlers are blocked by safety/no-string-handlers.",
|
|
3996
|
+
spec: {
|
|
3997
|
+
nodes: [
|
|
3998
|
+
{
|
|
3999
|
+
id: "danger",
|
|
4000
|
+
type: "Button",
|
|
4001
|
+
props: { children: "Save", onClick: 'alert("saved")' }
|
|
4002
|
+
}
|
|
4003
|
+
]
|
|
4004
|
+
}
|
|
4005
|
+
},
|
|
4006
|
+
{
|
|
4007
|
+
reason: "Raw CSS values should use design tokens; call tokens.suggest before writing them.",
|
|
4008
|
+
spec: {
|
|
4009
|
+
nodes: [
|
|
4010
|
+
{
|
|
4011
|
+
id: "panel",
|
|
4012
|
+
type: "Box",
|
|
4013
|
+
props: { style: { padding: "6px" } }
|
|
4014
|
+
}
|
|
4015
|
+
]
|
|
4016
|
+
}
|
|
4017
|
+
}
|
|
4018
|
+
]
|
|
4019
|
+
},
|
|
4020
|
+
notes: [
|
|
4021
|
+
"Use component names from the active catalog; unknown components fail components/allow.",
|
|
4022
|
+
"Use props.children for simple text labels, especially Button text.",
|
|
4023
|
+
"Do not put JavaScript source strings in event handler props.",
|
|
4024
|
+
"Run validate_and_fix after govern when the verdict asks for revision or deterministic repair."
|
|
4025
|
+
]
|
|
4026
|
+
};
|
|
4027
|
+
function serializeUiSpecSchema() {
|
|
4028
|
+
return JSON.stringify(UI_SPEC_SCHEMA_RESOURCE, null, 2);
|
|
5444
4029
|
}
|
|
5445
4030
|
|
|
5446
4031
|
// src/server.ts
|
|
@@ -5452,12 +4037,13 @@ var TOOL_DEFINITION_BY_KEY = new Map(
|
|
|
5452
4037
|
function createMcpServer(config) {
|
|
5453
4038
|
const server = new Server(
|
|
5454
4039
|
{
|
|
5455
|
-
name: `${
|
|
4040
|
+
name: `${BRAND4.nameLower}-mcp`,
|
|
5456
4041
|
version: MCP_SERVER_VERSION
|
|
5457
4042
|
},
|
|
5458
4043
|
{
|
|
5459
4044
|
capabilities: {
|
|
5460
4045
|
tools: { listChanged: true },
|
|
4046
|
+
resources: { listChanged: false },
|
|
5461
4047
|
logging: {}
|
|
5462
4048
|
}
|
|
5463
4049
|
}
|
|
@@ -5488,21 +4074,20 @@ function createMcpServer(config) {
|
|
|
5488
4074
|
let loadDataPromise = null;
|
|
5489
4075
|
let resolvedRoot = null;
|
|
5490
4076
|
let resolveProjectRootPromise = null;
|
|
5491
|
-
let componentIndex = null;
|
|
5492
|
-
let blockIndex = null;
|
|
5493
|
-
let tokenIndex = null;
|
|
5494
4077
|
async function resolveProjectRoot() {
|
|
5495
4078
|
if (resolvedRoot) return resolvedRoot;
|
|
5496
4079
|
if (resolveProjectRootPromise) return resolveProjectRootPromise;
|
|
5497
4080
|
resolveProjectRootPromise = (async () => {
|
|
5498
|
-
|
|
5499
|
-
|
|
5500
|
-
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
|
|
4081
|
+
if (server.getClientCapabilities()?.roots) {
|
|
4082
|
+
try {
|
|
4083
|
+
const result2 = await server.listRoots();
|
|
4084
|
+
if (result2.roots?.length > 0) {
|
|
4085
|
+
const rootUri = result2.roots[0].uri;
|
|
4086
|
+
resolvedRoot = fileURLToPath(rootUri);
|
|
4087
|
+
return resolvedRoot;
|
|
4088
|
+
}
|
|
4089
|
+
} catch {
|
|
5504
4090
|
}
|
|
5505
|
-
} catch {
|
|
5506
4091
|
}
|
|
5507
4092
|
resolvedRoot = config.projectRoot;
|
|
5508
4093
|
return resolvedRoot;
|
|
@@ -5519,11 +4104,6 @@ function createMcpServer(config) {
|
|
|
5519
4104
|
loadDataPromise = (async () => {
|
|
5520
4105
|
const projectRoot = await resolveProjectRoot();
|
|
5521
4106
|
const loaded = await adapter.load(projectRoot);
|
|
5522
|
-
const allFragments = Object.values(loaded.components);
|
|
5523
|
-
const allBlocks = Object.values(loaded.blocks ?? {});
|
|
5524
|
-
componentIndex = buildComponentIndex(allFragments);
|
|
5525
|
-
blockIndex = allBlocks.length > 0 ? buildBlockIndex(allBlocks) : null;
|
|
5526
|
-
tokenIndex = loaded.tokens && loaded.tokens.total > 0 ? buildTokenIndex(loaded.tokens) : null;
|
|
5527
4107
|
cachedData = loaded;
|
|
5528
4108
|
return loaded;
|
|
5529
4109
|
})();
|
|
@@ -5538,21 +4118,49 @@ function createMcpServer(config) {
|
|
|
5538
4118
|
return {
|
|
5539
4119
|
tools: registry.listTools(
|
|
5540
4120
|
{
|
|
5541
|
-
hasViewer:
|
|
5542
|
-
hasPlayground:
|
|
4121
|
+
hasViewer: false,
|
|
4122
|
+
hasPlayground: false,
|
|
5543
4123
|
capabilities: data.capabilities
|
|
5544
4124
|
},
|
|
5545
4125
|
TOOLS
|
|
5546
4126
|
)
|
|
5547
4127
|
};
|
|
5548
4128
|
});
|
|
4129
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
4130
|
+
resources: [
|
|
4131
|
+
{
|
|
4132
|
+
uri: UI_SPEC_SCHEMA_URI,
|
|
4133
|
+
name: UI_SPEC_SCHEMA_NAME,
|
|
4134
|
+
title: "Fragments govern UI spec schema",
|
|
4135
|
+
description: "JSON schema and examples for the spec argument accepted by govern and validate_and_fix.",
|
|
4136
|
+
mimeType: UI_SPEC_SCHEMA_MIME_TYPE
|
|
4137
|
+
}
|
|
4138
|
+
]
|
|
4139
|
+
}));
|
|
4140
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
4141
|
+
if (request.params.uri !== UI_SPEC_SCHEMA_URI) {
|
|
4142
|
+
throw new McpError(
|
|
4143
|
+
ErrorCode.InvalidParams,
|
|
4144
|
+
`Unknown resource URI: ${request.params.uri}`
|
|
4145
|
+
);
|
|
4146
|
+
}
|
|
4147
|
+
return {
|
|
4148
|
+
contents: [
|
|
4149
|
+
{
|
|
4150
|
+
uri: UI_SPEC_SCHEMA_URI,
|
|
4151
|
+
mimeType: UI_SPEC_SCHEMA_MIME_TYPE,
|
|
4152
|
+
text: serializeUiSpecSchema()
|
|
4153
|
+
}
|
|
4154
|
+
]
|
|
4155
|
+
};
|
|
4156
|
+
});
|
|
5549
4157
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
5550
4158
|
const { name, arguments: args } = request.params;
|
|
5551
4159
|
const data = await loadData();
|
|
5552
4160
|
const toolContext = {
|
|
5553
4161
|
data,
|
|
5554
4162
|
config: mergedConfig,
|
|
5555
|
-
indexes: { componentIndex, blockIndex, tokenIndex },
|
|
4163
|
+
indexes: { componentIndex: null, blockIndex: null, tokenIndex: null },
|
|
5556
4164
|
mcp: {
|
|
5557
4165
|
server,
|
|
5558
4166
|
clientCapabilities: server.getClientCapabilities()
|
|
@@ -5567,8 +4175,8 @@ function createMcpServer(config) {
|
|
|
5567
4175
|
return "your-component-library";
|
|
5568
4176
|
}
|
|
5569
4177
|
const root = resolvedRoot ?? config.projectRoot;
|
|
5570
|
-
const packageJsonPath =
|
|
5571
|
-
if (
|
|
4178
|
+
const packageJsonPath = join7(root, "package.json");
|
|
4179
|
+
if (existsSync8(packageJsonPath)) {
|
|
5572
4180
|
try {
|
|
5573
4181
|
const content = readFileSync6(packageJsonPath, "utf-8");
|
|
5574
4182
|
const pkg = JSON.parse(content);
|
|
@@ -5632,11 +4240,6 @@ function createSandboxServer() {
|
|
|
5632
4240
|
|
|
5633
4241
|
export {
|
|
5634
4242
|
loadConfigFile,
|
|
5635
|
-
SYNONYM_MAP,
|
|
5636
|
-
USE_CASE_TOKEN_CATEGORIES,
|
|
5637
|
-
MINIMUM_SCORE_THRESHOLD,
|
|
5638
|
-
BLOCK_BOOST_PER_OCCURRENCE,
|
|
5639
|
-
DEFAULT_ENDPOINTS,
|
|
5640
4243
|
CORE_TOOLS,
|
|
5641
4244
|
VIEWER_TOOLS,
|
|
5642
4245
|
INFRA_TOOLS,
|
|
@@ -5657,4 +4260,4 @@ export {
|
|
|
5657
4260
|
startMcpServer,
|
|
5658
4261
|
createSandboxServer
|
|
5659
4262
|
};
|
|
5660
|
-
//# sourceMappingURL=chunk-
|
|
4263
|
+
//# sourceMappingURL=chunk-KGFM5SCE.js.map
|